mobile menu icon

Role tests for implementation of interfaces discovered through TDD

Published by Manuel Rivero on 03/04/2022

Learning, TDD, Contract Testing, Role Testing, Object-Oriented Design, Polymorphic Testing, Testing, Test Doubles


Introduction.

Working through the first three iterations of a workshop’s exercise, we produced several application services that at some point collaborated with a users repository that we hadn’t yet created so we used a test double in its place in their tests.

These are the tests:

In these tests, every time we allow or expect a method call on our repository double, we are defining not only the messages that the users repository can respond to (its public interface)[1] but also what its clients can expect from each of those messages, i.e. its contract.

In other words, at the same time we were testing the application services, we defined from the point of view of its clients the responsibilities that the users repository should be accountable for.

The users repository is at the boundary of our domain. It’s a port that allows us to not have to know anything about how users are stored, found, etc. This way we are able to just focus on what its clients want it to do for them, i.e., its responsibilities.

Focusing on the responsibilities results in more stable interfaces. As I heard Sandi Metz say once:

“You can trade the unpredictability of what others do for the constancy of what you want.”[2]

which is a very nice way to explain the “Program to an interface, not an implementation”[3] design principle.

How those responsibilities are carried out is something that each different implementation (or adapter) of the users repository port is responsible for. However, the terms of the contract that its clients rely on, must be respected by all of the adapters. They must play their roles. In this sense, any adapter must be substitutable by any other without the clients being affected, (yes, you’re right, it’s the Liskov substitution principle).

Role or contract tests.

The only way to ensure this substitutability is by testing each adapter to check if it also respects the terms of the contract, i. e. it fulfils its role. Those tests would ensure that the Liskov substitution principle is respected[4].

I will use the term role test used by Sandi Metz because contract test has become overloaded[5].

Ok, but how can we test that all the possible implementations of the user repository respect the contract without repeating a bunch of test code?

Using shared examples in RSpec to write role tests.

There’s one very readable way to do it in Ruby using RSpec.

We created a RSpec shared example in a file named users_repository_role.rb where we wrote the tests that describes the behaviour that users repository clients were relying on:

Then for each implementation of the users repository you just need to include the role tests using RSpec it_behaves_like method, as shown in the following two implementations:

You could still add any other test that only has to do with a given implementation in its specific test.

This solution is very readable and reduces a lot of duplication in the tests. However, the idea of role tests is not only important from the point of view of avoiding duplication in test code. In dynamic languages, such as Ruby, they also serve as a mean to highlight and document the role of duck types that might otherwise go unnoticed because there is no interface construct.

Acknowledgements.

I’d like to thank my Codesai colleagues for reading the initial drafts and giving me feedback.

Notes.

[1] Read more about objects communicating by sending and receiving messages in Alan Kay’s Definition Of Object Oriented

[2] You can find a slightly different wording of it in her great talk Less - The Path to Better Design at 29’48’’.

[3] Presented in chapter one of Design Patterns: Elements of Reusable Object-Oriented Software book.

[4] This is similar to J. B. Rainsberger’s idea of contract tests mentioned in his Integrated Tests Are A Scam talk and also to Jason Gorman’s idea of polymorphic testing.

[5] For example Martin Fowler uses contract test to define a different concept in Contract Test.

References.

Photo from Anna Rye in Pexels

Originally published in Manuel Rivero's blog.

Volver a posts