Organizing and Naming Tests
Introduction
Tests can be valuable documentation, but only if they are well-organized and named. When writing tests, it is important to organize and name them in a way that makes it easy to understand what is being tested and why. This helps developers quickly identify the purpose of each test and makes it easier to maintain and update the test suite.
Your names and structure should make it clear which cases are being tested and the expected behavior. If you see a test failure, you should have a good of the problem just from reading the test name.
Consistent naming is critical to making your test suite easy to understand and maintain. If you have a naming convention, stick to it. If you don't have one, your team should adopt one. When working with an existing test suite, you should follow whatever convention is already in place, regardless of your personal preferences.
Keeping a consistent convention reduces the cognitive load on developers and makes it easier to navigate and understand the test suite which improves its value as documentation.
General Guidelines
Some general guidelines for naming tests include:
- Use a name that can be read by a non-technical person
- Use descriptive names that clearly explain what is being tested
- Avoid referencing implementation details in the test name
- Use underscores or spaces to separate words in the test name
- Include the expected behavior and the state being test in the test name
Non-Technical Names
While programmers are the primary audience for tests, it is still better to use names that can be understood by non-technical people. This allows non-technical stakeholders to understand what is being tested and why, which can be valuable for communication and documentation purposes. It also can reduce the cognitive load on developers who are reading the tests.
Descriptive Names
Test names should be descriptive and clearly explain what is being tested. A good test name should make it clear what the expected behavior is and why it is important. This helps developers quickly understand the purpose of the test and makes it easier to maintain and update the test suite.
Avoid Referencing Implementation Details
Test names should describe the behavior being tested, not the implementation details. If you find yourself including class names, method names, or variable names in your test names, you are likely violating this principle. This ties your tests directly to the implementation, which will slow you down when refactoring.
Separating Words
While using camelCase
or TitleCase
is common in programming, it is better to use underscores or spaces to separate words in test names. Test names tend to be significantly longer than variable or method names, and using underscores or spaces can make them easier to read and understand.
You should not feel compelled to use the same convention with test functions that you use with other functions. Tests are never called directly and should be handled differently.
Include State and Expected Behavior
The test name should provide enough information to understand what is being tested and why. This includes the state being tested and the expected behavior. By including it in the test name, you can quickly identify the root cause of a test failure just by reading the name.
While it is important to have a consistent naming convention, it is not worth spending a lot of time debating the perfect test name. The test name has value as a form of documentation, but it is not the most important part of the test. Focus on writing good tests that cover the important cases and provide value to the team.
I've seen teams spend longer debating a test name than it took to write the test itself. This is a waste of time and energy. If you can't agree on a name quickly, just pick one and move on. You can always change it later if you need to.
Example Conventions
The following are some common conventions for naming tests. You should choose a convention that works best for your team and stick to it. Consistency is key when naming tests. Avoid long debates over test names or mixing conventions within the same test suite. Remember that the test name is a form of documentation, and it should be clear and easy to understand. However, it brings limited value on its own, so don't spend too much time on it.
The conventions here assume that you are using a test framework that does not allow for spaces in test names. If your test framework allows spaces, you can use them instead of underscores.
Should_ExpectedBehavior_When_StateUnderTest
This convention is my personal favorite. Starting the test name with should
may feel a little repetitive, but it makes the ordering more obvious and acts as a reminder that the test name needs to be focused on the expected behavior.
Java
public void should_returnZero_when_givenEmptyString()
public void should_return_zero_when_given_an_empty_string()
public void should_throwDataBaseAccessException_when_databaseIsUnavailable()
JUnit 5 offers a @DisplayName
annotation that allows you to use spaces in test names. This can make the test names more readable, but it is not supported by all test runners.
@DisplayName("Should return zero when given an empty string")
public void should_returnZero_whenGivenEmptyString()
Kotlin
fun `should return zero when given an empty string`()
fun `should throw DataBaseAccessException when the database is unavailable`()
JavaScript
it('should return zero when given an empty string', () => {});
When_StateUnderTest_Expect_ExpectedBehavior
This convention is similar to the previous one but reverses the order of the state under test and the expected behavior. This can be useful if you prefer to list the state first or if you find it easier to read.
@DisplayName("When given an empty string expect zero to be returned")
public void when_GivenEmptyString_expect_ReturnZero()
@DisplayName("When the database is unavailable expect a DataBaseAccessException to be thrown")
public void when_databaseIsUnavailable_expect_throwDataBaseAccessException()
Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior
The previous convention can be extended to include preconditions. This can be useful for grouping similar tests together or when the preconditions are important to understand the test. I generally prefer to use nested classes to group related tests, but this can be a good alternative.
Include the Function Being Tested
A common complaint about the previous conventions is that it isn't always clear what function is being tested. Including the function name in the test name can help with this. However, this ties the test name to the implementation, which can make it harder to refactor. Changing the function name will now require changing all the test names as well.
The typical approach is to prefix the test name with the function name, and then use one of the previous conventions.
MethodName_ExpectedBehavior_StateUnderTest
MethodName_StateUnderTest_ExpectedBehavior
@DisplayName("calculateTotal should return zero when given an empty list")
public void calculateTotal_whenGivenEmptyList_returnZero()
@DisplayName("calculateTotal should throw IllegalArgumentException when given a negative number")
public void calculateTotal_whenGivenNegativeNumber_throwIllegalArgumentException()
@DisplayName("calculateTotal when given an empty list expect zero to be returned")
public void calculateTotal_whenGivenEmptyList_expectReturnZero()
Test Structure
For larger test suites, it can be helpful to group related tests together. This can make it easier to find and understand the tests and can help you identify patterns or common issues. There are several ways to structure your tests, depending on your preference and the test framework you are using.
Nested Classes
One common approach is to use nested classes to group related tests together. This can be useful for organizing tests that are testing the same class or method.
@Nested
@DisplayName("when calculateTotal is called")
inner class CalculateTotalTests {
@Test
fun `should return zero when given an empty list`() {
// test code here
}
@Test
fun `should throw IllegalArgumentException when given a negative number`() {
// test code here
}
}
One advantage of this is that when a test fails, the combined test name will be a full sentence that includes everything you need to know about the test. This can make it easier to identify the problem without having to read the test code.
CalculatorTest > when calculateTotal is called > should return zero when given an empty list
This approach also makes it easier to tell if some tests are missing. If you have a CalculateTotalTests
class, but only one test inside it, you know that you there are cases which aren't being covered. This helps to identify gaps in your test suite and give context that might be missing if you only rely on test coverage reports.
Conventions to Avoid
There are some commonly used conventions that should really be avoided. These conventions encourage bad practices and make it harder to understand and maintain your tests.
Unclear Test Names
Test names should be clear and descriptive. If you find yourself using vague or unclear names, you should consider revising them. A good test name should make it clear what is being tested and why.
For example, consider the following test names:
fun test1()
fun testGetCost1()
fun testCalculateTotal1()
These test names provide no information about what is being tested or why. If you see a test failure, you will have no idea what the problem until you've read the test code.
Test Names that Describe Implementation Details
Test names should describe the behavior being tested, not the implementation details. If you find yourself including class names, method names, or variable names in your test names, you are likely violating this principle. This ties your tests directly to the implementation, which will slow you down when refactoring.
For example, consider the following test name:
@Test
public void testCalculateTotal() {
// test code here
}
This test name describes how the method is implemented, not what it does. If someone changes the implementation of the calculateTotal
method, they will have to update the test name as well, which is unnecessary.
Conclusion
A well organized and named test suite can be a valuable asset to your project. It can serve as documentation, help you identify bugs, and make it easier to refactor your code. Consistent naming conventions are key to making your tests easy to understand and maintain. Choose a convention that works for your team and stick to it. Remember that the test name is just one part of the test, and it is not worth spending too much time debating the perfect name. Focus on writing good tests that cover the important cases and provide value to the team.