Creating assertable helpers

Ricard
4 min readNov 25, 2016

--

For some time I’ve been using a pattern I informally called assertable helpers, which I’ve found to be very helpful when working with integration and acceptance tests (and legacy code in particular). Their creation follows a natural progression but, perhaps surprisingly, little seems to be found about it in the wild.

This post is born with two objectives:

  • Learn if others are applying the same pattern (and what name it receives, as my Google-fu skills are rusting)
  • Explain how is it applied and its benefits, in case you’ve never used them

Note: the examples in this post will use generic language constructs, as this approach is largely language-independent.

A natural progression

Let’s start with a peculiar test:

class UserTest extends AcceptanceTestCase {    test "Has a country of residence" {
newUser := User(123, "ES");
userRepository.save(newUser);
user := userRepository.find(user.id());
assertEquals("ES", user.location().country().code());
}
}

We can quickly identify (at least) three issues with this test:

  • It looks more like repository test, but this is because I’ve failed to write a better example. Please ignore this one.
  • It uses string literals to represent concepts. Not only are we losing expressiveness, but we are coupling ourselves to a specific primitive representation that could change in the future.
  • It uses a deep object graph, which you should avoid or at least hide it from the outside (https://en.wikipedia.org/wiki/Law_of_Demeter).

We are going to pretend that we have inherited this domain model and we are not allowed to change it (but we still have to test it).

Repeating the assertion for the first time

Next we add a new test that uses a similar assertion:

class UserTest extends AcceptanceTestCase {    test "Has a country of residence" {
newUser := User(123, "ES");
userRepository.save(newUser);
user := userRepository.find(user.id());
assertEquals("ES", user.location().country().code());
}
test "Can change its country of residence" {
newUser := User(123, "ES")
newUser.changeCountry("FR");
userRepository.save(newUser);
user := userRepository.find(user.id());
assertEquals("FR", user.location().country().code());
}
}

We are now beginning to see that this assertion could pose a problem in the future. At the smallest change of the way of identifying a country, or the object graph that exposes a user’s current location, our tests will break and we will have to update multiple lines of code. It’s time to create a custom assertion:

class UserTest extends AcceptanceTestCase {    test "Has a country of residence" {
newUser := User(123, "ES");
userRepository.save(newUser);
user := userRepository.find(user.id());
assertLivesInCountry("ES", user);
}
test "Can change its country of residence" {
newUser := User(123, "ES")
newUser.changeCountry("FR");
userRepository.save(newUser);
user := userRepository.find(user.id());
assertLivesInCountry("FR", user);
}
void assertLivesInCountry(string countryCode, User user) {
assertEquals(countryCode, user.location().country().code());
}
}

Going beyond the single class

So far we haven’t suffered too much about this issue, as it’s been located inside a single class. But now that we will begin to repeat the same lines of code in different classes, we feel like it’s time to prevent future duplication. What should we do? Some options:

  • We could move the assertLivesInCountry() method to a superclass from which all these test cases will inherit from. This is far from ideal, as we can find ourselves with lots of assertions bundled together in a giant abstract test case.
  • We could make assertLivesInCountry() a static method and move it to a AssertUser class (for example AssertUser.livesInCountry("ES", user)).
  • We could create a custom matcher (using Hamcrest, for example).
  • We could alternatively create an AssertableUser that contains all useful assertions that can be made against a user.

Our first assertable helper

Let’s rewrite our last example with our new AssertableUser:

class UserTest extends AcceptanceTestCase {    test "Has a country of residence" {
newUser := User(123, "ES");
userRepository.save(newUser);
user := findUser(user.id());
user.assertLivesInCountry("ES");
}
test "Can change its country of residence" {
newUser := User(123, "ES")
newUser.changeCountry("FR");
userRepository.save(newUser);
user := findUser(user.id());
user.assertLivesInCountry("FR");
}
AssertableUser findUser(int userId) {
return AssertableUser(userRepository.find(userId));
}
}
class AssertableUser { constructor(User aUser) {
user = aUser;
}
void assertLivesInCountry(string countryCode) {
assertEquals(countryCode, user.location().country().code());
}
}

We are simply wrapping a production User instance and making an assertion, but it opens a door to a much more expressive world of assertions, which can even do queries themselves to other repositories to complement the information that the user has.

Let’s try checking if the user has a friend:

class UserTest extends AcceptanceTestCase {    ...    test "Can have a friend" {
newUser := aUser()
aFriend := aDifferentUser()
newUser.addFriendshipWith(aFriend);
userRepository.save(newUser);
user := findUser(user.id());
user.assertHasFriend(aFriend);
}
...
}
class AssertableUser { constructor(User aUser, FriendsRepository repository) {
user = aUser;
friendsRepository = repository;
}
... void assertHasFriend(User aFriend) {
friends := friendsRepository.findFriendsFor(user);
assertTrue(friends.contains(aFriend));
}
}

The complexity of knowing who to query to know if a user is friends with another user has been moved outside our test. This allows the test to concentrate on the what, not the how.

But remember, be the change you want to see in the world. If you’re working with legacy code, these helpers can prove very useful in your daily work. However, instead of making your life easier just in your tests, try to change your production code so that all the codebase benefits from these improvements.

What else?

Not much, really. It is a simple pattern. But after talking with many people and discovering that they weren’t aware of this approach, I began to feel like more people could benefit from having this tool in their tool belt.

--

--