Unit Testing: Behavior vs State

Unit testing is a part of the life of any developer, however, as we usually are in a hurry to finish our work, we forget how important those unit tests are.

In this post, we are going to focus on two strategies we have for unit testing, behavior and/or state.

TRY IT YOURSELF: You can find the source code of this post here.

Unit Testing Series

Behavior vs State

Let’s start by defining what behavior and state are.

Imagining you request a delivery from Amazon, you ask for a new beautiful pillow to stress relief when your code doesn’t work, like this one:

Enter pillow for stress relief

NOTE: You might think: “This guy hits the desk when some code doesn’t work“, well, you are right.

The pillow weight is 1 kilogram, is totally sealed and it was requested on September 23, and must arrive September 28.

The pillow is in Los Angeles, US, and it needs to be delivered to Bogota, Colombia, so, how can you validate that your pillow is going to arrive in a good shape and on time?

My pillow from Los Angeles to Bogota

The following is the process Amazon is going to do to deliver the pillow:

The process Amazon does to delivery my pillow
  1. Amazon send my pillow from Los Angeles to Dallas in a fly.
  2. From Dallas, my pillow travels in a truck to Houston.
  3. And from Houston, it takes a fly to Bogota.

NOTE: Before leaving US, Amazon always validate that the package is on time and in shape.

Behavior Validation

Let’s define some questions to guarantee that our new pillow is going to arrive on time and in shape focusing on the behavior of the delivery process. We do the following steps:

  1. Is the pillow in Los Angeles on September 23?
  2. Is the pillow moving from Los Angeles to Dallas on a flight?
  3. Is the pillow moving from Dallas to Houston in truck?
  4. Is the pillow in Houston with 1 kilogram of weight and totally sealed?
  5. Is the pillow moving from Houston to Bogota on a flight?
  6. Does the pillow arrive on September 28?
  7. Is the pillow in Bogota 1 kilogram of weight and totally sealed?

NOTE: If the answers to the whole questions is YES, my pillow arrives ok

As we can see, we are doing micromanagement here, we want to be sure everything is done as Amazon said.

State Validation

Let’s define some questions to guarantee that our new pillow is going to arrive on time and in shape focusing on the state or the delivery. We do the following steps:

  1. Is the pillow in Los Angeles on September 23?
  2. Is the pillow in Houston 1 kilogram and totally sealed?
  3. Does the pillow arrive on September 28?
  4. Is the pillow in Bogota 1 kilogram of weight and totally sealed?

NOTE: If the answers to the whole questions is YES, my pillow arrives ok

As we can see, we are checking only the state of my package, we don’t care about transporting types or intermediate cities.

Do you think that difference matters? well, let’s see.

Changing Amazon Process

Now, Amazon changed the process due to some cost savings he wants to do, as we can see in the following image:

Amazon changes its routes

Well, the first leg now from Los Angeles to Dallas is in truck, and the flight from Houston to Bogota have a stop in Mexico City.

Now, let’s see how we can validate the behavior of this new route.

Behavior validation

Let’s rewrite the previous questions to guarantee that our new pillow is going to arrive on time and in shape focusing on the behavior of the delivery process. Our steps are updated like this:

  1. Is the pillow in Los Angeles on September 23?
  2. Is the pillow moving from Los Angeles to Dallas on a flight truck?
  3. Is the pillow moving from Dallas to Houston in truck?
  4. Is the pillow in Houston 1 kilogram of weight and totally sealed?
  5. Is the pillow moving from Houston to Bogota on a flight?
  6. Is the pillow moving from Houston to Mexico City in fly?
  7. Is the pillow moving from Mexico City to Bogota in fly?
  8. Does the pillow arrive on September 28?
  9. Is the pillow in Bogota 1 kilogram of weight and totally sealed?

As we are doing micromanagement, we needed to update refactor our validation to support the new Amazon process.

State Validation

Now, do we need to change something in the state validation?

  1. Is the pillow in Los Angeles on September 23?
  2. Is the pillow in Houston 1 kilogram and totally sealed?
  3. Does the pillow arrive on September 28?
  4. Is the pillow in Bogota 1 kilogram of weight and totally sealed?

Well, no, we don’t need to change anything because we are focusing on the state (the expected result).

Remember, I don’t care if Amazon delivers my package using airplanes, boats, or spaceships; what I care is the final result, my pillow is in my door on September 28, is 1 kilogram of weight and totally sealed.

This is a principle math and programming share, you have inputs, do some operations on them, and get some outputs, that’s all.

NOTE: You might need to validate some behavior…. but, that should be at the minimum amount possible, or your validations are going to be fragile

So Boring, Let’s See Some Code

Okey, as we now understand better when you validate the behavior vs the state, let’s move to some code. The following classes will be used as example.

class User {
  private String id;
  private String name;
  private LocalDateTime createdAt;

  public User(String id, String name,
              LocalDateTime createdAt) {
    this.id = id;
    this.name = name;
    this.createdAt = createdAt;
  }

  ...
}

This is a normal User with id, name and when it was created.

interface Repository {
  User save(User user);
}

Now, there is a repository that saves a User.

class Service {
  private final Repository repository;

  public Service(Repository repository) {
    this.repository = repository;
  }

  public User save(User user){
    user.setCreatedAt(LocalDateTime.now());

    return repository.save(user);
  }
}

And finally, a Service which uses the repository to save a User with its current createdAt field set to current date.

NOTE: You might think “That code is pretty trivial, come on man, you can do it better“. Well, yes, it is trivial, but, you will notice how hard could be to test this right.

Now, let’s move to see the unit tests. We are going to start from a behavior validation, refactoring it until we reach a good state validation.

Unit Testing with Only Behavior Validations

Well, let’s see how a unit test focused in behavior looks like:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private Repository repository;
  @InjectMocks
  private Service service;

  @Test
  public void save(){
    //Given

    User user = mock(User.class);

    User userSaved = service.save(user);

    //Then

    verify(user).setCreatedAt(any(LocalDateTime.class));
    verify(repository).save(user);
  }
}

REMEMBER: verify() checks that a method was call in this test with the exact parameters.

As we can see, we mock the User and verify the behavior it has on the test, and we also verify that the save method in the repository is executed as expected. And the worst thing, we cannot verify which date the user was created on. We just can tell if the date is there and the setCreatedAt method was invoked.

Besides, see in line 18 how we verify the repository call. We are validating the exact behavior of our Service save method.

NOTE: You shouldn’t mock entities/vos/dtos. Those objects are usually easy to create, so, use the real ones.

Moving to Use a Real User Object

Now, let’s use a real User and verify against its state, not behavior.

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private Repository repository;
  @InjectMocks
  private Service service;

  @Test
  public void save(){
    //Given

    User user = new User(null, "Daniel", null);

    when(repository.save(user)).thenReturn(user);

    //When

    User userSaved = service.save(user);

    //Then

    assertThat(userSaved.getCreatedAt()).isNotNull();
    assertThat(userSaved.getCreatedAt())
                .isEqualTo(LocalDateTime.now());

    verify(repository).save(user);
  }
}

Well, we created a real User, tell the repository to return it when the Repository.save method is invoked, and finally, we assert the createAt attribute against the current date. However, this is what we got:

Unit test fails by nanoseconds

We can see that the difference is in nanoseconds due to we don’t have the exact date and time when the User was created. Now, we can improve our state validation to get closer to the real date and time using the following hack:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private Repository repository;
  @InjectMocks
  private Service service;

  @Test
  public void save(){
    //Given

    User user = new User(null, "Daniel", null);

    when(repository.save(user)).thenReturn(user);

    //When

    User userSaved = service.save(user);

    //Then

    assertThat(userSaved.getCreatedAt()).isNotNull();
    assertThat(userSaved.getCreatedAt().withNano(0))
            .isEqualTo(LocalDateTime.now().withNano(0));

    verify(repository).save(user);
  }
}

What we did is to remove the nanoseconds, so, we get this now:

Unit test passes by removing the nanoseconds

However, this strategy is risky, if you run the unit test close to finish a second, you might find the new date is one second ahead, so, your unit test is going to fail, that means, this unit test is randomly green.

NOTE: Any time we need to hack our unit tests, think twice, and find another way.

Exact Match with Powermock

Well, the questions is, how can I validate an accurate date and time so my tests never fail? we need to mock LocalDateTime.now() somehow. As that method is an static method, we must enhance our tests with Powermock.

NOTE: Powermock “hacks” the JVM to allow us to mock things we usually cannot using a normal mocker frameworks like Mockito.

Now, we changed the unit tests to use Powermock:

@RunWith(PowerMockRunner.class)
@PrepareForTest( { Service.class, LocalDateTime.class})
public class ServiceTest {
  @Mock
  private Repository repository;
  @InjectMocks
  private Service service;

  @Test
  public void save() throws Exception {
    //Given

    User user = new User(null, "Daniel", null);

    LocalDateTime createdAtExpected = LocalDateTime
            .of(2019, 2, 3, 4, 5, 8, 123);
    User userExpected = new User(null, "Daniel",
            createdAtExpected);

    PowerMockito.mockStatic(LocalDateTime.class);
    when(LocalDateTime.now()).thenReturn(createdAtExpected);

    when(repository.save(user)).thenReturn(user);

    //When

    User userSaved = service.save(user);

    //Then

    assertThat(userSaved)
                 .isEqualTo(userExpected);

    verify(repository).save(user);
  }
}

As we can see, we mocked the LocalDateTime.now() telling it that when this method is invoked, return our previously created date.

See the line 17 and 31, that’s the most interesting ones, as we now can verify the User by an exact match, we created an expected full User, and we assert it against the result of the save method, using the equals function.

NOTE: Remember to override the equals in the object to assert before using isEqualTo method.

Now, as Powermock “hacks” the JVM, we get consequences:

  • The tests are slower than normal ones.
  • You need to add more code for a simple test.
  • Getting Powermock working is not always straightforward.
  • You can find things that even Powermock cannot handle.

NOTE: Powermock is a great tool, but, should be used as last resource. There are better ways to handle this, we are going to see them in the following sections.

Spying Instead of Powermocking

Well, let’s replace the Powermock code by a spy.

NOTE: A spy is usually the same subject under tests, but we mock some of its behavior, not all.

Now, we need to refactor a little bit our Service class:

class Service {
  private final Repository repository;

  public Service(Repository repository) {
    this.repository = repository;
  }

  public User save(User user){
    user.setCreatedAt(getNow());

    return repository.save(user);
  }

  LocalDateTime getNow() {
    return LocalDateTime.now();
  }
}

As we can see, we extracted the LocalDateTime.now() to a new method, so, we can spy Service object and mock the getNow() method with the behavior we want.

Later, we have the following unit test:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private Repository repository;
  @Spy
  @InjectMocks
  private Service service;

  @Test
  public void save() {
    //Given

    User user = new User(null, "Daniel", null);

    LocalDateTime createdAtExpected = LocalDateTime
            .of(2019, 2, 3, 4, 5, 8, 123);
    User userExpected = new User(null, "Daniel",
            createdAtExpected);

    doReturn(createdAtExpected).when(service).getNow();
    when(repository.save(user)).thenReturn(user);

    //When

    User userSaved = service.save(user);

    //Then

    assertThat(userSaved)
                 .isEqualTo(userExpected);
  }
}

In the line 5, we see how we tell Mockito to convert our subject under tests to a spy, and in line 20, we mock the behavior of getNow() to return a predefined date.

NOTE: See line 20 again, we need to invert the mocking structure from when-then to then-when as this class is a spy. This is to avoid side effects in our tests due to we are mocking part of a real object.

Finally Using the Single Responsibility Principle

Well, if you are in a Object Oriented Programming world, why you don’t delegate problems to objects?

So, we are going to add a new interface to our solution:

public interface DateTimeHandler {
  LocalDateTime now();
}

DateTimeHandler is responsible of handling dates (obviously). You ask him about which time it is and he tells you.

NOTE: You might think “when I create unit tests for a DateTimeHandler implementation, I will need Powermock or a spy to test it“, well, yes, that’s correct, but, at least, you have that isolated to one class, not to every class that needs a current date.

Now, let’s refactor a little bit our Service class:

class Service {
  private final Repository repository;
  private final DateTimeHandler dateTimeHandler;

  public Service(Repository repository,
          DateTimeHandler dateTimeHandler) {
    this.repository = repository;
    this.dateTimeHandler = dateTimeHandler;
  }

  public User save(User user){
    user.setCreatedAt(dateTimeHandler.now());

    return repository.save(user);
  }
}

We just added the new dependency to DateTimeHandler class and use it as a current date provider.

NOTE: Is really worth to create a whole interface only to get the current date? well, that depends on your use cases, but, what I can tell you in my experience, is worth. I have seen plenty of timezone issues and current dates problems, plenty of Powermocking everywhere because LocalDateTime, Calendar.getInstance, systems that need to be tested at different current dates because your business logic depends on that. So, think twice before making this call.

Now, this is how our unit tests look like:

@RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
  @Mock
  private Repository repository;
  @Mock
  private DateTimeHandler dateTimeHandler;
  @InjectMocks
  private Service service;

  @Test
  public void save() {
    //Given

    User user = new User(null, "Daniel", null);

    LocalDateTime createdAtExpected = LocalDateTime
            .of(2019, 2, 3, 4, 5, 8, 123);
    User userExpected = new User(null, "Daniel",
            createdAtExpected);

    when(dateTimeHandler.now())
            .thenReturn(createdAtExpected);
    when(repository.save(user)).thenReturn(user);

    //When

    User userSaved = service.save(user);

    //Then

    assertThat(userSaved).isEqualTo(
            userExpected);
  }
}

Well, we mock DateTimeHandler too, and define its behavior when the now() method is called, we are using real User object, an expected response, we validate exact match, and avoid as much as we can validating behavior.

NOTE: You notice we deleted the line verify(repository).save(user) . Well, you will be tempted of let it there, but, is that line really necessary? that’s behavior validation, but, how do I guarantee that any developer is not going to change that line from my Service class and return the User directly? the test is going to be green…… we might need another kind of tests at higher level like integration tests….

Final Thought

It is difficult to create Unit Tests that only uses state as a validation mechanism, but, we should try to use that strategy as much as we can to avoid fragile unit tests.

Powermock is a great tool, but, don’t use it too much, try to find better ways, be creative and use the whole OOP capacity. If you must use it, isolate it to a few places, so, you don’t spread that through your whole application.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

4 comments

Leave a comment