---
title: Don't Stub the System Under Test
teaser:
tags: web,ruby,testing
author: Joe Ferris
published_on: 2013-11-22
---

xUnit Test Patterns defines the [System Under Test (SUT)][sut] as:

> whatever class, object or method we are testing; when we are writing customer
> tests, the SUT is probably the entire application or at least a major
> subsystem of it.

[sut]: http://xunitpatterns.com/SUT.html

The System Under Test helps us focus on what we're really testing, what
[Depended-On Components (DOC)][doc] interact with it, and how to replace a
Depended-On Component with a [Test Double][double] (such as a stub, spy, or
fake).

[doc]: http://xunitpatterns.com/DOC.html
[double]: http://xunitpatterns.com/Test%20Double.html

However, it can be tempting to also stub parts of the System Under Test. This
should be avoided.

## Why not stub the System Under Test

The goal of the guideline "Don't Stub the System Under Test" is to help us use
tests as a guide for when to split up a class. If a behavior is so complicated
that we felt compelled to stub it out in a test, that behavior is its own
concern that should be encapsulated in a class.

## Example

Let's say you're test-driving the client library for a credit card processing
gateway. You start with a basic test case for the `CreditCard` class:

    describe CreditCard, '#create_charge' do
      it 'returns transaction IDs on success' do
        body = { 'transaction_id' => '1234' }.to_json
        stub_request(:post, 'payments.example.com/cards/4111/charges').
          to_return(body: body)
        credit_card = CreditCard.new('4111')

        result = credit_card.create_charge(100)

        expect(result.transaction_id).to eq('1234')
      end
    end

The implementation:

    class CreditCard
      def initialize(id)
        @id = id
      end

      def create_charge(amount)
        response = Net::HTTP.start('payments.example.com') do |http|
          request = Net::HTTP::Post.new("/cards/#{@id}/charges")
          request.body = { 'amount' => amount }.to_json
          http.request(request)
        end

        data = JSON.parse(response.body)
        Response.new(transaction_id: data['transaction_id'])
      end
    end

Now that we can place charges, we need to be able to refund them. The test looks
familiar:

    describe CreditCard, '#refund_charge' do
      it 'returns transaction IDs on success' do
        body = { 'transaction_id' => '2345' }.to_json
        stub_request(:post, 'payments.example.com/cards/4111/charges/1234/refund').
          to_return(body: body)
        credit_card = CreditCard.new('4111')

        result = credit_card.refund_charge('1234')

        expect(result.transaction_id).to eq('2345')
      end
    end

The implementation for `#refund_charge` looks similar to `#create_charge`:

    def refund_charge(transaction_id)
      response = Net::HTTP.start('payments.example.com') do |http|
        request =
          Net::HTTP::Post.new("/cards/#{@id}/charges/#{transaction_id}/refund")
        http.request(request)
      end

      data = JSON.parse(response.body)
      Response.new(transaction_id: data['transaction_id'])
    end

We can't stand this kind of duplication. So, it's time to extract a method for
the common logic. We start by writing a test for a common, private method:

    describe CreditCard, '#create_transaction' do
      it 'performs JSON POST requests' do
        request = { 'request' => 'body' }
        response = { 'transaction_id' => '1234' }
        stub_request(:post, 'payments.example.com/example_path').
          with(body: request.to_json)
          to_return(body: response.to_json)
        credit_card = CreditCard.new('4111')

        result = credit_card.send(:create_transaction, '/example_path', request)

        expect(result.transaction_id).to eq('2345')
      end
    end

Next, we implement that method:

    private

    def create_transaction(path, data = {})
      response = Net::HTTP.start('payments.example.com') do |http|
        post = Net::HTTP::Post.new(path)
        post.body = data.to_json
        http.request(post)
      end

      data = JSON.parse(response.body)
      Response.new(transaction_id: data['transaction_id'])
    end

We can expect a call to this method in our tests:

    describe CreditCard, '#create_charge' do
      it 'returns transaction IDs on success' do
        expected = stub('expected')
        credit_card.
          stub(:create_transaction).
          with('/cards/4111/charges/1234/refund', amount: 100).
          and_return(expected)
        credit_card = CreditCard.new('4111')

        result = credit_card.create_charge(100)

        expect(result).to eq(expected)
      end
    end

Then we can use the method in our class:

    def create_charge(amount)
      create_transaction("/cards/#{@id}/charges", amount: amount)
    end

We can then create a similar stub for `#refund_charge`. No more duplication!

However, things have gone a little wrong: we're not listening to our tests.
The need to stub out a private method in our SUT tells us there's a concern to
be encapsulated: formatting and transmitting requests to our gateway server.

Let's extract that concern.

First, we'll move our tests for the private method over to a new `Client`
class test:

    describe Client, '#post' do
      it 'performs JSON POST requests' do
        request = { 'request' => 'body' }
        response = { 'transaction_id' => '1234' }
        stub_request(:post, 'payments.example.com/example_path').
          with(body: request.to_json)
          to_return(body: response.to_json)
        client = Client.new

        result = client.create_transaction('/example_path', request)

        expect(result.transaction_id).to eq('2345')
      end
    end

We can copy the code over from `#create_transaction`:

    class Client
      def post(path, data = {})
        response = Net::HTTP.start('payments.example.com') do |http|
          post = Net::HTTP::Post.new(path)
          post.body = data.to_json
          http.request(post)
        end

        data = JSON.parse(response.body)
        Response.new(transaction_id: data['transaction_id'])
      end
    end

Then, we'll change our test to inject a stubbed dependency:

    describe CreditCard, '#create_charge' do
      it 'returns transaction IDs on success' do
        expected = stub('expected')
        client = stub('client')
        client.
          stub(:post).
          with('/cards/4111/charges/1234/refund', amount: 100).
          and_return(expected)
        credit_card = CreditCard.new(client, '4111')

        result = credit_card.create_charge(100)

        expect(result).to eq(expected)
      end
    end

Next, we'll change our class to accept and use that dependency:

    class CreditCard
      def initialize(client, id)
        @client = client
        @id = id
      end

      def create_charge(amount)
        @client.post("/cards/#{@id}/charges", amount: amount)
      end

      def refund_charge(transaction_id)
        @client.post("/cards/#{@id}/charges/#{transaction_id}/refund")
      end
    end

By avoiding a stub on the SUT, we discovered that we can cleanly split our class
into two: one class to handle the high level details of which requests to make
for semantic actions like creating charges, and another class which knows how to
translate those actions into HTTP requests.

## How to avoid stubbing the System Under Test

Each time I'm tempted to stub the SUT, I think about why I didn't want to set up
the required state.

If extracting a helper or factory to set up the state wouldn't be ugly or cause
other issues, I'll do that and remove the stub.

If the method I'm stubbing has complicated behavior that's aggravating to
retest, I use that as a cue to extract a new class, and then I stub the new
dependency.

## What's next

If you found this useful, you might also enjoy:

* [Spy vs. Spy][spy] (test spies)
* [Have You Ever... Faked It?][fake] (test fakes)
* [How to Stub External Services in Tests][external]
* [Using Capybara to Test JavaScript that Makes HTTP Requests][capybara]

[spy]: https://thoughtbot.com/blog/spy-vs-spy
[fake]: https://thoughtbot.com/blog/fake-it
[external]: https://thoughtbot.com/blog/how-to-stub-external-services-in-tests
[capybara]: https://thoughtbot.com/blog/using-capybara-to-test-javascript-that-makes-http
