Created
March 17, 2013 02:04
-
-
Save wojtekmach/5179226 to your computer and use it in GitHub Desktop.
POC "inline" contract tests with MiniTest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # POC "Inline" contract tests with MiniTest | |
| # | |
| # The idea is to run a contract test immediately after a mock object is created. | |
| # | |
| # Example: | |
| # | |
| # Say a `Registration` uses `Payment` (which uses `PaymentGateway`) | |
| # | |
| # def test_successfull_payment | |
| # payment = double | |
| # payment.stub(:create).with(100) { true } | |
| # verify_contract(PaymentTest, "test_returns_true_when_successfull", payment) | |
| # stub_const('Payment', payment) | |
| # | |
| # Registration.create('joe', :small).must_equal true | |
| # end | |
| # | |
| # (sorry for a weird mix of minitest/unit, minitest/spec & rspec/mocks :-) | |
| require 'minitest/autorun' | |
| # minitest + rspec/mocks | |
| require 'rspec/mocks' | |
| class MiniTest::Unit::TestCase | |
| def setup | |
| RSpec::Mocks.setup(self) | |
| end | |
| def teardown | |
| RSpec::Mocks.verify | |
| ensure | |
| RSpec::Mocks.teardown | |
| end | |
| end | |
| # minitest/contracts | |
| class MiniTest::Unit::TestCase | |
| def verify_contract(test, method, *actors) | |
| test.new("Contract test: #{test.name}").run_test(method, *actors) | |
| end | |
| end | |
| # code | |
| class PaymentGateway | |
| def self.create(merchant_id, amount) | |
| end | |
| end | |
| class Payment | |
| MERCHANT_ID = 42 | |
| def self.create(amount, options = {}) | |
| PaymentGateway.create(MERCHANT_ID, amount) | |
| end | |
| end | |
| class PaymentTest < MiniTest::Unit::TestCase | |
| def test_uses_payment_gateway_correctly | |
| gateway = double | |
| gateway.should_receive(:create).with(Payment::MERCHANT_ID, 100) { true } | |
| stub_const('PaymentGateway', gateway) | |
| Payment.create(100) | |
| end | |
| def test_returns_true_when_successfull(payment = Payment) | |
| PaymentGateway.stub(:create, true) do | |
| payment.create(100).must_equal true | |
| end | |
| end | |
| def test_returns_false_when_failed(payment = Payment) | |
| PaymentGateway.stub(:create, false) do | |
| payment.create(100).must_equal false | |
| end | |
| end | |
| end | |
| class Registration | |
| def self.price_for(plan) | |
| 100 | |
| end | |
| def self.create(username, plan) | |
| Payment.create(price_for(plan)) | |
| end | |
| end | |
| class RegistrationTest < MiniTest::Unit::TestCase | |
| def test_small_plan_costs_100 | |
| Registration.price_for(:small).must_equal 100 | |
| end | |
| def test_successfull_payment | |
| payment = double | |
| payment.stub(:create).with(100) { true } | |
| verify_contract(PaymentTest, "test_returns_true_when_successfull", payment) | |
| stub_const('Payment', payment) | |
| Registration.create('joe', :small).must_equal true | |
| end | |
| def test_failed_payment | |
| payment = double | |
| payment.stub(:create).with(100) { false } | |
| verify_contract(PaymentTest, "test_returns_false_when_failed", payment) | |
| stub_const('Payment', payment) | |
| Registration.create('joe', :small).must_equal false | |
| end | |
| end |
Author
@kurko @jbrains thanks guys for feedback. I guess you're right, it's hard to have the double enforce the contract without complicating the double or making the contract less strict:
-
standard test:
def test_new_customers @repository.add_customer 'alice' @repository.add_customer 'bob' @repository.add_customer 'carol' @repository.new_customers.map(&:name).must_equal ['carol', 'bob'] end
-
something a double (with stubbed out
add_customer) can implement:def test_new_customers @repository.add_customer 'alice' @repository.add_customer 'bob' @repository.add_customer 'carol' @repository.new_customers.size.must_equal 2 end
Back to square one... @jbrains do you know if somebody followed-up on your idea from 2009 on static analysis for violation or lack of contracts when using mocks?
@wojtekmach I commonly use something along the lines of (with RSpec):
describe ClassA do
it_should_behave_like "object A contract"
# ...normal ClassA tests
end
describe ClassB do
it_should_behave_like "object A contract"
# ...normal ClassB tests
end
shared_examples "object A contract" do
describe ClassA do
subject { ClassA.new }
it "should respond to token" do
expect { subject.interface }.to_not raise_error NoMethodError
end
end
endIn this case, ClassB's instance uses ClassA's instance.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
First, I applaud you for trying to do this. I'm glad that someone is trying to make this work. Even so, I don't like the idea of running a contract test on a test double instance. This feels backwards, but it might work out. I have to think about it more.
I don't like the idea of specifying in
verify_contract()a test name, or even a set of test names, that the test double has to pass. When stubbingcreate()to returntrue, you need to write at least one contract test showing whencreate()returnstrue. That would mean that your test double stubbingcreate()to returntrueshould pass at least one contract test. That sounds right, but feels risky. It might work. Key question: what would have to be true for a test double T to pass at least one contract test C_n in C, but break the contract of its interface? (In other words: what would cause a false positive?)Your example here has one feature that worries me much more: your contract test for
Paymentincludes the implementation detail of usingPaymentGateway. This tells me thatPaymentclients want to work with thePaymentGatewaydirectly. When I see the twoPaymentTeststest_returns_false_when_failedandtest_returns_true_when_successfull, I see more evidence thatPaymentis doing nothing useful: these tests check that the client returns whatever its collaborator returns, and nothing more. Why arePaymentandPaymentGatewaydifferent things? It seems likePaymentGatewayimplementsPayment. (Worse:test_uses_payment_gateway_correctlyshows an implicit dependency betweenPaymentandPaymentGatewaythat destroys much of the value of contract testing.)Keep at it. This could go somewhere interesting, and if it does, it could really help Ruby programmers who don't want omakase. :)