Domain-Driven Tests

Dan North just blogged about how the DRY may not apply to stories/tests/specs:

But then I saw a pattern beginning to emerge. The test code was starting to read like a story. He would introduce these little methods and classes just before their one walk-on line in the narrative. It was quite an eye-opener for me […] The A-ha! moment for me was when I imagined reading a story book where the plot and characters had been DRYed out. Everything would be in footnotes or appendices.

That’s a very interesting point. In a project some time ago I could see the pain that DRY test cases can cause. The team was great, we had enough tests and bugs were rare. Code quality was all right since the safety net provided by TDD allowed everyone to refactor mercilessly.

The problem for the new developers was to actually understand the system. As most agile projects the specifications were expressed in automated tests; but in this case those were written with DRY instead of readability in mind.

Looking into a well written and Domain-Driven Designed code you can understand the domain and how business concepts relate to each other but it is still very hard to understand why those objects act like that. The "why" question is not answered by the production code, it is answered by tests and they should be readable for that matter. I like to think of tests as executable story cards, for me is often more important for a test to be understandable than DRY.

But we all know that code duplication is terrible. How can we avoid that and still have readable tests? In a recent project we tried to follow the same approach as RSpec’s Story Runner, using lots of Behaviour-Driven Development concepts to create really expressive tests. We did DRY our tests a bit but instead of magical setUp()-and-tearDown() methods that contained the common code we preferred to extract those in methods with names that were meaningful in that domain, both to the developer as to the user.

As a quick simple example, suppose you have a test like this:

public class MessagesShouldBeProcessedTest {

	MessageHandler handler;
	Publisher fakePublisher;
	Message msg1 = new Message("m1", true);
	Message msg2 = new Message("m2", true);
	Message msg3 = new Message("m3", true);
	Message invalidMsg = new Message("invalid", false);

	@Before
	public void setupMocks() {
		handler = mock(Handler.class);
	}

	@Before
	public void setupPublisher() {
		fakePublisher = new FakePublisher();
	}

	@After
	public void checkMocks(){
		verifyAll();
	}

	@Test
	public void allMessagesShouldBeProcessed() {
		setupWith(msg1, msg2, msg3);

		handler.handle(msg1);
		handler.handle(msg2);
		handler.handle(msg3);
		replayAll();

		process();
	}

	@Test
	public void shouldNotHandleInvalidMessage() {
		setupWith(msg1, invalidMsg, msg3);

		handler.handle(msg1);
		handler.handle(msg3);
		replayAll();

		process();
	}

	private void process() {
		fakePublisher.publishAll();
	}

	private void setupWith(Message... msgs) {
		for (Message message : msgs) {
			fakePublisher.prepareToPublish(message);
		}

	}
}

This code uses instance variables and before/after methods to minimise repetition but the readability was poor. Instead of writing code like that we would write this:

public class MessagesShouldBeProcessedTest {

	MessageHandler handler;
	Publisher fakePublisher;
	Message msg1 = new Message("m1", true);
	Message msg2 = new Message("m2", true);
	Message msg3 = new Message("m3", true);
	Message anInvalidMsg = new Message("invalid", false);

	@After
	public void checkMocks() {
		verifyAll();
	}

	@Test
	public void allMessagesShouldBeProcessed() {
		// given
		aWorkingHandler();
		aWorkingPublisher();
		thatTheUserHasPosted(msg1);
		thatTheUserHasPosted(msg2);
		thatTheUserHasPosted(msg3);

		// expect
		handlerToBeCalledFor(msg1);
		handlerToBeCalledFor(msg2);
		handlerToBeCalledFor(msg3);
		replayAll();

		// when
		messagesArePublished();
	}

	@Test
	public void shouldNotHandleInvalidMessage() {
		// given
		aWorkingHandler();
		aWorkingPublisher();
		thatTheUserHasPosted(msg1);
		thatTheUserHasPosted(anInvalidMsg);
		thatTheUserHasPosted(msg3);

		// expect
		handlerToBeCalledFor(msg1);
		handlerToBeCalledFor(msg3);
		replayAll();

		// when
		messagesArePublished();
	}

	private void aWorkingPublisher() {
		fakePublisher = new FakePublisher();
	}

	private void aWorkingHandler() {
		handler = mock(Handler.class);
	}

	private void thatTheUserHasPosted(Message message) {
		fakePublisher.prepareToPublish(message);
	}

	private void handlerToBeCalledFor(Message message) {
		handler.handle(message);
	}

	private void messagesArePublished() {
		fakePublisher.publishAll();
	}

}

The amount of code is pretty much the same but I think the latter is far more readable than the first one, even for a extremely simple example. We still had some noise like the unintuitive flow caused by Easymock’s replay and verify methods but it was a lot better already. Most of the utility methods used in the given/then/when blocks are shared among tests so most were extracted into a common super-class. When a new developer joined the team it wasn’t hard for him to understand the test, although it took a while to learn the “new” way of writing tests.

And what about JBehave? Well, I really don’t like JBehave 1.x, I’m waiting for JBehave 2 official release. The new version has these ideas more easily expressed, like in the example:

public class GridSteps extends Steps {

	private Game game;
	private StringRenderer renderer;

	@Given("a $width by $height game")
	public void theGameIsRunning(int width, int height) {
		game = new Game(width, height);
		renderer = new StringRenderer();
		game.setObserver(renderer);
	}

	@When("I toggle the cell at ($column, $row)")
	public void iToggleTheCellAt(int column, int row) {
		game.toggleCellAt(column, row);
	}

	@Then("the grid should look like $grid")
	public void theGridShouldLookLike(String grid) {
		ensureThat(renderer.asString(), equalTo(grid));
	}

}

13 Responses to “Domain-Driven Tests”


  1. 1 David Peterson Jul 2nd, 2008 at 2:31 am

    Phillip, have you looked at Concordion? (http://www.concordion.org) I’d be interested to hear what you think of it.

  2. 2 Alexandre Martins Jul 11th, 2008 at 10:27 am

    Hey Phillip,

    Liked the second test. Despite the fact that you’re duplicating code, I think the main purpose of a test is to be readable, so you should know the context before verifying any behaviour (setups), so it’s easier to spot the outcome of this verification. Tear downs should always be hidden when possible.

    @David Peterson, regarding Concordion, we’ve being using it in my current project. It’s pretty flexible in the way that you can write your specification, but poor in terms of writing assertions. Unless your client wants the HTML output to put in a Wiki, I wouldn’t recommended using it. You can achieve pretty much the same result by writing tests in a Domain-Driven Design way (feedback from a had after a business sign-off).

    Cheers,
    Alexandre.

  3. 3 Alexandre Martins Jul 11th, 2008 at 10:30 am

    But you could change you mock framework and get rid of that replayAll(); eh ;-)

    See you!

  4. 4 Rafael Noronha Mar 24th, 2009 at 10:01 am

    Phillip,

    Some bdd stuff is coming from .net community.

    I wrote the folowing code after some research on NBehave.
    http://rafanoronha.net/testando-com-bdd/

    Please don’t note the lambdas stuff… ;D

    []’s

  1. 1 Clean Code: Book Review at Mark Needham Pingback on Sep 15th, 2008 at 7:53 pm
  2. 2 Internal DSLs and Paradigms: Declarativeness at Fragmental.tw Pingback on Sep 17th, 2008 at 11:36 pm
  3. 3 Where do Acceptance Tests go to Die? at Fragmental.tw Pingback on Sep 29th, 2008 at 11:05 pm
  4. 4 Fragmental » Blog Archive » Domain-Driven Design & Agile: Fechando Malas Pingback on Oct 8th, 2008 at 1:29 am
  5. 5 What is a unit test? at Mark Needham Pingback on Oct 10th, 2008 at 11:23 pm
  6. 6 Fearless Change: Book Review at Mark Needham Pingback on Oct 21st, 2008 at 11:40 pm
  7. 7 What make a good unit test? at Mark Needham Pingback on Dec 4th, 2008 at 12:36 am
  8. 8 TDD: Mock expectations in Setup at Mark Needham Pingback on Dec 19th, 2008 at 8:59 pm
  9. 9 Book Club: The Readability of Tests - Growing Object Oriented Software (Steve Freeman/Nat Pryce) at Mark Needham Pingback on Jun 20th, 2009 at 11:29 am

Leave a Reply








Creative Commons License

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.