Java / Unit Testing


Java / Unit Testing

1. Setup Project

2. Analyze Project

The project contains three classes:

As you can see, the distinct method in the ListAggregator class, depends on the ListDeduplicator class in order to calculate the number of unique elements in a list.

Also, the ListDeduplicator class depends on the ListSorter class as it is much easier to remove duplicates in an already sorted list.

3. Simplify Test Setup

Take a moment to notice that our test methods are organized along three different phases (the 3 As):

Notice that the setup for the ListAggregator tests is always the same:

List<Integer> list = Arrays.asList(1,2,4,2,5);

Do one of two things:

Do the same for the other test classes making sure that all tests still pass.

4. Corner Cases

You received a bug report:

Bug report #7263

Created a list with values "-1, -4 and -5".

Tried to calculate the maximum of these values but got 0 instead of -1.

5. Distinct

You received a bug report:

Bug report #8726

Created a list with values "1, 2, 4 and 2".

Tried to calculate the number of distinct values in the list but got 4 instead of 3.

The problem is that when we are testing the distinct() method, we are also testing the ListDeduplicator.deduplicate() code. Before fixing the bug, lets fix the test.

To test the distinct() and the deduplicate() methods independently from each other, we must go from a design that looks like this:

Where our client (the ListAggregator) depends directly on its service (the ListDeduplicator). To something like this:

Where the client depends on an interface (lets call it GenericListDeduplicator) instead, and some Assembler class (the ListAggregatorTest) is responsible for creating the concrete service (the ListDeduplicator) and injecting it into the client (the ListAggregator).

Like this:

ListDeduplicator deduplicator = new ListDeduplicator();
int distinct = aggregator.distinct(Arrays.asList(1, 2, 4, 2), deduplicator);

This is what is called Dependency Injection and it allows our test to inject into the ListAggregator any list deduplicator service. Even one that always responds with the same canned answer (a Stub).

To remove the dependency between the ListAggregatorTest and the ListDeduplicator class using a stub, we first need to:

And then create the stub:

This did not fix any bug, we simply corrected the failing test as it should not be the one failing. To fix our code we still have to:

6. Mockito

Redo the previous exercise but this time use Mockito to create the stubs.

To use Mockito, you must first add this to the dependencies on your build.gradle file:

  testImplementation 'org.mockito:mockito-core:3.7.7'

Creating a deduplicator using Mockito, should look like this:

GenericListDeduplicator deduplicator = Mockito.mock(GenericListDeduplicator.class);

Making the stub return the correct list can then be done like this:

  Mockito.when(deduplicator.deduplicate(Mockito.anyList())).thenReturn(Arrays.asList(1, 2, 4));

7. Coverage

The report should appear on the right side of the screen.

Enter inside the com package, then inside the aor and numbers packages and verify if all classes, methods and lines are covered by your tests. If not add more tests until they are.

8. Filters

Create a new class ListFilterer that will be capable of filtering a list of numbers. This class should have a constructor that receives a filter:

  public ListFilterer(GenericListFilter filter) { ... }

And single method called filter with the following signature:

  public List<Integer> filter(List<Integer> list);

As you can see, this method returns a list of numbers that have been filtered by a certain filter (Dependency Injection again).

The GenericListFilter interface, should have only one method that returns true if a certain number should be accepted for that filter and false otherwise:

  public boolean accept(Integer number);

Create two classes that follow this interface: PositiveFilter (that accepts only positive numbers) and DivisibleByFilter (that receives an integer upon construction and accepts only numbers divisible by that number).

9. Mutation Testing

Test coverage allows us to access the percentage of lines covered by our tests but it doesn’t verify the quality of those tests.

Mutation testing tries to mitigate this problem by creating code mutations (that should not pass the tests) and verifying if any of those mutations survive our test suite.

To use PIT (a test mutation system for Java) we must first add the following line to the plugin section of our build.gradle file:

  id 'info.solidsoft.pitest' version '1.6.0'

Also add the following section anywhere in your build.gradle file:

pitest {
  junit5PluginVersion = '0.12'

By default, PIT runs all tests under the package with the same name as the group defined in your build.gradle file. So if all your classes and tests are under the com.aor.numbers package, no other configuration should be necessary.

PIT should have automatically created a gradle task called pitest that you can execute by doing (or using the IntelliJ gradle panel):

./gradlew pitest

This will run PIT and create a report under “build/reports/pitest/<date>“. You can open this report using your browser and check if any mutations survived.

Try improving your tests so all mutations die.

10. Hero Testing