-
-
Save jbrains/a44701162b41c423e312792f94eee37e to your computer and use it in GitHub Desktop.
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); | |
} | |
} | |
} |
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.
@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.
In general, I quite like writing custom matchers make my tests more expressive or diagnosable.
If the intent of these tests is to test the
ReaderBasedTextSource
, I don't like that theparsesInto()
matcher hides some of the important aspects of the responsibility. While it emphasizes the desired relationship between inputs and outputs, it hides both the agent to which you've assigned the responsibility and the means of invoking the responsibility (parseIntoLines()
).If the intent of the tests is something else, I'd likely have a different opinion.