Investigating JUnit 5 asserts
Pesky asserts!
Testing tests.
Of course if your tests are going to be informative, it is not enough that they pass when everything is right and fail when everything is wrong. It is vital that when a test does fail, it gives an informative error message to help identify the problem. Ideally, your well designed test will fail at an assert that gives a clear description of the problem. That requires some effort to find out what your test will report if there is a problem. You need to test your tests.
In my case, the failure was unexpected, but exactly the kind of thing that might happen when automatically testing websites. My first attempt at this setting up this site was pretty, but really did not reflect what I am trying to do, so I used a new theme. One of the changes in the new theme was new CSS that capitalised the title. (From ‘Alexander on Testing’ to ‘ALEXANDER ON TESTING’) This failed my previously working test!
So time to write some tests to find out how usable JUnit 5 Assertions are.
To test this I need to choose some test cases.
1. String expected = “Hello” against itself
2. String expected = “Hello” against String sameAsExpected = “Hello”
3. String expected = “Hello” against String actual = “HELLO”
4. String expected = “Hello” against String different = “Goodbye”
- I expect 1 and 2 should always pass.
- 3 should fail until we use ‘equalsIgnoreCase’
- 4 should always fail
To avoid too much code, and allow me to see the results of ALL failing tests I am going to use
assertAll(String heading, Executable... executables)
Asserts that all suppliedexecutables
do not throw exceptions.
1 using assertEquals
assertEquals(Object expected, Object actual, String message)
Asserts that
expected
andactual
are equal.
Code:
@Test void assertEqualsStringComparisonTest() { assertAll("JUnit assertEquals String comparisons", () -> assertEquals(expected,expected, "Same objects should never fail"), () -> assertEquals(expected, sameAsExpected, "Different Strings with same content also pass."), () -> assertEquals(expected, actual, "Strings differing in case should fail."), () -> assertEquals(expected, different, "Strings differing in content should always fail.") ); }
and the results as reported:
JUnit Jupiter:AssertTest:assertEqualsStringComparisonTest()
MethodSource [className = ‘pftester.AssertTest’, methodName = ‘assertEqualsStringComparisonTest’, methodParameterTypes = ”]
=> org.opentest4j.MultipleFailuresError: JUnit assertEquals String comparisons (2 failures)
Strings differing in case should fail. ==> expected: <Hello> but was: <HELLO>
Strings differing in content should always fail. ==> expected: <Hello> but was: <Goodbye>
Mmm. OK. That is pretty intelligible, but different cases fails the assertion. What if I want it to ignore case?
2 using assertLinesMatch
Looking at the Assertions class there is one that is written specifically for working with (Lists of) Strings.
assertLinesMatch(List<String> expectedLines, List<String> actualLines)
Asserts that
expected
list of Strings matchesactual
list.
Code:
@Test void assertLinesMatchStringComparisonTest() { final List<String> expectedList = Arrays.asList(expected, expected, expected, expected); final List<String> actualList = Arrays.asList(expected, sameAsExpected, actual, different); assertLinesMatch(expectedList, actualList); }
Nice succinct code, especially if you can easily grab a list of Strings in your test. The results as reported:
JUnit Jupiter:AssertTest:assertLinesMatchStringComparisonTest()
MethodSource [className = ‘pftester.AssertTest’, methodName = ‘assertLinesMatchStringComparisonTest’, methodParameterTypes = ”]
=> org.opentest4j.AssertionFailedError: expected line #3:`Hello` doesn’t match ==> expected: <Hello
Hello
Hello
Hello> but was: <Hello
Hello
HELLO
Goodbye>
Yuck! That’s horrid. Good luck making sense out of that with long lists. I’m not sure if this is failing at first failure, or accepting HELLO and only failing on Goodbye. That’s so ugly I’m not even going to investigate.
3 using assertTrue and String.equalsIgnoreCase(String)
assertTrue(boolean condition, String message)
Asserts that the supplied
condition
istrue
.
Code:
@Test void assertTrueIgnoreCaseStringComparisonTest() { assertAll("JUnit assertTrue (equalsIgnoreCase(..)) String comparisons", () -> assertTrue(expected.equalsIgnoreCase(expected), "Same objects should never fail"), () -> assertTrue(expected.equalsIgnoreCase(sameAsExpected), "Different Strings with same content should pass."), () -> assertTrue(expected.equalsIgnoreCase(actual), "Strings differing in case should not fail now."), () -> assertTrue(expected.equalsIgnoreCase(different), "Strings differing should always fail.") ); }
and the results as reported:
JUnit Jupiter:AssertTest:assertTrueIgnoreCaseStringComparisonTest()
MethodSource [className = ‘pftester.AssertTest’, methodName = ‘assertTrueIgnoreCaseStringComparisonTest’, methodParameterTypes = ”]
=> org.opentest4j.MultipleFailuresError: JUnit assertTrue (equalsIgnoreCase(..)) String comparisons (1 failure)
Strings differing should always fail.
OK Now we are getting somewhere – but the fail message isn’t clear and it doesn’t report expected and actual results. Let’s see how we can improve it.
4 using assertTrue, String.equalsIgnoreCase(String) and a messageSupplier
assertTrue(boolean condition, Supplier<String> messageSupplier)
Asserts that the supplied
condition
istrue
.
This allows me to use a lambda to provide a better message of the form:
assertTrue(expectedString.equalsIgnoreCase(actualString), () -> String.format("%1$s -> The expected is: '%2$s' while the actual is: '%3$s'", description, expectedString, actualString));
Nested lambdas however are hard to read and no need to repeat myself. Time for a helper method.
Code:
@Test void assertTrueIgnoreCaseWithExplanationStringComparisonTest() { assertAll("JUnit assertTrue (equalsIgnoreCase(..)) String comparisons with explanation", () -> assertTrueIgnoreCaseWithExplanation("Same objects",expected, expected), () -> assertTrueIgnoreCaseWithExplanation("Same contents", expected, sameAsExpected), () -> assertTrueIgnoreCaseWithExplanation("Different Case", expected, actual), () -> assertTrueIgnoreCaseWithExplanation("Different Words", expected, different) ); } private void assertTrueIgnoreCaseWithExplanation(final String description, final String expectedString, final String actualString){ assertTrue(expectedString.equalsIgnoreCase(actualString), () -> String.format("%1$s -> The expected is: '%2$s' while the actual is: '%3$s'", description, expectedString, actualString)); }
and the results as reported:
JUnit Jupiter:AssertTest:assertTrueIgnoreCaseWithExplanationStringComparisonTest()
MethodSource [className = ‘pftester.AssertTest’, methodName = ‘assertTrueIgnoreCaseWithExplanationStringComparisonTest’, methodParameterTypes = ”]
=> org.opentest4j.MultipleFailuresError: JUnit assertTrue (equalsIgnoreCase(..)) String comparisons with explanation (1 failure)
Different Words -> The expected is: ‘Hello’ while the actual is: ‘Goodbye’
That’s better. It is just about usable, and of course we could always create our own Asserts.java class and save that method as a static method for reuse.
So is that what I would do?
Well no actually. I would use Joel Costigliola’s assertj library. That means adding a dependency to our build.gradle. Here we only actually need it in the testCompile group, but as I plan to use it widely in my main code I’m going to drop it straight into the compile group.
// https://mvnrepository.com/artifact/org.assertj/assertj-core
compile group: ‘org.assertj’, name: ‘assertj-core’, version: ‘3.8.0’
Code:
@Test void assertjTest() { assertAll("String comparison using Joel Costigliola's assertj library.", () -> assertThat(expected).isEqualToIgnoringCase(expected), () -> assertThat(sameAsExpected).isEqualToIgnoringCase(expected), () -> assertThat(actual).isEqualToIgnoringCase(expected), () -> assertThat(different).isEqualToIgnoringCase(expected) ); }
This is concise and readable, so what does it report?
JUnit Jupiter:AssertTest:assertjTest()
MethodSource [className = ‘pftester.AssertTest’, methodName = ‘assertjTest’, methodParameterTypes = ”]
=> org.opentest4j.MultipleFailuresError: String comparison using Joel Costigliola’s assertj library. (1 failure)
Expecting:
<“Goodbye”>
to be equal to:
<“Hello”>
ignoring case considerations
Perfect. Job done. Would be even clearer without the wrapper from the assertAll call.
All that’s left to do now is to swap in my shiny new assertion into my original test.
assertThat(driver.findElement(By.cssSelector(".site-title")).getText()) .isEqualToIgnoringCase("Alexander on Testing");
and undo most of my changes this week.
Next week I will be looking at how to use Abstract classes to build tests with a new browser for each test, and if I can make one with one browser reused for a whole Test class.
Progress made this week:
- Investigated JUnit 5 assertions and how they can be made to report failures well.
- Adopted assertj instead for its ease of use and clarity.
- Our first test now passes, and should give a meaningful message if my site changes / breaks.
Lessons learnt:
- Intellij IDEA cannot run individual tests or classes when using the unit-platform-gradle-plugin (nor I suspect can anything else!
- JUnit 5 Asssertions contain some cool ideas, but I’m not sure how much I will use them.
- JUnit 5 assertAll(..) is the one exception I could see me using.
Full classes used this week:
build.gradle
group 'com.alexanderontesting' version '0.1' buildscript { repositories { mavenCentral() } dependencies { classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-RC2' } } apply plugin: 'org.junit.platform.gradle.plugin' junitPlatform { platformVersion '1.0.0-RC2' reportsDir file('build/test-results/junit-platform') //enableStandardTestTask true } apply plugin: 'java' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.5.1' compile group: 'org.assertj', name: 'assertj-core', version: '3.8.0' testCompile group: 'junit', name: 'junit', version: '4.12' testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.0.0-RC2' testCompile group: 'org.junit.platform', name: 'junit-platform-launcher', version: '1.0.0-RC2' testRuntime group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.0.0-RC2' testRuntime group: 'org.junit.vintage', name: 'junit-vintage-engine', version: '4.12.0-RC2' }
AssertTest.java
package pftester; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class AssertTest { private final String expected = "Hello"; private final String sameAsExpected = "Hello"; private final String actual = "HELLO"; private final String different = "Goodbye"; @Test void assertEqualsStringComparisonTest() { assertAll("JUnit assertEquals String comparisons", () -> assertEquals(expected,expected, "Same objects should never fail"), () -> assertEquals(expected, sameAsExpected, "Different Strings with same content also pass."), () -> assertEquals(expected, actual, "Strings differing in case should fail."), () -> assertEquals(expected, different, "Strings differing in content should always fail.") ); } @Test void assertLinesMatchStringComparisonTest() { final List<String> expectedList = Arrays.asList(expected, expected, expected, expected); final List<String> actualList = Arrays.asList(expected, sameAsExpected, actual, different); assertLinesMatch(expectedList, actualList); } @Test void assertTrueIgnoreCaseStringComparisonTest() { assertAll("JUnit assertTrue (equalsIgnoreCase(..)) String comparisons", () -> assertTrue(expected.equalsIgnoreCase(expected), "Same objects should never fail"), () -> assertTrue(expected.equalsIgnoreCase(sameAsExpected), "Different Strings with same content should pass."), () -> assertTrue(expected.equalsIgnoreCase(actual), "Strings differing in case should not fail now."), () -> assertTrue(expected.equalsIgnoreCase(different), "Strings differing should always fail.") ); } @Test void assertTrueIgnoreCaseWithExplanationStringComparisonTest() { assertAll("JUnit assertTrue (equalsIgnoreCase(..)) String comparisons with explanation", () -> assertTrueIgnoreCaseWithExplanation("Same objects",expected, expected), () -> assertTrueIgnoreCaseWithExplanation("Same contents", expected, sameAsExpected), () -> assertTrueIgnoreCaseWithExplanation("Different Case", expected, actual), () -> assertTrueIgnoreCaseWithExplanation("Different Words", expected, different) ); } private void assertTrueIgnoreCaseWithExplanation(final String description, final String expectedString, final String actualString){ assertTrue(expectedString.equalsIgnoreCase(actualString), () -> String.format("%1$s -> The expected is: '%2$s' while the actual is: '%3$s'", description, expectedString, actualString)); } @Test void assertjTest() { assertAll("String comparison using Joel Costigliola's assertj library.", () -> assertThat(expected).isEqualToIgnoringCase(expected), () -> assertThat(sameAsExpected).isEqualToIgnoringCase(expected), () -> assertThat(actual).isEqualToIgnoringCase(expected), () -> assertThat(different).isEqualToIgnoringCase(expected) ); } }
That’s all for now. See you next week!
Alexander