The long way through Software Craftsmanship

Exploring Testing with Java 8 Lambdas

Jul 30, 2015 - 4 minute read - Comments - draftjavalambdaexplorationclient

At a client, we were testing a simple feature, but the resulting tests had much subtle repetition:

(source code is here)

@Test
public void log_when_sending_greeting_letters() {

	sut.send(mock(GreetingLetter.class));

	verify(logger).sentGreetingLetter();
}

@Test
public void log_when_sending_love_letters() {

	sut.send(mock(LoveLetter.class));

	verify(logger).sentLoveLetter();
}

and the production code:

public class MailSender {
	private final EventLogger eventLogger;

	public MailSender (final EventLogger eventLogger) {
		this.eventLogger = eventLogger;
	}

	public void send (final GreetingLetter letter) {
		// more business logic
		eventLogger.sentGreetingLetter();
	}

	public void send (final LoveLetter letter) {
		// more business logic
		eventLogger.sentLoveLetter();
	}
}

Note: this is an adapted code, so the business logic is not complete and seems simple.

So I decided to explore a bit on refactoring these tests to use lambdas and reduce repetition:

private MailSender sut;
private Consumer<MailSender> arrange;
private Consumer<EventLogger> verify;

@Test
public void log_greetings_letter() {
	arrange = (MailSender sut) -> sut.send(mock(GreetingLetter.class));

	verify = EventLogger::sentGreetingLetter;

	assertAndVerify();
}

@Test
public void log_love_letter() {
	arrange = (MailSender sut) -> sut.send(mock(LoveLetter.class));

	verify = EventLogger::sentLoveLetter;

	assertAndVerify();
}

private void assertAndVerify() {
	arrange.accept(sut);
	verify.accept(logger);
}

Some comments, mine and my teammates:

  • The tests are not very readable, maybe because we’re still not used to java 8 lambdas
  • assertAndVerify has multiple purposes / responsibilities
  • The gain of usability does not outweight the loss of readability

Finally, the refactor was discarded and we are using the initial version.

Later, in the dojo, Manuel and I have refactored this a little bit more. Source code is here

public class MailSenderShould {

	private EventLogger eventLogger;
	private MailSender mailSender;

	@Before
	public void setUp () {
		eventLogger = mock(EventLogger.class);
		mailSender = new MailSender(eventLogger);
	}

	@Test
	public void log_greetings_letter() {
		checkThat(aLoggingLine().forA(greetingLetter()).wasLoggedWhen(aGreetingLetterWasSent()));
	}

	@Test
	public void log_love_letter() {
		checkThat(aLoggingLine().forA(loveLetter()).wasLoggedWhen(aLoveLetterWasSent()));
	}

	private void checkThat (final Check check) {
		check.checkFor(mailSender, eventLogger);
	}

	private Consumer<EventLogger> aGreetingLetterWasSent () {
		return EventLogger::sentGreetingLetter;
	}

	private Consumer<MailSender> greetingLetter () {
		return (MailSender sut) -> sut.sendGreetingLetter(mock(GreetingLetter.class));
	}

	private Consumer<MailSender> loveLetter () {
		return (MailSender sut) -> sut.sendLoveLetter(mock(LoveLetter.class));
	}

	private Consumer<EventLogger> aLoveLetterWasSent () {
		return EventLogger::sentLoveLetter;
	}
}

(the logger is here)

Later, we have created a simpler functional DSL for the logging, joining the act and verify into an object called Check

public class MailSenderShould {

	private EventLogger eventLogger;
	private MailSender mailSender;

	@Before
	public void setUp () {
		eventLogger = mock(EventLogger.class);
		mailSender = new MailSender(eventLogger);
	}

	@Test
	public void log_greetings_letter() {
		mailSenderLogs(whenSendingAGreetingLetter());
	}

	@Test
	public void log_love_letter() {
		mailSenderLogs(whenSendingALoveLetter());
	}

	private Check whenSendingALoveLetter () {
		return new Check(
			(MailSender sut) -> sut.send(letter(LoveLetter.class)),
			EventLogger::sentLoveLetter
		);
	}

	private void mailSenderLogs (Check check) {
		check.checkFor(mailSender, eventLogger);
	}

	private Check whenSendingAGreetingLetter () {
		return new Check(
			(MailSender sut) -> sut.send(letter(GreetingLetter.class)),
			EventLogger::sentGreetingLetter
		);
	}

	private <T> T letter (final Class<T> typeOfLetter) {
		return mock(typeOfLetter);
	}
}

and the Check, just a placeholder for two Consumers:

public class Check {

	private final Consumer<MailSender> act;
	private final Consumer<EventLogger> verify;

	public Check (final Consumer<MailSender> act, final Consumer<EventLogger> verify) {
		this.act = act;
		this.verify = verify;
	}

	public void checkFor(MailSender mailSender, EventLogger eventLogger) {
		act.accept(mailSender);
		verify.accept(eventLogger);
	}
}

Comparison

Comparison between the original form and the three refactored forms of the test:

@Test
public void log_when_sending_greeting_letters() {

	sut.send(mock(GreetingLetter.class));

	verify(logger).sentGreetingLetter();
}

This reads like a classical JUnit tests, using the interaction (with mocks).

@Test
public void log_greetings_letter() {
	arrange = (MailSender sut) -> sut.send(mock(GreetingLetter.class));

	verify = EventLogger::sentGreetingLetter;

	assertAndVerify();
}

This is the first step towards more cohesion, at the cost of readability. You save the verify at the end, but the repeated method assertAndVerify is still present.

@Test
public void log_greetings_letter() {
	checkThat(aLoggingLine().forA(greetingLetter()).wasLoggedWhen(aGreetingLetterWasSent()));
}

This one reads more like a classical DSL, but is harder to read compared to a classical test.

@Test
public void log_greetings_letter() {
	mailSenderLogs(whenSendingAGreetingLetter());
}

This one reads more like a business requirement.

Other

Other ideas we have tested:

Builder.aNew().sendGreetingLetter()

assertAndVerifyThat(
	theLoggingLine()
		.for(aGreetingLetter())
		.isLoggedWhen(greetingLetterIsSent()
	)
);

public Consumer<MailSender> aGreetingLetter(){
	return (MailSender sut) -> sut.sendGreetingLetter(mock(GreetingLetter.class));
}

public Consumer<EventLogger> greetingLetterIsSent(){
	return EventLogger::sentGreetingLetter;
}
assertAndVerifyThat(
	theLoggingLine()
		.for(
			(MailSender sut) -> sut.sendGreetingLetter(mock(GreetingLetter.class))
		)
		.isLoggedWhen(EventLogger::sentGreetingLetter)
	)
);
public void assertAndVerifyThat(TestContext testContext){
	testContext.assert();
	testContext.verify();
}

Craftsmanship analogy Tip: Use the same Import Order in IntelliJ IDEA as Eclipse's

comments powered by Disqus