Stefan Birkner's System Rules is one of my favorite JUnit extension libraries. I commonly use it to verify System.out
and System.err
, for example validating audit trail logging.
Growing tired of the same boilerplate, I rolled some simple rules into an aggregated JUnit @Rule
, called NiceLoggingRule. It enforces:
- No logging to
System.err
- No WARN or ERROR logging to
System.out
A more sophisticated version would let the user decide on more than "log level" as to what is an acceptable log line, but it gives a good demonstration of writing complex JUnit rules:
public final class NiceLoggingRule implements TestRule { private static final Pattern NEWLINE = compile("\n"); private final SystemOutRule sout = new SystemOutRule(). enableLog(). muteForSuccessfulTests(); private final SystemErrRule serr = new SystemErrRule(). enableLog(); private final Pattern logLinePattern; private final Predicate<String> problematic; private final RuleChain delegate; public NiceLoggingRule(final String logLinePattern, final Predicate<String> problematic) { this.logLinePattern = compile(logLinePattern); this.problematic = problematic; delegate = outerRule(NiceLoggingStatement::new). around(sout). around(serr); } @Override public Statement apply(final Statement base, final Description description) { return delegate.apply(base, description); } private final class NiceLoggingStatement extends Statement { private final Statement base; private final Description description; private NiceLoggingStatement(final Statement base, final Description description) { this.base = base; this.description = description; } @Override public void evaluate() throws Throwable { base.evaluate(); checkSystemErr(description); checkSystemOut(description); } private void checkSystemErr(final Description description) { final String cleanSerr = serr.getLogWithNormalizedLineSeparator(); final List<String> errors = NEWLINE.splitAsStream(cleanSerr). collect(toList()); if (!errors.isEmpty()) fail("Output to System.err from " + description + ":\n" + cleanSerr); } private void checkSystemOut(final Description description) { final String cleanSout = sout.getLogWithNormalizedLineSeparator(); final List<LogLine> problems = NEWLINE.splitAsStream(cleanSout). map(LogLine::new). filter(LogLine::problematic). collect(toList()); if (!problems.isEmpty()) fail(problems.stream(). map(Object::toString). collect(joining("", "Problems to System.out from " + description + ":\n", ""))); } } private final class LogLine { @Nonnull private final String line; @Nonnull private final String level; private LogLine(@Nonnull final String line) { final Matcher match = logLinePattern.matcher(line); if (!match.find()) // Not match! Ignore trailing CR?NL fail(format( "Log line does not match expected pattern (%s): %s", logLinePattern.pattern(), line)); this.line = line; level = match.group("level"); } public boolean problematic() { return problematic.test(level); } @Override public String toString() { return line; } } }
For example, using it with Spring Boot's default log pattern one might write a factory helper:
public final class SpringDefaultNiceLoggingRule { private static final String logLevels = Stream.of(LogLevel.values()). filter(level -> OFF != level). map(Enum::name). collect(joining("|")); public static NiceLoggingRule springDefaultNiceLoggingRule() { return new NiceLoggingRule( "^(?<timestamp>\\d{4,4}-\\d{2,2}-\\d{2,2} \\d{2,2}:\\d{2,2}:\\d{2,2}\\.\\d{3,3}) +(?<level>" + logLevels + ") +", SpringDefaultNiceLoggingRule::problematic); } private static boolean problematic(final String level) { return 0 > INFO.compareTo(LogLevel.valueOf(level)); } }
Then a simple Spring Boot unit test becomes:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = MockServletContext.class) public final class RootControllerTest { @Rule public final NiceLoggingRule niceLogging = springDefaultNiceLoggingRule(); private MockMvc mvc; @Before public void setUp() throws Exception { mvc = standaloneSetup(new RootController()).build(); } @Test public void shouldGetRoot() throws Exception { mvc.perform(get("/"). accept(APPLICATION_JSON_UTF8)). andExpect(status().isOk()). andExpect(jsonPath("$.message", equalTo("Hello, world!"))); } }