I strongly believe that a lot of people do not do evil intentionally (although, some people do). A lot of problems in today’s world are because of the misunderstanding and different views on different topics, combined with human emotions, differences in cultures etc.
In particular, I think that the aphorism:
The road to hell is paved with good intentions
Is quite accurate. Thus, one of the interpretations of the aphorism is that by enforcing certain policies or behavior that we think are generally right for most people, we often can make the overall situation worse, unintentionally.
In particular, the so-called “single assert rule” stated in the Clean Code I often find confusing and even impractical in a lot of cases in software development in general.
This rule is sometimes interpreted in a way, that a test should fail because of a single reason. What “single reason” means exactly is quite vague, so it is quite difficult to argue with this statement.
Nevertheless, a lot of developers interpret this rule as having a single assertions statement in the test. And my advice to you is to not follow it strictly, and deviate from it if you see fit.
Why?
Because the Software is quite diverse and complicated. Let’s imagine that we’re writing a factory class, like below:
public class ConfigurationFactory {
public static Configuration createInstance() {
String javaVersion = System.getProperty("java.version");
String user = System.getenv("USER");
int cpusAvailable = Runtime.getRuntime().availableProcessors();
return new Configuration(javaVersion, user, cpusAvailable);
}
static class Configuration {
String javaVersion;
String user;
int cpusAvailable;
// constructor, getters etc.
}
}
The example above is a Java code that constitutes the factory, that creates an object, configuring it beforehand. This pattern, at least semantically, is quite common in software in general.
Now, let’s assume that I want to test the createInstance() factory method. So, here is the question – what behavior exactly do I want to test?
Well, I want to test that the object that is constructed is initialized with the state that I consider valid in a given test environment. Sound reasonable.
Now, here is another question – how do I test this state?
Following the Rule
If we follow the “single assert rule”, then I would have to have a single assertion, a single check inside the test. So, it is quite obvious, that if I do not want to sacrifice the quality of the testing, I need to create an instance of the Configuration myself. And then check if the manually constructed Configuration in the test equals by its internal state to the Configuration provided by the factory.
Problem 1.
In the example above, because of the brevity, the object is quite small. But if it is large, then the construction of the object with 30 fields would be insanely:
- Boring (and that one is huge, far more important than people think)
- Error prone
- Difficult to maintain (imagine how fun would be adding 31st field)
And the core of this complexity, is that this object was not intended to be created manually. It just wasn’t. That is why we have a factory in the first place. Moreover, the constructor may not even be public.
Another problem is that I might want to test only a portion of fields for the exact match. For instance, if the factory generates the random UUID, I cannot possibly manually create the exact same UUID in the test code – I need to just check that UUID is set by the factory and is a valid by spec.
Problem 2.
Well, even if we assume, that the object is lightweight, and we can allow ourselves to construct it manually etc., then another problem arises.
You see, I do not know the programming language in which you’re writing your code, but in Java, when two references are compared for equality, by default, the comparison just checks whether two references point to the exact same object on the OS process heap.
Now, the problem in our case, is that creating a separate object manually would not work for us, that is obvious. Well, what we can do? We can just override the equals()/hashcode() in Java, in order for comparison to happen based on fields, and not just references:
static class Configuration {
String javaVersion;
String user;
int cpusAvailable;
// constructor, getters etc.
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Configuration that = (Configuration) o;
return cpusAvailable == that.cpusAvailable && Objects.equals(javaVersion, that.javaVersion) && Objects.equals(user,
that.user);
}
@Override
public int hashCode() {
return Objects.hash(javaVersion, user, cpusAvailable);
}
}
So, how is that, good? No, awful.
Why is it so bad? Because our test code dictates how our production code should look like. This should never happen. In general, the problem is that such an approach causes the code abstractions to evolve in the way that is unnatural, and this results in some really wired API for users at the end. So, this should be avoided at all costs.
Solution.
Just write multiple asserts, if you see fit. Do not be afraid. You won’t go to jail for it, I promise. It is not that hard:
@Test
void testCreateInstance() {
Configuration instance = ConfigurationFactory.createInstance();
assertSoftly(softAssertions -> {
softAssertions.assertThat(instance.getUser()).isEqualTo("my_user");
softAssertions.assertThat(instance.getJavaVersion()).isEqualTo("17.0.4.1");
});
}
In the result, you not only does not have to create this object yourself, but you can check the fields you’re actually interested in by the conditions that make sense (recall the UUID example). You’re not forcing your Configuration class to re-implement the equals()/hashcode() or adapt any behavior that is required only for tests.
So, as often true in life, just adhere to common sense when you make decisions. Have a nice day!





