Blog-Archiv

Mittwoch, 4. Dezember 2019

Naming Unit Tests in Java

According to test-driven development you should start with a unit test, not with an implementation. Unit-testing has made its way into most notable programming environments. Still the ways how tests are named and organized are quite different.

This article was inspired by Roy Osherove ("The Art of Unit Testing") that argues about naming standards for unit tests. He proposes

UnitOfWork_StateUnderTest_ExpectedBehavior

as naming convention, where "UnitOfWork" may be the name of the called method, "StateUnderTest" describing its input data, and "ExpectedBehavior" the output result to be asserted, sometimes simply "fail" or "succeed".

→ findNameById_ExistingId_SucceedsReturningName

Target is to have a readable, short but precise name for a test where you know immediately what's wrong when it fails, without reading its implementation.

Variants could be:

UnitOfWOrk_ExpectedBehavior_whenStateUnderTest → findNameById_SucceedsReturningName_whenExistingId
Without "UnitOfWork":
whenStateUnderTest_expectExpectedBehavior → whenExistingId_expectSuccessReturningName
givenPreconditions_whenStateUnderTest_thenExpectedBehavior → givenIdExists_whenNonNullId_thenSucceedReturningName

You may decorate the names with words like "when", "then", "should", "given", "if", "expect", "fail", "succeed", .... Try to form syntactically correct English sentences.

You could also name the test after the feature or user-story it is about to check. Suitable for integration tests.

Example Java Source

Let's assume that following application classes need to be covered by unit tests. They represent a team that can have several members, but when adding more than 7, an exception will be thrown. (This may be business logic, but can be placed into domain model objects in the sense of Domain-Driven Design.)

Mind that I made this source as short as possible and left out @Entity annotations and JPA-required setters and getters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Objects;

public class Member
{
    public final String name;
    private Team team;
    
    public Member(String name) {
        this.name = name;
    }
    
    public Team getTeam() {
        return team;
    }
    public void setTeam(Team team) {
        this.team = team;
    }
    
    @Override
    public int hashCode() {
        return Objects.hashCode(name);
    }
    @Override
    public boolean equals(Object o) {
        final Member other = (Member) o;
        return Objects.equals(name, other.name);
   }
}

Member is the list-element class. I restricted equality to the name property, this is to avoid duplicates when adding members to a team.

Following is the list-container class, holding up to 7 members:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.HashSet;
import java.util.Set;

public class Team
{
    public static final int MAXIMUM_MEMBERS = 7;
    
    public final String dedication;
    private Set<Member> members = new HashSet<>();
    
    public Team(String dedication) {
        this.dedication = dedication;
    }
    
    public Set<Member> getMembers() {
        return members;
    }
    
    public void addMember(Member member) {
        if (member == null)
            throw new IllegalArgumentException("Can not add null member to team!");
        
        if (getMembers().size() >= MAXIMUM_MEMBERS)
            throw new IllegalStateException("Team can not hold more than "+MAXIMUM_MEMBERS+" members!");
        
        members.add(member);
        member.setTeam(this);
    }
}

You see that the addMember() method implements the "maximum members" business logic.

What we need to do now is test the class Team. It exposes following public API, or "Units of Work":

  • dedication
  • Set getMembers()
  • addMember(Member)

Java Unit Tests

In Java, a "unit test" is a method inside a test class. Remember that unit tests should not depend on each other, so classes are used just as test-containers. Testing frameworks like JUnit would allocate a new instance of the test class for the execution of each of it's methods. (This is different to application classes where several methods may work on the same instance fields, the fields representing the "state" of the class.)

Naming

Test Classes

In Java, test classes are named like the class they are testing. So if you have a class Team you want to test, you would write a TeamTest class. Inside you'd have test-methods (actually these are "tests") that check each public piece of the class to test. For convenience the test-class could hold an instance of the class to be tested in a field, being initialized in a @Before setUp() procedure.

import static org.junit.Assert.*;
import org.junit.*;

public class TeamTest
{
    private Team team;
    
    @Before
    public void setUp() {
        team = new Team(TEAM_DEDICATION);
    }

    // TODO: add tests
}

Test Methods

Naming a test method is not so easy. Obviously the name of the called method should be in the test method's name, that is what Osherove's "UnitOfWork" demands.

Generally a name should express what's under that name. This applies to both classes and methods.
A class managing a name and an identity may be called NameAndId, or NamedId, or IdentifiedName.
A method returning a name when an id parameter is given may be named findNameById.

→ We don't want to be forced to read what's under the name, because we may find other names there that we again have to investigate.

The same goes for test method names. We need to downsize semantics, recommendably without using abbreviations.

Here is the outline of the test-class. I didn't add test implementations, because this is about naming. (Implementation is on bottom of the article.)

....

public class TeamTest
{
    ....

    @Test
    public void dedication_Constructed_Succeeds()  {
    }
    @Test
    public void getMembers_Empty_Succeeds()  {
    }
    @Test
    public void getMembers_One_Succeeds()  {
    }
    @Test
    public void addMember_NotNull_Succeeds()  {
    }
    @Test
    public void addMember_Null_Fails()  {
    }
    @Test
    public void addMember_ExceedingMaximum_Fails()  {
    }
    @Test
    public void addMember_Duplicate_SucceedsButNoChange()  {
    }
}

Remember that we have the @Test annotation in Java, so we don't need the "test" prefix any more to mark test methods. This helps to make the name shorter.

This outline expresses which possible uses get tested. Would you know how to implement each of these methods?

  • dedication_Constructed_Succeeds() checks that the constructor parameter is available after construction

  • getMembers_Empty_Succeeds() makes sure that the count of members can be retrieved without getting a NullPointerException

  • getMembers_One_Succeeds() adds one member and makes sure that the count of members is one afterwards

  • addMember_NotNull_Succeeds() adds a member and makes sure that it is in the team's member list after

  • addMember_Null_Fails() adds null instead of a member and makes sure that an exception is thrown in that case

  • addMember_ExceedingMaximum_Fails() makes sure that not more than 7 members can be added

  • addMember_Duplicate_SucceedsButNoChange() asserts that adding a member clone does not replace the original member

This is a compromise between readability and systematic naming approach. Because we test all methods of class Team, we need to take the "UnitOfWork" into the test method's name. Underscores separate unit (method name), input (state) and output (expected result) and make this systematically readable.

When talking about Java unit tests it is important to not call the class TeamTest a test. It is a test container. The contained methods are tests!

Full Test Source

Here is my implementation of the tests. Remember that one unit test should test no more than one thing. Thus getMembers_One_Succeeds() goes just for the size of members, while addMember_NotNull_Succeeds() ensures that it is the added member which is in the list. To avoid repetetive code, only one test asserts that the member list never is null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import static org.junit.Assert.*;
import org.junit.*;

public class TeamTest
{
    private static final String TEAM_DEDICATION = "Java Programming";
    
    private Team team;
    
    @Before
    public void setUp() {
        team = new Team(TEAM_DEDICATION);
    }
    
    @Test
    public void dedication_Constructed_Succeeds()  {
        assertEquals("Team dediction is wrong!", TEAM_DEDICATION, team.dedication);
    }

    @Test
    public void getMembers_Empty_Succeeds()  {
        assertTrue("Members set is null!", team.getMembers() != null);
        assertEquals("Number of members is wrong!", 0, team.getMembers().size());
    }

    @Test
    public void getMembers_One_Succeeds()  {
        addMember("John Doe");
        assertEquals("Number of members is wrong!", 1, team.getMembers().size());
    }

    @Test
    public void addMember_NotNull_Succeeds()  {
        final Member member = addMember("John Doe");
        assertTrue("Inserted member is wrong!", member == team.getMembers().iterator().next());
        assertTrue("Team of the member is wrong!", team == member.getTeam());
    }

    @Test
    public void addMember_Null_Fails()  {
        try {
            team.addMember(null);
            fail("Adding a null member must fail!");
        }
        catch (IllegalArgumentException e)  {
            // is expected here
        }
    }
    
    @Test
    public void addMember_ExceedingMaximum_Fails()  {
        for (int i = 0; i < Team.MAXIMUM_MEMBERS; i++)
            addMember("John Doe "+i);
        
        assertEquals("Number of team members is wrong!", Team.MAXIMUM_MEMBERS, team.getMembers().size());
        try {
            addMember("James Last");
            fail("Adding too many members must fail!");
        }
        catch (IllegalStateException e)  {
            // is expected here
        }
    }
    
    @Test
    public void addMember_Duplicate_SucceedsButNoChange()  {
        final String NAME = "John Doe";
        final Member member1 = addMember(NAME);
        final Member member2 = addMember(NAME);
        assertEquals("Team contains duplicates!", 1, team.getMembers().size());
        
        final Member member = team.getMembers().iterator().next();
        assertTrue("First member was replaced by duplicate!", member1 == member && member2 != member);
    }
    
    
    private Member addMember(final String name) {
        final Member member = new Member(name);
        team.addMember(member);
        return member;
    }
}

Conclusion

Unit tests make up about a third of development efforts. They duplicate application logic, i.e. when you change the application, you will also have to change the tests.

Note that a script-language like JavaScript strongly demands unit-testing, because it has no type-system. Here, the only way to find typing-pitfalls is to actually execute the code through a unit test. With strongly typed languages like Java your source code will be more stable, nevertheless also Java is much more runtime-bound nowadays than it was initially. Thus unit tests are here to stay.




Keine Kommentare: