Skip to content

Instantly share code, notes, and snippets.

@DenisVerkhoturov
Created February 25, 2020 20:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DenisVerkhoturov/3e48d0ec89fa61e64e6c3f266ce5c3d2 to your computer and use it in GitHub Desktop.
Save DenisVerkhoturov/3e48d0ec89fa61e64e6c3f266ce5c3d2 to your computer and use it in GitHub Desktop.
Testing: Matchers agains asserts

Testing: Matchers

Let's weight up a couple of testing approaches. We will test these two simple functions:

  • sum(List<Integer>) : int
  • max(List<Integer>) : int

The simplest way to cover this functionality with tests will be to use assertEquals and assertTrue members of the org.junit.jupiter.api.Assertions class, so we write tests like these:

public class FunctionsTest {
    @Test void testIfListIsEmptyForSum() {
        assertEquals(0, sum(emptyList()));
    }

    @Test void testIfListHasSameElements() {
        final int x = 5;
        final List<Integer> sameElements = List.of(x, x, x);
        assertEquals(x * sameElements.size(), sum(sameElements));
    }

    @Test void commutativeTest() {
        final List<Integer> list = List.of(1, 2, 3, 4, 5);
        final List<Integer> reversed = List.of(5, 4, 3, 2, 1);
        assertEquals(sum(list), sum(reversed));
    }

    @Test void associativeTest() {
        assertEquals(sum(List.of(1, 2, 3, 4, 5)), sum(List.of(1, 2, 3)) + sum(List.of(4, 5)));
    }

    @Test void testIfListIsNullForMax() {
        assertThrows(NoSuchElementException.class, () -> max(emptyList()));
    }
    @Test void testIfListContainOnePositionForMax() {
        final int x = 7;
        assertEquals(x, max(singletonList(x)));
    }

    @Test void testIfListContainsMax() {
        final List<Integer> list = List.of(1, 2, 3, 4, 5);
        assertTrue(list.contains(max(list)));
    }

    @Test void testIfNextMaxIsLessOrEqualsToPrevious() {
        final List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        while (list.size() != 1) {
            int previous = max(list);
            list.remove(list.indexOf(previous));
            assertTrue(previous >= max(list));
        }
    }
}

All these tests written correctly and prevent us from making logical mistakes in implementation, but are they verbose enough to let you find the mistake fast? Why don't we check it? To make it let's say that implementation of both functions is (int) (Math.random() * 1000). And then take a look what our tests can say to us about our implementation:

bash gradlew clean test
> Task :test FAILED

metamer.FunctionsTest > associativeTest() FAILED
    org.opentest4j.AssertionFailedError: expected: <851> but was: <1425>

metamer.FunctionsTest > testIfListIsNullForMax() FAILED
    org.opentest4j.AssertionFailedError: Expected java.util.NoSuchElementException to be thrown, but nothing was thrown.

metamer.FunctionsTest > testIfListContainsMax() FAILED
    org.opentest4j.AssertionFailedError: expected: <true> but was: <false>

metamer.FunctionsTest > commutativeTest() FAILED
    org.opentest4j.AssertionFailedError: expected: <50> but was: <916>

metamer.FunctionsTest > testIfListHasSameElements() FAILED
    org.opentest4j.AssertionFailedError: expected: <15> but was: <91>

metamer.FunctionsTest > testIfListContainOnePositionForMax() FAILED
    org.opentest4j.AssertionFailedError: expected: <7> but was: <4>

metamer.FunctionsTest > testIfNextMaxIsLessOrEqualsToPrevious() FAILED
    java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 5

metamer.FunctionsTest > testIfListIsEmptyForSum() FAILED
    org.opentest4j.AssertionFailedError: expected: <0> but was: <193>

8 tests completed, 8 failed

FAILURE: Build failed with an exception.

It is easy to notice that if we only tell what we expected and what was the actual to our testing framework is not enough. We can't reason what is the problem using just this information. So what can we do? It is great that there is a solution to help us. If we rewrite our tests using some matchers library (I will use hamcrest) then our tests will become a way more verbose:

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static metamer.Functions.sum;
import static metamer.Functions.max;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isIn;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class FunctionsTest {
    @Test void testIfListIsEmptyForSum() {
        assertThat(sum(emptyList()), is(0));
    }

    @Test void testIfListHasSameElements() {
        final int x = 5;
        final List<Integer> sameElements = List.of(x, x, x);
        assertThat(sum(sameElements), is(x * sameElements.size()));
    }

    @Test void commutativeTest() {
        final List<Integer> list = List.of(1, 2, 3, 4, 5);
        final List<Integer> reversed = List.of(5, 4, 3, 2, 1);
        assertThat(sum(reversed), is(sum(list)));
    }

    @Test void associativeTest() {
        assertThat(sum(List.of(1, 2, 3)), is(sum(List.of(4, 5)) + sum(List.of(1, 2, 3, 4, 5))));
    }

    @Test void testIfListIsNullForMax() {
        assertThrows(NoSuchElementException.class, () -> max(emptyList()));
    }
    @Test void testIfListContainOnePositionForMax() {
        final int x = 7;
        assertThat(max(singletonList(x)), is(x));
    }

    @Test void testIfListContainsMax() {
        final List<Integer> list = List.of(1, 2, 3, 4, 5);
        assertThat(max(list), isIn(list));
    }

    @Test void testIfNextMaxIsLessOrEqualsToPrevious() {
        final List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        while (list.size() != 1) {
            final int previous = max(list);
            list.remove(list.indexOf(previous));
            assertThat(max(list), lessThanOrEqualTo(previous));
        }
    }
}

And what we see now as a result of:

bash gradlew clean test
> Task :test FAILED

metamer.FunctionsTest > associativeTest() FAILED
    java.lang.AssertionError: 
    Expected: is <865>
         but: was <697>

metamer.FunctionsTest > testIfListIsNullForMax() FAILED
    org.opentest4j.AssertionFailedError: Expected java.util.NoSuchElementException to be thrown, but nothing was thrown.

metamer.FunctionsTest > testIfListContainsMax() FAILED
    java.lang.AssertionError: 
    Expected: one of {<1>, <2>, <3>, <4>, <5>}
         but: was <507>

metamer.FunctionsTest > commutativeTest() FAILED
    java.lang.AssertionError: 
    Expected: is <698>
         but: was <867>

metamer.FunctionsTest > testIfListHasSameElements() FAILED
    java.lang.AssertionError: 
    Expected: is <15>
         but: was <849>

metamer.FunctionsTest > testIfListContainOnePositionForMax() FAILED
    java.lang.AssertionError: 
    Expected: is <7>
         but: was <263>

metamer.FunctionsTest > testIfNextMaxIsLessOrEqualsToPrevious() FAILED
    java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 5

metamer.FunctionsTest > testIfListIsEmptyForSum() FAILED
    java.lang.AssertionError: 
    Expected: is <0>
         but: was <675>

8 tests completed, 8 failed

FAILURE: Build failed with an exception.

That is a way better!=D Especially if you look, what at the test that checks if a list contains the max - element retrieved from that list. Before:

@Test void testIfListContainsMax() {
    final List<Integer> list = List.of(1, 2, 3, 4, 5);
    assertTrue(list.contains(max(list)));
}
metamer.FunctionsTest > testIfListContainsMax() FAILED
    org.opentest4j.AssertionFailedError: expected: <true> but was: <false>

After:

@Test void testIfListContainsMax() {
    final List<Integer> list = List.of(1, 2, 3, 4, 5);
    assertThat(max(list), isIn(list));
}
metamer.FunctionsTest > testIfListContainsMax() FAILED
    java.lang.AssertionError: 
    Expected: one of {<1>, <2>, <3>, <4>, <5>}
         but: was <507>

I hope, it is obvious enough now that using org.hamcrest.MatcherAssert.assertThat instead of org.junit.jupiter.api.Assertions. { assertEquals, assertTrue } will result in more readable tests and make test reports more readable either.

Thanks for your attention, please, see also other matchers in the org.hamcrest.Matchers package.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment