Codementor Events

Unit testing best practices — Naming

Published Feb 21, 2023
Unit testing best practices — Naming

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 about 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.

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 on 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 in 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_HasOwner_ThrowsGDPRException
  • ScanCreature_PokemonGenerationIsDifferentThanPokedexGeneration_ThrowsGenerationException

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.

Discover and read more posts from Henrique Dalcin Dalmas
get started