We were working through the following iterations of an exercise:
A user can register with a user name.
For instance: @foolano
If anyone else has already registered with that name there is an error.
A user can follow another users.
To do so it's only required to know the user name of the user to be followed.
Anyone can query the followers of any given user just knowing its user name.
The registered users and their followers must be persisted.
(source: Diseño modular dirigido por pruebas workshop at PyConEs 2014)
We produced several application services for this features that at some point collaborated with a users repository that we hadn't yet created so we mocked it in their specs.
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) but also what its clients 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.
This 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."
which is a very nice way to explain the "Program to an interface, not an implementation" design principle.
How those responsibilities are carried out is something that each different implementation (or adapter) of the users repository must be responsible of.
However, the terms of the contract that its clients rely on, must be respected by all of the adapters.
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).
The only way to ensure this substitutability is by testing each new adapter to see if it also respects the terms of the contract.
This is related to J. B. Rainsberger's idea of contract tests mentioned in his Integrated Tests Are A Scam talk and in his great TDD course, and also to Jason Gorman's idea of polymorphic testing.
Ok, but how can we test that all the possible implementations of the user repository respect the contract without repeating a bunch of tests?
This is one way to do it in Ruby using RSpec.
We created a RSpec shared example in a file named users_repository_contract.rb where we wrote the tests that characterize the behavior that users repository clients were relying on:
Then for each implementation of the users repository you just need to include the contract using RSpec it_behaves_like method, as shown in the following two implementations:
You could still add any other test that only had to do with a given implementation in its spec.
This solution is both very readable and reduces a lot of duplication in the tests.
However, the idea of contract tests is not only important from the point of view of testing.
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.