Last week’s post was about how hexagonal architecture results in fast, mock-free tests around your core domain. Unfortunately, that does not remove all mocks, yet it groups them in the same, less critical, zone. In last week’s code sample, this was the controller. I concluded that at least, this was easier to manage. Let’s see how.

This is the 7th post in a series about avoiding mocks. If you haven’t, you might start from the beginning.

Mock concentration

Let’s get back to the last post’s code sample. As a reminder, it’s a very basic TODO app built on Rails. I extracted the domain part, the tasks, in a core domain area. This allowed to push all mocks out of this section. A consequence though, is that all mocks gathered in the controller test. Here is the controller code :

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 controller is now dealing both with the Twitter connection and the database. This is visible in the controller test :

require 'rails_helper' RSpec . describe TasksController , type: :controller do before :each do allow ( TwitterClient :: Client ). to receive ( :update ) end # ... describe "PUT #update" do context "with valid params" do let ( :new_attributes ) { { done: true } } it "updates the requested task" do task = Task . create! valid_attributes put :update , params: new_attributes . merge ( id: task . to_param ) task . reload expect ( task ). to be_done end it "tweets about completed tasks" do task = Task . create! valid_attributes expect ( TwitterClient :: Client ). to receive ( :update ). with ( task . description ) put :update , params: { id: task . to_param , done: true } end it "redirects to the task" do task = Task . create! valid_attributes put :update , params: valid_attributes . merge ( id: task . to_param ) expect ( response ). to redirect_to ( task_url ( task . id )) end end # ... end end

We need to stub out the twitter API for most tests. We are also still using a mock to verify that the tweet is sent. Finally, as we can see from the test execution times, we are still using the database in some tests.

If the project grew large this would become an issue. Sadly, mocking is often the fix people jump on …

💡 Mocking is the unfortunate quick fix to slow tests.

From a mocking point of view, our current controller test can seem worse than before ! There’s something pretty effective we can do though !

In memory fakes

Instead of stubbing and mocking in every test, let’s write a full fledged in-memory fake that does the job we need. We could then install it once and for all, and forget about it !

Actually, this is nothing new. This is exactly what Rails provides out of the box with ActionMailer::Base.delivery_method = :test .

Here’s how we could do the same thing for our Twitter Client.

spec/rails_helper.rb

class FakeTwitterClient def initialize @tweets = [] end attr_accessor :tweets def update ( message ) @tweets . push ( message ) end end RSpec . configure do | config | # ... config . before ( :each ) do stub_const ( "TwitterClient::Client" , FakeTwitterClient . new ) end end

spec/controllers/tasks_controller_spec.rb

it "tweets about completed tasks" do task = Task . create! valid_attributes put :update , params: { id: task . to_param , done: true } expect ( TwitterClient :: Client . tweets ). to include ( task . description ) end

Simple isn’t it ?

Wait a sec …

There’s a catch though … How do we make sure that this fake is behaving the same way as the real thing ?

Let’s run the same tests on both ! We could mimic the twitter API in our fake, but that might not be a great idea. Do you remember the moto “Always wrap your 3rd parties” ? It takes all its meaning here, for 2 reasons.

The first is to make faking easier. We can build a minimal wrapper API that is just enough for our use. By keeping this interface small, we’ll make it a lot easier to fake.

The second reason is that we can write real integration tests on the 3rd party through this wrapper. They’d look like ordinary unit tests, except that they’d end up calling the real 3rd party in a sandbox. They are usually pretty slow, but as 3rd parties don’t change everyday, that’s ok. We can ensure up-front that integration will go well. As a bonus, we can be very fast to detect and contain changes to online services. (I’m looking at you Scrappers!)

Here is what it would look like for our Twitter client :

class FakeTwitterClient def initialize @tweets = [] end attr_accessor :tweets def tweet ( message ) @tweets . push ( message ) end def search_tweets ( text ) @tweets . select { | tweet | tweet . include? ( text ) } end end class RealTwitterClient def initialize ( & block ) @client = Twitter :: REST :: Client . new ( & block ) end def tweet ( message ) @client . update ( message ) end def search_tweets ( text ) @client . search ( "from:test_user #{ text } " ) end end

As you can see, we renamed update to tweet in the wrapper. We’d have to update the calls accordingly. Let’s look at the tests.

require 'rails_helper' require 'infrastructure/twitter_client' require 'securerandom' RSpec . shared_examples "a twitter client" do | new_client_instance | let ( :client ) { new_client_instance } it "sends tweets" do token = SecureRandom . uuid message = "Philippe was here #{ token } " client . tweet ( message ) expect ( client . search_tweets ( token )). to include ( message ) end end context FakeTwitterClient do it_behaves_like "a twitter client" , FakeTwitterClient . new end context RealTwitterClient , integration: true , speed: :slow do it_behaves_like "a twitter client" , ( RealTwitterClient . new do | config | config . consumer_key = "TEST_CONSUMER_KEY" config . consumer_secret = "TEST_CONSUMER_SECRET" config . access_token = "TEST_ACCESS_TOKEN" config . access_token_secret = "TEST_ACCESS_SECRET" end ) end

We had to add a search method to our interface for the sake of testing. This should remain “For testing only”. We’d also adapt the controller test to use this search_tweets method.

Let’s look at where we stand now. We’re injecting each mock only once. Tests are fast yet straightforward, almost as if they were testing the real thing. Doing so, we’ve split our system in cohesive parts and we’ve wrapped our 3rd parties. We’ve actually done a lot more than removing mocks ! Mocking really is a design smell.

💡 Merciless mock hunting will improve the design of your system !

Last word about implementation

Sometimes, this 3rd party wrapper can become pretty complicated. Try to reuse as much of it as possible between the real and the fake. For example, an ORM, like ActiveRecord for example, is a wrapper around the database. Reimplementing a fake ORM would be real challenge. We’re far better plugin it on top of SQLite instead !

References

Smart people have already spoken and written about this subject. If you want to learn more, I recommend that you have a look at Aslak Hellesøy’s Testable Architecture talk. James Shore, the author of The Art of Agile Development, also wrote a pattern language called Testing Without Mock.

Next week

This was the 7th blog post in a series about how to avoid mocks. Hopefully, I’m reaching the end ! Next week’s post should be the last in series, and deal with a few remaining points. What to do when you really need a mock ? What about mocking and legacy code ?