Created
October 13, 2018 19:16
-
-
Save jbrains/a44701162b41c423e312792f94eee37e to your computer and use it in GitHub Desktop.
An example of using a custom Hamcrest matcher to write tests. What do you think of this style?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ca.jbrains.java.test; | |
import ca.jbrains.java.ReaderBasedTextSource; | |
import io.vavr.collection.List; | |
import io.vavr.collection.Stream; | |
import org.hamcrest.Description; | |
import org.hamcrest.Factory; | |
import org.hamcrest.Matcher; | |
import org.hamcrest.TypeSafeMatcher; | |
import org.junit.Assert; | |
import org.junit.Test; | |
import java.io.StringReader; | |
import static ca.jbrains.java.test.LearnReadingLinesTest.TextParsesIntoLines.parsesInto; | |
public class LearnReadingLinesTest { | |
@Test | |
public void readOneLineWithoutALineSeparator() throws Exception { | |
Assert.assertThat( | |
"this is one line of text without a line separator.", | |
parsesInto(List.of( | |
"this is one line of text without a line separator."))); | |
} | |
@Test | |
public void readOneLineFromABlobOfTextEndingWithALineSeparator() throws Exception { | |
Assert.assertThat( | |
endWithLineSeparator("this is one line of text ending in a line separator."), | |
parsesInto(List.of( | |
"this is one line of text ending in a line separator."))); | |
} | |
@Test | |
public void readMultipleLinesEndingWithSeveralEmptyLines() throws Exception { | |
Assert.assertThat(new StringBuilder() | |
.append(endWithLineSeparator("a non-empty line of text")) | |
.append(endWithLineSeparator("a non-empty line of text")) | |
.append(endWithLineSeparator("a non-empty line of text")) | |
.append(endWithLineSeparator("")) | |
.append(endWithLineSeparator("")) | |
.append(endWithLineSeparator("")) | |
.append(endWithLineSeparator("")) | |
.toString(), | |
parsesInto(List.of( | |
"a non-empty line of text", | |
"a non-empty line of text", | |
"a non-empty line of text", | |
"", | |
"", | |
"", | |
""))); | |
} | |
@Test | |
public void readNothing() throws Exception { | |
Assert.assertThat("", parsesInto(List.empty())); | |
} | |
@Test | |
public void readOnlyOneEmptyLine() throws Exception { | |
Assert.assertThat(System.lineSeparator(), parsesInto(List.of(""))); | |
} | |
// REFACTOR Maybe move to a generic text library? | |
public static String endWithLineSeparator(String text) { | |
return String.format("%s%s", text, System.lineSeparator()); | |
} | |
/** | |
* Matches multiline text as lines, where lines are separated | |
* by {@code System.lineSeparator()}. | |
*/ | |
public static class TextParsesIntoLines extends TypeSafeMatcher<String> { | |
private List<String> expectedLines; | |
public TextParsesIntoLines(List<String> expectedLines) { | |
this.expectedLines = expectedLines; | |
} | |
@Override | |
protected boolean matchesSafely(String text) { | |
return tokenizeAsLines(text).equals(expectedLines); | |
} | |
private List<String> tokenizeAsLines(String text) { | |
ReaderBasedTextSource textSource = new ReaderBasedTextSource(new StringReader(text)); | |
Stream<String> streamOfLines = textSource.parseIntoLines(); | |
return List.ofAll(streamOfLines); | |
} | |
@Override | |
protected void describeMismatchSafely(String text, Description mismatchDescription) { | |
mismatchDescription.appendText("instead parses into lines ") | |
.appendValueList("[", ",", "]", tokenizeAsLines(text)); | |
} | |
@Override | |
public void describeTo(Description description) { | |
description.appendText("parses into lines ") | |
.appendValueList("[", ",", "]", expectedLines); | |
} | |
@Factory | |
public static Matcher<String> parsesInto(List<String> expectedLines) { | |
return new TextParsesIntoLines(expectedLines); | |
} | |
} | |
} |
@dhemery @rubberduck203 I find one aspect of ReaderBasedTextSource
that I believe doesn't fit the test: parseIntoLines()
returns a Stream
, but we can't compare Stream
s directly for equality, as far as I know, so I want List
s. Now we end up combining two bits of behavior in the matcher: invoking the production code and adapting the output to something test-friendly. I would prefer to separate those things.
I suppose I could compose the matcher with something that runs the production code in order to call that out better. I might play around with that a little.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I like the way the tests read, but I don’t like that I don’t see the code under test being called. I’m assuming the CUT is being called from inside of the matcher and that’s weird. We can’t get feedback about the API that way. Unless the fact that you felt the need to abstract away calling the CUT is itself a comment on the API design.