Avoid Mocks and Test Your Core Domain Faster With Hexagonal Architecture

As I’ve written in my last few posts, we can get a long way to avoid mocks with small scale coding best practices. Unfortunately, when systems reach a certain size, we need something at architecture scale.

This is the 6th post of a series about avoiding mocks. If you haven’t, you can start by the beginning.

A drawing of a hexagon-shaped building

Why do we end up with mocks in large systems ?

A few years ago, I joined a team working in a legacy system. We wanted to apply TDD and refactoring. As expected, adding tests legacy code proved a real challenge. With a lot of effort we could manage to add a few. Unfortunately, this did not seem to have any positive effect on our maintainability ! The tests we were writing all involved a lot of mocking. The system was such a large mass of spaghetti code that there was no clear place to mock. We were actually mocking where it seemed the easiest on a test by test basis. We were making progress at small scale, but the big picture was not improving at all !

Large systems are beasts with many faces. They Ā involve a lot of IOs. They write and read data from the disk and databases. They call 3rd parties and remote services.

As we test these large systems, we’ll need to stub out these IOs. Even if the tests are fast enough, we usually don’t want to call external services for real. Most of the time though, tests are slow. That’s 2 reasons why end up adding some mocks.

Here comes the nasty part. These large systems are so complex that we, developers, don’t have the full picture. When we test, we tend to mock at different places, depending on our knowledge. This is bad for maintenance. Mocks duplicate production code behavior. When many different mocks are in place to isolate an external dependency, we end up with ‘n’ versions of the code. That’s a nightmare to refactor !

šŸ’” When many different mocks are in place to isolate an external dependency, we end up with ‘n’ versions of the code !

Hexagonal architecture to the rescue

Alistair Cockburn coined the term. The idea is pretty simple : Ā isolate a piece of code from all dependencies. This is particularly useful for the core functional areas. With this in place, it becomes straightforward (and fast) to test the core domain logic.

To main techniques to isolate a piece of code from any dependency areĀ :

It’s also possible to split a system in many ‘hexagons’ and glue them together with adapters at startup. If you want to learn more on this style of architecture, have a look into the Domain Driven Design lore. This community has been building systems this way for years now.

Enough talk, show me the code !

This post was the occasion to try to inject a Hexagonal Architecture and a dash of DDD in a Rails application. There’s one caveat though : DDD shines on complex systems. Unfortunately, large and complex systems make very poor didactic examples. The following code highlights the gains about mocking. We would not use DDD for such a small app in real life.

The starting point

I chose a simple TODO app. I started by generating a scaffold for a Task with a description and a done/not-done status. As third party interaction, completing a task sends an automatic tweet. Here is the only specific code I wrote on top of the Rails scaffold :

app/models/task.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Task < ApplicationRecord
  include ActiveModel::Dirty

  validates :description, presence: true

  before_save :tweet_if_done

  private
  def tweet_if_done
    if done_changed?
      TwitterClient::Client.update(self.description)
    end
  end
end

Thanks Jason Charnes for the change attribute technique.

spec/models/task_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'rails_helper'

RSpec.describe Task, type: :model do

  it "is valid with all attributes set" do
    expect(Task.create(description: "Finish presentation", done: false)).to be_valid
  end

  it "requires a description" do
    expect(Task.create(description: nil, done: false)).to be_invalid
    expect(Task.create(description: "", done: false)).to be_invalid
  end

  it "tweets when a task is finished" do
    task = Task.create(description: "Wash the car", done: false)

    expect(TwitterClient::Client).to receive(:update).with("Wash the car")

    task.done = true
    task.save
  end
end

This is pretty simple and to the point !

5 years later

Now let’s imagine that the app grew to tens of thousands of lines. We added a lot of features to the app, which transformed the TODO domain into a very complex thing. Now suppose that, for the sake of maintenance, we want to isolate the domain logic into its own hexagon. Unlike traditional Rails ActiveRecords, we want to make it independent from the database. We also want it to be independent from the Twitter API.

Here is what the code might look like.

lib/core/task.rb

First, we have a core task class, independent from anything else. The Core module is our hexagon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
module Core
  class Task

    attr_reader :description
    attr_accessor :db_id

    def initialize(attributes = {})
      @description= "What do you need to do ?"
      @done = false
      @done_subscribers = []

      self.update(attributes)
    end

    def done?
      @done
    end

    def mark_as_done
      @done = true
      @done_subscribers.each {|proc| proc.call(self) }
    end

    def update(attributes={})
      self.description= attributes[:description] unless attributes[:description].nil?
      self.mark_as_done if attributes[:done]
    end

    def notify_when_done(&proc)
      @done_subscribers.push(proc)
    end

    def description=(desc)
      raise ArgumentError.new("Task description cannot be blank") if desc.blank?

      @description = desc
    end
  end
end

As we can see, it contains only domain logic and nothing else.

# spec/lib/core/task_spec.rb

Here is the corresponding test, fast, mock-free and independent from the database and any external system.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
require 'rails_helper'
require 'core/task'

context 'Task' do

  let(:task) { Core::Task.new}

  specify 'is not done by default' do
    expect(task).not_to be_done
  end

  specify 'comes with a default description' do
    expect(task.description).not_to be_blank
  end

  specify 'it can be initialized from a hash' do
    task = Core::Task.new(description: "Old description", done: true)

    expect(task.description).to eq("Old description")
    expect(task).to be_done
  end

  specify 'can have a custom description' do
    task.description= "Clean up the house"
    expect(task.description).to eq("Clean up the house")
  end

  specify 'forbids empty descriptions' do
    expect{task.description = nil }.to raise_error(ArgumentError)
    expect{task.description = "" }.to raise_error(ArgumentError)
  end

  specify 'can be done' do
    task.mark_as_done
    expect(task).to be_done
  end

  specify 'publishes when done' do
    done_task = nil
    task.notify_when_done {|t| done_task = t}

    task.mark_as_done

    expect(done_task).to be(task)
  end

  specify 'can be updated with a hash' do
    task.update(description: "New description", done: true)

    expect(task.description).to eq("New description")
    expect(task).to be_done
  end

  specify 'has no DB id by default' do
    expect(task.db_id).to be_nil
  end
end
# lib/infrastructure/task_repo.rb

To read and save with the database, we now go through an adapter. This is not considered to be part of our core domain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
module Infrastructure
  class TaskRepo

    def self.all
      Task.all.map do |db_task|
        from_db(db_task)
      end
    end

    def self.load(db_id)
      from_db(Task.find(db_id))
    end

    def self.save(task)
      if task.db_id.nil?
        db_task = Task.create!(to_db_attributes(task))
        task.db_id = db_task.id
      else
        db_task = Task.find(task.db_id)
        db_task.update!(to_db_attributes(task))
      end
      task
    end

    def self.delete(task)
      unless task.db_id.nil?
        db_task = Task.find(task.db_id)
        db_task.destroy!
        task.db_id = nil
      end
    end

    private

    def self.to_db_attributes(task)
      {description: task.description, done: task.done?}
    end

    def self.from_db(db_task)
      result = Core::Task.new
      result.db_id = db_task.id
      result.description = db_task.description
      result.mark_as_done if db_task.done?
      result
    end

  end
end
# app/controllers/tasks_controller.rb

Finally, all the pieces interact together in the controller. This controller basically does what the previous version was, it’s just using different classes. Obviously, we’ll need to adapt the views and the tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
require 'core/task'
require 'infrastructure/task_repo'

class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]

  # GET /tasks
  def index
    @tasks = Infrastructure::TaskRepo.all
  end

  # GET /tasks/1
  def show
  end

  # GET /tasks/new
  def new
    @task = Core::Task.new
  end

  # GET /tasks/1/edit
  def edit
  end

  # POST /tasks
  def create
    begin
      @task = Core::Task.new(task_params)
      Infrastructure::TaskRepo.save(@task)

      redirect_to task_url(@task.db_id), notice: 'Task was successfully created.'

    rescue ArgumentError
      render :new
    end
  end

  # PATCH/PUT /tasks/1
  def update
    begin
      @task.update(task_params)
      Infrastructure::TaskRepo.save(@task)

      redirect_to task_url(@task.db_id), notice: 'Task was successfully updated.'

    rescue ArgumentError
      render :edit
    end
  end

  # DELETE /tasks/1
  def destroy
    Infrastructure::TaskRepo.delete(@task)
    redirect_to tasks_url, notice: 'Task was successfully destroyed.'
  end

  private
    def set_task
      @task = Infrastructure::TaskRepo.load(params[:id])
      @task.notify_when_done do |task|
        TwitterClient::Client.update(task.description)
      end
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def task_params
      params.permit(:description, :done)
    end
end

The main gain here is that our core domain, our most valuable asset is now easy to test without mocks. This means that we are able to write and execute fast tests for this area of the code. This puts us in a great position to increase our competitive advantage in our core business !

šŸ’” By keeping your tests around your core domain fast, Hexagonal Architecture increases your competitive advantage.

As you can see, we are now wiring everything together at the controller level. We could later build a facade to isolate the controller from the inside of our domain. A presenter might do, but it seemed over-engineered, even in this made up example. (I’ll post something about that some day)

Next post

As we can deduce from the controller code above, we still have to use fakes or mocks when testing the controller. The good thing though is that this is now more local which already makes mocking less of an issue. If a mock is used in less tests, it’s easier to use the same mock everywhere ! This is a great opportunity for simplifying test setup, as we’ll see in the next post about in-memory fakes.

Comments