Abstract Tests

By Phlip Plumlee
May 3, 2010 | Comments: 2

My last post showed how to mock a webservice. When you have more than one webservice, all their common code, tests, and mocks should remain DRY. This post demonstrates a ruthlessly effective test pattern that forces many different interfaces to behave as similarly as possible, using the minimum possible test code.

The Problem

Here are two WET test cases that authorize credit card transactions, through two different payment gateways. "WET" stands for "We Edit Timidly". You may notice that one test case has one more assertion than the other. That's an example of a timid edit. Whoever added the assert didn't really know if the other case should get it, too:

class PayflowTest < Test::Unit::TestCase

  def test_successful_authorization
    @gateway.stubs(:ssl_post).returns(successful_authorization_response)
    
    assert response = @gateway.authorize(@amount, @credit_card, @options)
    assert_equal "Approved", response.message
    assert_success response
    assert response.test?
    assert_equal "VUJN1A6E11D9", response.authorization
  end
...
class SkipJackTest < Test::Unit::TestCase

  def test_authorization_success    
    @gateway.expects(:ssl_post).returns(successful_authorization_response)

    assert response = @gateway.authorize(@amount, @credit_card, @options)
    assert_instance_of Response, response
    assert_success response
    assert_equal '9802853155172.022', response.authorization
  end
...

When things are that similar, you should make them more similar, then pass all the tests, then merge their common code, then pass all the tests, then exploit the structure of the common code to perform further refactors.

class SkipJackTest < Test::Unit::TestCase

  def test_successful_authorization
    @gateway.expects(:ssl_post).returns(successful_authorization_response)

    assert response = @gateway.authorize(@amount, @credit_card, @options)
    assert_instance_of Response, response
    assert_success response
    assert @gateway.test?
    assert_equal '9802853155172.022', response.authorization
  end
...

Extract Superclass Refactor

Now that SkipJackTest is on board with our plan, we make response into an instance variable, @response, pass all tests, and then pull out this module:

module AbstractTests

  def test_successful_authorization
    @gateway.expects(:ssl_post).returns(successful_authorization_response)

    assert @response = @gateway.authorize(@amount, @credit_card, @options)
    assert_success @response
    assert @gateway.test?
    assert_successful_authorization
  end
   
end
...
class PayflowTest < Test::Unit::TestCase
  include AbstractTests

  def assert_successful_authorization
    assert_instance_of Response, @response
    assert_equal "Approved", @response.message
    assert_equal "VUJN1A6E11D9", @response.authorization
  end
...
class SkipJackTest < Test::Unit::TestCase
  include AbstractTests

  def assert_successful_authorization
    assert_instance_of Response, @response
    assert_equal '9802853155172.022', @response.authorization
  end

Conclusion

The new pattern is a little harder to follow. That's okay, because other test cases should self-document our code, and test cases may be a little bit redundant. The example-style test cases should present every detail, in order from top to bottom, in simple statements, without any refactoring. Those test cases will, in turn, help keep our Abstract Tests honest!

After the concrete setup() creates a concrete @gateway, the abstract test mocks its wire service, and commands the mock to return the XML from the concrete successful_authorization_response() method. Each version of that XML sample contains its own unique, gateway-specific authorization code, so the specific assertion, assert_successful_authorization(), can trivially check for it.

From here, other transaction types, such as capture and purchase, should front in the AbstractTests module, so it will eventually represent the list of interfaces that all gateways provide. And as many gateways as possible should re-use the same abstract tests. The gateways that use some different architecture must get their own abstract tests.

The tests improve the odds of achieving Programming Nirvana: You can swap from one gateway to another at whim, without changing any code. Because all gateway adapters pass the same tests, the odds are very high that they will all transparently perform the same.

Going forward, if we needed to add a new kind of gateway to our list of concrete classes, the first import AbstractTests call will fail, because none of its assert_successful_authorization() and similar methods exist or work yet. Fix this by stubbing out all the abstract test cases with concrete overrides: def test_successful_authorization ; 'TODO'; end. Then build passing obvious tests which work, and copy their statements into the setups and assertions that the abstract test cases need.

Refactoring is like hammering on hot steel to harden it. With our resulting pattern, each time we think of a new behavior that all gateways must follow, we can trivially add its common assertions to the common test cases. This helps us achieve the most flexibility, and the most code safety, within the smallest surface area of details that can change freely.


You might also be interested in:

2 Comments

Hey Phlip,

Is there any reason you left

assert_instance_of Response, @response

in both test methods?

Good catch - thanks!

The next phase, in general, obeys my suggestion "exploit the structure of the common code to perform further refactors."

If the code were any more complex (comparable to the real active-merchant), then the first refactor must perforce create very mundane, simplistic code, only stretched between base and abstract classes. That refactor, itself, is not very important.

After the first refactor you now have a class hierarchy, and you can use it to move details up and down, to approach Contractive Delegation [ http://broadcast.oreilly.com/2009/04/contractive-delegation.html ].

So imagine if we refactored those tests into 3 layers. The most basic test suite would enforce all behaviors that all gateways must provide, and a mixin suite would enforce all behaviors that only SOAP gateways must do.

That assert_instance_of() line would move all the way to the most basic suite, because all gateways must return a response object deriving from the most basic Response class. Robert C Martin calls this pattern "Stairway to Heaven".

(And, no, I didn't leave that assertion there just to subsequently make an example of it; I think I just stopped refactoring when the concrete classes looked visually similar to one of my current projects. Another cautionary tale!)

News Topics

Recommended for You

Got a Question?