DEV Community

Cover image for Role tests for interfaces discovered through TDD
Manuel Rivero
Manuel Rivero

Posted on • Originally published at codesai.com

Role tests for interfaces discovered through TDD

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:

require 'spec_helper'
require 'cos/actions/users/register_user'
require 'cos/core/users/errors'

describe Actions::RegisterUser do
  let(:users_repository) { double('UsersRepository') }
  let(:new_user_name) {'@foolano'}

  before do
    stub_const('Users::Repository', users_repository)
  end

  describe "Registering a user" do
    it "can register a user that is not already registered" do
      allow(users_repository).
        to receive(:registered?).
        with(new_user_name).and_return(false)
      expect(users_repository).
        to receive(:register).with(new_user_name)

      Actions::RegisterUser.do(new_user_name)
    end

    it "fails when trying to register a user that is already registered" do
      allow(users_repository).
        to receive(:registered?).with(new_user_name).and_return(true)

      expect{Actions::RegisterUser.do(new_user_name)}.
        to raise_error(Users::Errors::AlreadyRegistered)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
require 'spec_helper'
require 'cos/actions/users/follow_user'
require 'cos/core/users/errors'

describe Actions::FollowUser do

  let(:follower_name) { "foolano" }
  let(:followed_name) { "mengano" }
  let(:users_repository) { double('UsersRepository') }

  before do
    stub_const('Users::Repository', users_repository)
  end

  describe "Following a user" do
    describe "when both users are registered" do
      it "succesfully adds a follower to a followed user" do
        allow(users_repository).
          to receive(:registered?).with(follower_name).and_return(true)
        allow(users_repository).
          to receive(:registered?).with(followed_name).and_return(true)
        expect(users_repository).
          to receive(:add_follower).with(follower_name, followed_name)

        Actions::FollowUser.do follower_name, followed_name
      end
    end

    describe "when any of them is not registered" do
      it "raises an error when trying to add a registered follower to a followed user that does not exist" do
        allow(users_repository).
          to receive(:registered?).with(follower_name).and_return(true)
        allow(users_repository).
          to receive(:registered?).with(followed_name).and_return(false)

        expect {Actions::FollowUser.do follower_name, followed_name}.
          to raise_error(Users::Errors::NonRegistered)
      end

      it "raises an error when trying to add a follower that does not exist to a registered followed user" do
        allow(users_repository).
          to receive(:registered?).with(follower_name).and_return(false)
        allow(users_repository).
          to receive(:registered?).with(followed_name).and_return(true)

        expect{Actions::FollowUser.do follower_name, followed_name}.
          to raise_error(Users::Errors::NonRegistered)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
require 'spec_helper'
require 'cos/queries/users/followers_of_user'

describe Queries::FollowersOfUser do
  let(:follower_names) {["pepe", "juan"]}
  let(:followed_name) {"koko"}
  let(:users_repository) { double('UsersRepository') }

  before do
    stub_const('Users::Repository', users_repository)
  end

  describe "Getting the followers of a user" do
    it "returns the list of followers" do
      allow(users_repository).to receive(:followers_of)
      .with(followed_name)
      .and_return(follower_names)

      expect(Queries::FollowersOfUser.do(followed_name)).to eq follower_names
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

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:

require 'spec_helper'

RSpec.shared_examples "users repository" do
  let(:user_name) { '@foolano' }
  let(:follower_name) { '@zutano' }
  let(:followed_name) { '@mengano' }

  it "knows when a user is registered" do
    given_already_registered(user_name)

    expect(@repo.registered?(user_name)).to be_truthy
  end

  it "knows when a user is not registered" do
    expect(@repo.registered?(user_name)).to be_falsy
  end

  it "registers a new user" do
    @repo.register(user_name)

    expect(@repo.registered?(user_name)).to be_truthy
  end

  it "adds a follower to a user" do
    given_both_already_registered(follower_name, followed_name)

    @repo.add_follower(follower_name, followed_name)

    expect(@repo.followers_of(followed_name)).to eq [follower_name]
  end

  def given_both_already_registered(follower_name, followed_name)
    given_already_registered(follower_name)
    given_already_registered(followed_name)
  end

  def given_already_registered(user_name)
    @repo.register(user_name)
  end
end
Enter fullscreen mode Exit fullscreen mode

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:

require 'spec_helper'
require 'infrastructure/users/repositories/in_memory'
require_relative '../cos/repositories_contracts/users_repository_role'

describe "In memory users repository" do
  before do
    @repo = Users::Repositories::InMemory.new
  end

  it_behaves_like "users repository"
end
Enter fullscreen mode Exit fullscreen mode
require 'spec_helper'
require 'infrastructure/users/repositories/mongoid'
require_relative '../cos/repositories_contracts/users_repository_role'

describe "Mongoid users repository" do
  before do
    @repo = Users::Repositories::Mongoid.new
  end

  it_behaves_like "users repository"
end
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)