The Magic Tricks of Testing

April 18, 2020   

These are my notes from the youtube video: The Magic Tricks of Testing presented by Sandi Metz at Rails Conf 2013.

Why Do you hate your tests:

  • They are slow
  • They are fragile - a small app change breaks all the tests
  • They tests are expensive - (see above)

For many the promise of testing has not been fulfilled.

Sandi recommends you should delete some tests.

Unit Tests: Goals This talk is not about integration tests.

A integration test pokes one side of the application and tests a result on the other side.

Unit tests should be

  • Thorough
  • Stable
  • Fast
  • Few - don’t write to many lines of these

Imagine your app - the objects and messages they pass

  • often your application turns into spagetti.
  • Solution: Focus the messages!

Objects are simple-minded

  • they are black boxes with an inside and an outside

Focus on the the object under tests and then messages it receives

The Object under test receive incoming messages and sends outgoing messages.

The object under test can send messages to itself.

Message Orgins

  • Incoming
  • Sent to Self
  • Outgoing

Types: query message - does not have a side affect - (add 1) Command Messages: returns nothing/change something (save a file)

We conflate commands and queries at our peril. but we do this often

But we do this all the time

  • example “pop”
  • automatic return to chain messages

Its important ot understand if you have a command or a query or both.

Message Origin x Type Message Type query command Origin incomming Sent to Self Outgoing

Incomming Query Messages

  • Test incoming query messages by making assertions about what they send back

ie Assert the result

gear class has gear_inches which sends a query to wheel

Test the interface !not the implementation!

Test incoming command message by making assertions about direct public side effects

“direct” the responsiblity of the last ruby class involved

Dry it out The Receiver of incoming message has sole responsiblity for asserting the result of direct public side effects

Sent to Self

!!!Do not test private methods!!!

  • They should be tested by the incoming messages
  • Do not set and an expection that a private message will be sent.

Its ok to break the rules sometimes

  • TDD’ing a complicated private method
  • Maybe you can’t bear to delete them?
  • Put them in one place and add a comment “if these tests fail delete them”

They will cost you money!

Outgoing Query Messages

Same rules as ‘Sent to Self’

gear_inches in Gear sends a diameter message to Wheel

Assert result of outgoing to gear.wheel.diameter!!! Don’t do it. This should be tested in Wheel.

gear.wheel.expect(:diameter)

  • Don’t do this. Don’t care what wheel does

Rule: Do not test outgoing query messages

Do not make assertions about their result. Do not expect to send them.

If a message has no visible side-effects the sender should not test it

Outgoing Command Messages

Given a

Gear with gear_inches and set_cog

Wheel with diameter

Observer with changed

If Observer get’s changed it will save data in the database

observer.changed(chainring, cog) «< This message MUST be sent

DO NOT DO THIS: It can be tempting to test the side effect: You could write code that triggers the outgoing message and then makes assertions about what happens when you do:

Ex:

class GearTest < MiniTest::Unit::TestCase
  
  def test_saves_changed_cog_in_db
    @observer = Observer.new
    @gear = Gear.new(
              chainring: 52,
              cog:       11,
              observer:  @observer)

    @gear.set_cog(27)
    # assert soemthing about state of the db
  end
end

^^^ This depends on a distant side affect!

Changing the database is not Gear’s responsiblity! This is a integration test hiding out in your unit tests. Instead you should:

class GearTest < MiniTest::Unit::TestCase

  def test_notifies_observers_when_cogs_change
    @observer = MiniTest::Mock.new
    @gear     = Gear.new(
                  chainring: 52,
                  cog:       11,
                  observer:  @observer)
    @observer.expect(:changed, true, [52, 27])
    @gear.set_cog(27)
    @observer.verify
  end
...

This test depnds on the interface. it tests at the nearest edge.

  • Gear is responsible for sending #changed to @observer

Rule: Test outgoing command messages by setting expections on them.

“Expect to send outgoing command messages”

Caveat: Bear rule if side effects are stable and cheap:

  • If its a value object and its close, just grab it.
  • its better to depend on a stable edge with a mock

BUT, What happens if Observer stops implementing #changed?

That’s a pain!

API Drift - A mock is a test double

The fake thing an the real thing promise that that implement a common API.

Rule: Honor the contract ensure test doubles stay in sync with the API.

This is impossible to do this by hand, look at these tools

Automatgically:

  • minitest requires that stubbed methods exist
  • rspec: #should_receive(:msg).and_call_original
  • psyho/bogus
  • benmoss/quacky
  • xaviershay/rspec-fire
  • cfcosta/minitest-firemock

Be a minimalist!!!

  • Test everything Once
  • Test the interface
  • Trust Collaborators
  • Inist on simplicity

Getting better at testing! It takes practice.