Phil Karlton once said:
“There are only two hard things in Computer Science: cache invalidation and naming things.”
Absolutely true. Today, most of us are pretty conscious about giving things the right name, like classes, methods, fields and local variables, and naming things correctly is a vital part of any code review.
While this is a good thing, we tend to forget to apply the same standards to our test code.
Let’s write some production code…
Consider the following class that we want to test:
public class GreetingService { public String greet(String username) { return "Hello, " + username; } }
…and then test it!
Let’s write a unit test that verifies the behaviour. I’m using AssertJ here, which I highly recommend for readable assertions:
public class GreetingServiceTest { private GreetingService sut = new GreetingService(); @Test public void test() { assertThat(sut.greet("John")).isEqualTo("Hello, John"); } }
Nice! This test() method verifies the behaviour correctly.
Now, what if the production code would change and the test would fail? That would require me to:
- Explicitly associate the test method to the “greet” method in the GreetingService;
- Find out what goes wrong;
- And more importantly: I have to dig into the implementation of the test method to find out what is being tested, so I will get a hint on what part of the production code might be broken!
Is this a contrived example? Well, no, I encounter this in real-world applications frequently.
So, let’s see if we can name the test in such a way it will be clearer what expected behaviour is actually failing!
Step 1: include the class name
@Test public void testGreetingService() { assertThat(sut.greet("John")).isEqualTo("Hello, John"); }
I see this pattern frequently in classes under test with only one public method.
Really, it still doesn’t tell you anything, right?
Except the unit test is working on the GreetingService class, which was already pretty obvious.
So, let’s make it a little more specific.
Step 2: reference the method name
@Test public void testGreet() { assertThat(sut.greet("John")).isEqualTo("Hello, John"); }
This is a little better. At least, now I understand what method fails.
But still, I have no idea what part of the behaviour is broken without looking at the internals of the test implementation.
So, let’s reflect the expected behaviour of the method in the test method name:
Step 3: specify expected behaviour, not implementation details
@Test public void whenGreetingIsCalledWithUserNameThenResponseShouldBeHelloWithTheUserName() { assertThat(sut.greet("John")).isEqualTo("Hello, John"); }
Ok, now the expectations of the test are clearly defined in the test method name: when greeting is called with username then response should be hello with the user name.
The most important thing here is that:
I don’t need to delve into the test method body to figure out what’s wrong with the production code. It just TELLS ME by its name what expected behaviour fails.
Pretty specific defined behaviour, right? Yes, but it’s still a bit unreadable. Now, this is a matter of taste, but I like Snake Case instead of camelCase to make things more readable:
Step 4: make things readable
@Test public void when_greeting_is_called_with_user_name_then_response_should_be_hello_with_the_user_name() { assertThat(sut.greet("John")).isEqualTo("Hello, John"); }
Very good. But, when you look at the production code, there are two separate behaviours to test:
- The greet method returns a “Hello,” prefix
- The prefix is followed by the username
The behaviour of the method has two aspects, not one. To find out what part of the behaviour is wrong, it is better to split the behaviour assertion into two tests:
Step 5: test one thing at a time
@Test public void when_greeting_is_called_with_user_name_then_response_should_start_with_hello() { assertThat(sut.greet("John")).startsWith("Hello,"); } @Test public void when_greeting_is_called_with_user_name_then_response_should_contain_the_user_name() { assertThat(sut.greet("John")).endsWith("John"); }
This way, we test two different aspects of the behaviour in two separate test methods, mirroring the behaviour of the code under test:
- The greet method returns a “Hello,” prefix
- The prefix is followed by the username
This way, if a test fails, we can immediately find out what part of the behaviour breaks, because the test method name clearly echoes what part of the production code fails, without having to look at the implementation of the test method.
Summary
Correctly naming test methods, and testing one thing at a time, will help you tremendously in finding out what is wrong with your production code, without the need to inspect the implementation of your test methods. So:
- Specify specific behaviour in your test method names;
- Not the implementation details;
- And make sure you Test one thing at a time.
This way, refactoring and debugging your production code becomes much easier!
If you want to look at some examples I explained here, go to the GitHub project at https://github.com/MichelSchudel/test-naming-demo
Hi Michel, great blog, I couldn’t agree more. If you like Behaviour Driven Development, you may want to have a look at the Spock framework, (spockframework.org). It allows _very_ readable test names, which may even include spaces, punctuation and test-variables! Plus it has lots of other specification/test goodness to offer…
Worth mentioning is the new @DisplayName that comes with JUnit 5, which gives another way to name your tests properly, but in a much more readable way. The newest Surefire plugin (3.0.0-M4) should support showing these names in the console as well (see https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html#Surefire_Extensions_and_Reports_Configuration_for_.40DisplayName)
100% agree and I like the abbreviation ‘sut’ 🙂
I think you like Kotlin as you can create method names with backticks. For example:
@Test
fun `toNonTankerAreaRequest should map attachments json string to null when attachment list is null`() {
val mooringApplication = fullMooringApplicationWithoutAttachments()
val berthVisit = BerthVisit(berth = mooringApplication.berth!!, externalId = mooringApplication.berthVisitId!!)
val nonTankerAreaRequest = mooringApplication.toNonTankerAreaRequest(“agentShortName”, berthVisit)
assertThat(nonTankerAreaRequest.attachmentList).isNullOrEmpty()
}