5 Unit Testing
ljacqu edited this page 2016-04-18 15:07:36 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Principles

Unit tests are for testing a unit: a particular class' methods are tested (ideally) in isolation to ensure that the logic contained within the class corresponds to our expectations. In contrast, integration tests verify that two or more components interact correctly.

Unit tests have no guaranteed order in which they are run neither for the order of the methods within a class, nor for the order of the test classes themselves. Unit tests should not rely on previous tests to be set up. This is typically the case if a test works fine while running all tests but fails when run in isolation.

Needless to say, things inside of the test/ folder may not be used anywhere outside. Testing is an isolated scope and the project must be able to be built without any of the test resources.

Technologies

We use JUnit, Hamcrest and Mockito for unit testing. JUnit is the general test framework to set up and run tests with; Hamcrest provides powerful matchers to test results with; Mockito enables us to mock a class, i.e. to use the same interface of a class but to provide it with custom functionality to simulate a certain situation. This facilitates unit testing: we only want to test one piece and imitate (mock) the rest.

Conventions

Tests for a particular class should be in a class with the class name and "Test" appended to it, e.g. to test the class Player create a class PlayerTest. JavaDoc is not required for tests that are (reasonably) self-explanatory. Start test methods with should and the expected behavior, e.g. shouldKickPlayer() if this is the expected end result.

While we don't use behavior-driven development, the typical format of the tests are still suitable and convenient for us. Each test is made up of three parts, namely given, when and then:

  • given instantiates and defines the necessary objects and values to run the test (e.g. to produce the desired condition to test).
  • when is the command to test. Typically one line with a method call of the class that is being tested.
  • then verifies the result

The examples further below should illustrate how this works.

Static injections and methods

Mockito cannot mock all classes and methods classes and methods may not be final, and methods may not be static.

Static methods are fine for lightweight utility classes (no state, no heavy actions like performing I/O) and for the logger.

Examples

Note that the following is a made up test and doesn't correspond to any existing classes in AuthMe. Given the following method:

public static boolean isLoggedIn(CommandSender sender) {
  if (sender == null || !(sender instanceof Player)) {
    return false;
  }
  return Status.LOGGED_IN.equals(((Player) sender).getStatus());
}

You may want to test that the method returns false if the sender is not an instance of Player, e.g. if the instance is Console.

@Test
public void shouldReturnFalseForNonPlayerSender() {
  // given
  CommandSender sender = Mockito.mock(Console.class);

  // when
  boolean result = Verifier.isLoggedIn(sender);

  // then
  assertThat(result, equalTo(false));
}

Now we want to test that a player is considered logged in if it returns the correct Status. This example shows the power of Mockito: we don't need to instantiate an actual player, where we might encounter issues to set the status to the desired one (e.g. if there is no setter for that method).

@Test
public void shouldReturnTrueForLoggedInPlayer() {
  // given
  Player player = Mockito.mock(Player.class);
  given(player.getStatus()).willReturn(Status.LOGGED_IN);

  // when
  boolean result = Verifier.isLoggedIn(sender);

  // then
  assertThat(result, equalTo(true));
}