xUnit Test Patterns defines the System Under Test (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.

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

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

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.

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.

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.

If you found this useful, you might also enjoy: