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.
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: