I’m pretty sure everybody has already read a sort of batch of advantages of unit testing, so I won’t bore you on this post. Instead, let’s focus on how to build great, maintainable, and easy-to-read unit tests. In this series of posts, I plan on walking you through some of the common unit testing best practices.
Context
So, let’s assume that we’ve just been recruited by our friend Dr. Oak, who is on the verge of finishing his masterpiece, the revolutionary pokédex. Unfortunately, the developers quit the company without creating any unit tests at all. Shame. And being conscious of the importance of unit testing, he asked us to do it for him. Seems good right?
Naming of unit tests
So, let’s begin from the beginning. Yes, my dear reader, choosing a good name is very important to the future of the unit test. Although “TestAdd” and “TestFail” might seem like acceptable choices, they are not perfectly clear, you see. And why is that? Let’s dig into it now. Continuing on our Pokédex unit testing, we’re going to create tests for the “Scan” function. You know, when the user points it at a Pokémon and it spits out a bunch of information. Something that I really would’ve liked in the games.
The first thing about naming is that unit tests should document the software. For example, let’s analyze the code below:
public PokemonDescription ScanCreature(object creature)
{
if (creature is not Pokemon pokemon)
throw new NotAPokemonException("The specified creature is not a Pokémon");
return pokemon.Description;
}
Seems simple enough. So, if we were to create a test for this class, we might think that “ScanWorks” would be clear, so let’s go for it! Oh yeah, if you are wondering what are those comments, it’s regarding the AAA pattern, a very neat way to organize your tests. You can read more about it here.
public void ScanWorks()
{
//Arrange
var pokedex = new Pokedex();
var pokemon = new Pokemon();
//Act
var pokemonDescription = pokedex.ScanCreature(pokemon);
//Assert
Assert.That(pokemonDescription, Is.Not.Null);
}
Perfect right? Yeah, well, I can’t argue that it doesn’t work, but there are some problems with it:
- Without looking at the test code, we don’t clearly know what “works” means. So we can’t look at this class’ unit tests and promptly know what it’s supposed to do.
- What if the ScanCreature method changes something, like its return, for example, and the test breaks? We would have to spend time digging around to discover what the code is supposed to test. Of course, in this case, it’s pretty simple, but I’ve seen unit tests with more than 100 lines. It’s not a pretty sight.
If you’re still not convinced, let me complicate things a bit. Let’s tune our ScanCreature method:
public PokemonDescription ScanCreature(object creature)
{
if (creature is not Pokemon pokemon)
throw new NotAPokemonException("The specified creature is not a Pokémon");
if(pokemon.Generation is null)
throw new GenerationException("Unable to identify the generation of the Pokémon");
if(pokemon.Generation != Generation)
throw new GenerationException("This Pokémon is from a different generation than this pokédex. Consider upgrading to Pokedex PRO.");
if (pokemon.HasOwner)
throw new GDPRException("This Pokémon data is protected by the laws of GDPR");
return pokemon.Description;
}
So, now then, if we would proceed to our simplistic naming approach, we would use something like “ScanThrows”. But you see, we have 3 situations that could throw an exception, how would we name them?
We could name the test methods after the exception!
Although it’s not a completely bad idea, it would not work, since we have 2 cases that would throw a GenerationException. And following the idea that a test should document the code, we would not be able to know when the exception should be thrown.
How the hell do we name them?
That’s simple: we just gotta tell exactly what we are testing. A good naming pattern that I like to follow is this one:
MethodName_Scenario_ExpectedResult
But you don’t have to follow exactly this way. Sometimes, we’ll join a team, which already has a pattern in place, so instead of changing every test name, we should adapt. The important thing is to tell what the test is doing. So, in our case, we would have these test cases:
- ScanCreature_ShouldReturnPokemonDetails
- ScanCreature_PokemonHasNoGeneration_ThrowsGenerationException
- ScanCreature_PokemonGenerationIsDifferentThanPokedexGeneration_ThrowsGenerationException
- ScanCreature_HasOwner_ThrowsGDPRException
Do you see that in the first test case I did not specify the scenario? That’s because I, personally, like to omit it in case it’s the normal/expected method behavior. But of course, we could stick to our pattern and do something like “ScanCreature_NoErrors_ShouldReturnPokemonDetails”.
Conclusion
So, basically, when it comes to naming, we wanna make it as explicit as possible to what the unit test is verifying. Namely, we wanna say what is being tested, what we expect, and when we should expect it.
But… but… my unit test verifies many functionalities! I would have to write a sentence!!!
Yes, my friend, this is something that naming can’t resolve. But be patient, because we will soon approach this topic. So, stay tuned for more unit testing best practices.