Blog

The very latest thinking from us on BDD, Continuous Delivery, and more.

Unit tests are your specification by Seb Rose

Published Sat 5 Dec 2015 00:06

Recently a Schalk Cronjé forwarded me a tweet from Joshua Lewis about some unit tests he’d written.

I took a quick look and thought I may as well turn my comments into a blog post. You can see the full code on github.

Comment 1 – what a lot of member variables

Why would we use member variables in a test fixture? The fixture is recreated before each test, so it’s not to communicate between the tests (thankfully).

In this case it’s because there’s a lot of code in the setup() method (see comment 2) that initialises them, so that they can be used by the actual tests.

At least it’s well laid out, with comments and everything. If you like comments – and guess what – I don’t. And they are wrapped in a #region so we don’t even have to look at them, if our IDE understands it properly.

Comment 2 – what a big setup() you have

I admit it, I don’t like setup()s – they move important information out of the test, damaging locality of reference, and forcing me to either remember what setup happened (and my memory is not good) or to keep scrolling to the top of the page. Of course I could use a fancy split screen IDE and keep the setup() method in view too, but that just seems messy.

Why is there so much in this setup()? Is it all really necessary? For every test? Looking at the method, it’s hard to tell. I guess we’ll find out.

Comment 3 – AcceptConnectionForUnconnectedUsersWithNoPendingRequestsShouldSucceed

Ok, so I’m an apostate – I prefer test names to be snake_case. I just find it so much easier to parse.

The name, though, is pretty good. Very descriptive, although I’m still not sure what ShouldSucceed really means.

The problem starts when I try to relate the comment (C):

///GIVEN User1 exists AND User2 exists AND they are not connected AND User1 has requested to Connect to User2

with the name of the test (N), which incorporates the text: WithNoPendingRequests

and the code in the test (T):

Given(user1Registers, user2Registers, user1RequestsConnectionToUser2);
When(user2AcceptsRequestFrom1);
Then(succeed);
AndEventsSavedForAggregate<User>(user1Id, user1Registered, connectionRequestedFrom1To2, connectionCompleted);
AndEventsSavedForAggregate<User>(user2Id, user2Registered, connectionRequestFrom1To2Received, connectionAccepted);

So, the name (N) says that:

  1. the users should not be connected, and
  2. they (?) should have no pending requests [this is probably a cut and paste error]

The comment (C) says that:

  1. the users should not be connected, and
  2. user1 has requested a connection to user2

And the test (T) says:

  1. nothing explicit about whether they are already connected
  2. nothing explicit about pending requests [although there is a request connection command, which is implicitly pending]
  3. plenty of assertions on events that have nothing directly to do with identifying a successful connection

Why is any of this a problem? Well, in the first place the tests (a.k.a. specification) should be consistent and easy to understand. These tests are way better than many that I see, but still it’s worth thinking about how they can be even better.

And, secondly, tests should only assert on the behaviour that they are actually interested in. Over-specifying a test makes it brittle in the face of change, and one thing we can do without is brittle test suites.

Criticism is easy – is there an alternative?

  1. Use builders to create instances at the point you need them.
  2. Use methods on the builder to express attributes that are important for the behaviour being validated in the test.
  3. Only assert on the event(s) that are directly related to the behaviour being validated in the test (that may require the writing of more tests)

This is one way you could write it in Gherkin, using Roger (the requester) and Andrea (the accepter):

Given Roger and Andrea are not connected
And Roger has made a connection request to Andrea
When Andrea accepts the connection request
Then Roger and Andrea are connected

So here’s my re-write. Note that there are utility classes/methods that would need to be written to allow this to compile:

[Test]
public void AcceptingConnectionRequestFromUnconnectedUserShouldSucceed
{
  User roger = userBuilder.withNoConnections().build();
  User andrea = userBuilder.withNoConnections().build();
  roger.requestsConnectionWith(andrea);

  andrea.acceptsConnectionRequestFrom(roger);

  assertThatConnectionExistsBetween(roger, andrea);
}

I’ve dropped the Given/When/Then format, because, though it was concise, I don’t find that it adds much when we’re at the implementation level. In fact, the very cleverness that allows a list of commands or events to be handled by the G/W/T doesn’t work for me – I find that it obscures what’s going on.

Instead, I’ve stuck to old school arrange/act/assert structure delineated by white space. I’ve posited the existence of a user builder object and a user class for use in the test code. Is this an overhead? Sure, but it will get used over and over again, and it localises the behaviour in a single place, so that when the flow of events changes, or new flows are identified, there’s only a single place in the code that needs maintenance.

I’ve also suggested the withNoConnections() method – which will likely be a no-op – to emphasise that being unconnected is important. You may consider this overkill in this situation, since there’s it’s unlikely that two freshly created users will be connected. I prefer to be explicit about these things.

A question I’m still left with is “how should we implement assertThatConnectionExistsBetween()“. My initial thought would be that it would check that the connectionCompleted() event had been recorded, but that really only checks that the event has been emitted, not that the connection has actually taken place. Without digging deeper into the domain it’s hard to know which approach is more appropriate.

This article was originally published here.