[Issue 002] Assertions, Assertions, Assertions

Abstract: Exercising assertions (or checks) on values during tests is a common occurrence in Java. However choosing the right API and determining the information to be displayed when an assertions fails can be tricky. We'll discuss some of the options we have in the Java space to achieve these goals.

Welcome to issue number 2! I'm writing this entry in transit between JBCNConf and Devoxx Poland, the inspiration came from a question I was asked after presenting my popular Java libraries you can't afford to miss talk (here's a video of a previous rendition of the talk at JFokus 2017 in case you may be interested), specifically if I had mentioned AssertJ as part of the libraries. As it turns out I did not because this particular talk covers about 20 projects for both production and testing code, however there's a follow up talk title Testing Java code effectively where I do touch the subject of assertions.

Most Java developers are very familiar with JUnit and Hamcrest, it's very likely you have written test code using these projects. Hamcrest makes it easy to define assertions on values; when these assertions fail an error message is generated which turns out to be more descriptive than using regular JUnit assertions. Take for example the following code

package com.andresalmiray.newsletter002;

import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.describedAs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class HamcrestStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject, describedAs("subject is not null", notNullValue()));
        assertThat(subject, describedAs("subject is not empty", not(isEmptyString())));
        assertThat(subject, describedAs("subject contains 'ing'", containsString("ing")));
        assertThat(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }
}

We've got a String defined as test subject and 4 assertions applied it. Notice the composition of the describedAs matcher with other matchers, this allow us to define additional information used in the error message in case of a failure. Notice also that the behavior of any matcher can be inverted by composing it with the not() matcher, as we do with the emptyness check. While still pretty readable some developers prefer a more descriptive way to read the code, a more fluent way as a matter of fact. There's also the problem of picking the right macher, as there are many subtypes and matcher factories to choose from, this means you may have to do a little bit of research in order to write more concise tests. Finally, there's the problem of having too many assertions within on test. What happens if the third assertion fails? The fourth won't even be executed. These issues are addressed by AssertJ, Truth, and JGoTesting.

Both AssertJ and Truth can trace their origins to Fest-assert, another assertions project that introduced a fluent API design coupled with an extension mechanism. AssertJ and Truth took these ideas and added more features on top of it. To begin with, we can obtain an appropriate matcher for a particular subject just by invoking the code completion capabilities of your favorite IDE. In the case of AssertJ you gain access to more conditions on base types, such as blank on a String, also you don't have to negate the emptyness check, there's already a condition for that one! See for yourself

package com.andresalmiray.newsletter002;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class AssertJStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject).as("subject is not null").isNotNull();
        assertThat(subject).as("subject is not null").isNotEmpty();
        assertThat(subject).as("subject is not blank").isNotBlank();
        assertThat(subject).as("subject contains 'ing'").contains("ing");
        assertThat(subject).as("subject has size = 7").hasSize(7);
        assertThat(subject).as("subject = 'testing'").isEqualTo("testing");
    }
}

Notice the flow of the assertions, perhaps you may find it more to your liking. Also, there are less import statements required, as the "matchers" flow from the starting assertion, following the type of the subject. Now, if any of these assertions fails the rest would not be executed, just like with plain JUnit and Hamcrest; how do we get around this problem? Easy, AssertJ leverages a feature found in JUnit called rules, which allow you to decorate test methods with additional behavior executed before and after each test. The previous code can be rewritten as follows

package com.andresalmiray.newsletter002;

import org.assertj.core.api.JUnitSoftAssertions;
import org.junit.Rule;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class AssertJStringTest {
    @Rule
    public final JUnitSoftAssertions softly = new JUnitSoftAssertions();

    @Test
    public void fluentAllChecksOnString() {
        String subject = "something";
        softly.assertThat(subject).as("subject is not null").isNotNull()
            .as("subject is not null").isNotEmpty()
            .as("subject is not blank").isNotBlank()
            .as("subject contains 'ung'").contains("ung")
            .as("subject has size = 7").hasSize(7)
            .as("subject = 'testing'").isEqualTo("testing");
    }
}

We even managed to shorten the code by following the fluent API, appending all checks one after the next. The JUnitSoftAssertions rule will capture all failures during a test and output all errors that may have occurred when the test finishes. As mentioned before Truth follows a similar approach, although it does not provide as many base matchers as AssertJ. There are the two previous tests rewritten using Truth

package com.andresalmiray.newsletter002;

import com.google.common.truth.Expect;
import org.junit.Rule;
import org.junit.Test;

import static com.google.common.truth.Truth.assertThat;


public class TruthStringTest {
    @Test
    public void testsOnString() {
        String subject = "testing";
        assertThat(subject).named("subject is not null").isNotNull();
        assertThat(subject).named("subject is not null").isNotEmpty();
        assertThat(subject).named("subject contains 'ing'").contains("ing");
        assertThat(subject).named("subject has size = 7").hasLength(7);
        assertThat(subject).named("subject = 'testing'").isEqualTo("testing");
    }

    @Rule
    public final Expect expect = Expect.create();

    @Test
    public void allChecksOnString() {
        String subject = "something";
        expect.that(subject).named("subject").isNotNull();
        expect.that(subject).named("subject").isNotEmpty();
        expect.that(subject).named("subject").contains("ung");
        expect.that(subject).named("subject").hasLength(7);
        expect.that(subject).named("subject").isEqualTo("testing");
    }
}

We lost the ability to check if a String may or may not be blank, also in particular the checks for String values do not allow further chaining using the fluent API, but it can be applied to other types such as Collections. Is worth noticing that both AssertJ and Truth allow you to create your own matchers that may be custom tailored for types that are specific to your own codebase. Finally, there's JGoTesting, a project inspired in the testing capabilities offered by the Go programming language. JGoTesting can be used to check any amount of boolean conditions and Hamcrest matchers. The following snippet shows how we could rewrite the very first snippet we saw earlier

package com.andresalmiray.newsletter002;

import org.jgotesting.rule.JGoTestRule;
import org.junit.Rule;
import org.junit.Test;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.describedAs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

public class JGoTestingStringTest {
    @Rule
    public final JGoTestRule t = new JGoTestRule();

    @Test
    public void testsOnString() {
        String subject = "testing";
        t.check(subject, describedAs("subject is not null", notNullValue()));
        t.check(subject, describedAs("subject is not empty", not(isEmptyString())));
        t.check(subject, describedAs("subject contains 'ing'", containsString("ing")));
        t.check(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }

    @Test
    public void fluentAllChecksOnString() {
        String subject = "something";
        t.check(subject, describedAs("subject is not null", notNullValue()))
            .check(subject, describedAs("subject is not empty", not(isEmptyString())))
            .check(subject, describedAs("subject contains 'ung'", containsString("ung")))
            .check(subject, describedAs("subject = 'testing'", equalTo("testing")));
    }
}

Used in this way, JGoTesting can provide the same behavior of AssertJ's soft assertions but for Hamcrest matchers. I'm just scratching the surface here, all these projects offer more features than what has been shown so far. I hope I've piqued your curiosity to give these projects a try. All the snippets are available at a GitHub repository.

Thank you for reading. Any feedback is appreciated.

See you next time.

Andres

ˆ Back To Top