Access specifiers in programming languages can be frustrating. Superficially, they seem ok. You use public to mark the methods you want to allow anyone to call, and use private to hide the ones that people shouldn’t call. When you’re writing code, it’s easy enough to make that decision, but there are situations where it would be useful to have more finely grained permissions. Adding tests to existing code is one of them.
Let’s imagine that we have a big class. It’s grown over the years and it has 50+ methods and maybe half as many responsibilities. It creates a few objects internally, and maybe it accesses a global variable or two. This is a realistic scenario. I see it all the time.
When we want to test that class, we often add a testing constructor. This is a constructor that is not meant to be used in production. Rather than creating objects internally, we supply them as arguments to the testing constructor. This works, and it allows us to mock out dependencies that might get in our way when we test. But, what access should the testing constructor have?
Making it public doesn’t seem right. People can create improperly configured objects and use them in production with bad consequences. But, even though this is possible, it doesn’t happen very often. In languages where dependency injection frameworks are used, the presence of wide constructors that accept many arguments is common, and not very confusing. People know that they that these constructors are for configuration or test and they use them appropriately. But still, it seems like it would be nice to have a special access specifier for them.
Another place where access specifiers fall short is refactoring. It’s rather common to extract methods from large public methods. Then you have to decide whether those methods will be public or private. Chances are, they shouldn’t be public, but isn’t it nice to be able to write tests for them? Often the desire to do that is a hint that they should move to a new class, but that takes time. Languages that offer a middle ground, like package private in Java or internal in .NET give us a middle ground that can be very useful. The big question is — how do we tend that middle ground?
Years ago I was visiting a team in a company that was using Java for nearly all of its development. I made the case that they should make a few methods public to order in enable testing. Their tests were in separate packages and there was no other way to get access easily. I was told that making those methods public would be a terrible idea. People in another team, in another part of the organization, could start to use them and we would never be able to change them or deprecate them.
The fear is understandable but the problem that they pointed out is a social problem, not a technical problem. We can use technology to try to solve social problems but often we create more problems in the process. It’s better to have a convention, or an understanding across an organization about what methods and APIs are fair to use. When you have that, and people respect those agreements, you have a healthy cooperative culture, and a lot is possible that wouldn’t be possible otherwise.
Early on, I looked for examples of this line of reasoning and I found one in IDE plugin development. There’s a document called the The Eclipse API Rules of Engagement that explained something the Eclipse developers called soft final. They realized that there were many classes and methods in their API that they could’ve made final, preventing them from being inherited from or overridden by plugin developers, but by doing that, they would be severely impacting people who might have a good reason to do those things in their local context. So, instead they marked them soft final. There is no Java keyword support for this; rather it is a convention which, in essence, says “You can inherit or override these, but aware that we may change them and if we do, it’s your problem, not ours.” In way, it’s like a warranty for a product. Some things that you do void the warranty. You can do them, but you have to accept the consequences. It empowers users of the API while giving them responsibility at the same time. It is also a good way of dealing with Hyrum’s Law.
When working with organizations, I often mention this issue of access control in code and its socio-technical aspects. At the very least, it’s an avenue toward having good working agreements across a code base, but it also points to a type of systems thinking that is useful and helps us make better design decisions.
With regard to testing, consider having a warranty — an understanding that some abstractions, marked with a particular standard marker in a comment, are not meant to be used outside of tests. Note when people void the warranty and have a discussion. If they can agree about the problem and find a solution which respects the integrity of your interface, you are doing cooperative design at an organization-wide level, and with that skill there are many other positive things you can do.