Metaprogramming Ruby Ch1

Course3 Elasticsearch

Build Robust & Production Quality Applications - Lesson 8: - Mocks & Stubs

Magic Tricks of Testing (Sandi Metz Talk)

We want unit tests to be:

  • Thorough
  • Stable
  • Fast
  • Few

Focus on Messages Objects are simple-minded.

Message Origin

Objects under test have three message origin options: *1. Incoming Messages *2. Outgoing Messages *3. Messages sent to self (messages that are completely contained inside the object and are unknown to the outside)

Message Type

  • 1. Query - returns something, changes nothing (addition method)
  • 2. Command - returns something, changes something (save method)

We often times conflate commands and queries (like .(pop))

Rules of the Unit Test Minimalist

INCOMING QUERY MESSAGES

Test Incoming Query Messages by making assertions about what they send back. Makes an assertion about a value (state). Here we want to test the interface and not the implementation. If we just care about what value is returned and not how we get there... then we can change the code without breaking it.

INCOMING COMMAND MESSAGES

Test incoming command messages by making assertions about direct public side effects. Direct means its the responsibility of the last ruby class involved. You're making an assertion on the new value as result of calling that command.

Key Takeaway - The receiver of incoming messages has the sole responsibility of asserting the result direct public side effect. This is the sole place where we are making an assertions about values, testing the value of the thing returned

MESSAGES SENT TO SELF

Private Methods - Don't test private methods. Don't make assertions about their result. Do not expect to send them. Doing so will add no value to your tests and will make it impossible to change in the future without breaking something. They also discourage other devs from touching your code so you may want to add the comment "if these tests fail, delete them."

Caveat: Break rule if it saves $$$ during development.

Key Takeaway - Ignore them.

OUTGOING QUERY MESSAGES

Making assertions on query messages is the job of the receiver. Therefore making an assertion on outgoing messages is redundant and should be avoided. It makes your tests are break when there is a change and it is very cumbersome to understand what going on. It adds no value.

Rule: Do not test outgoing query messages. Do not make assertions about their result. Do not expect to send them.

You will notice that both tests made to self and tests on outgoing query messages have the same rule, don't do them. This is because they have no visible side effects, they are invisible to the rest of your application and their tests add no value.

Rule: If a message has no visible side-effects, the sender should not test it.

OUTGOING COMMAND MESSAGES

Outgoing command messages must be sent, they have to be sent or the app will not be correct. Testing, however the distant side effects of sending the message is an integration test. Asserting that the state of a thing changed in the database is not job of the outgoing command message. It's job is ensure that a message is sent. Asserting that some change has occurred or making an assertion about the state of the databse will forever connect you to that result and the objects.. even those distant involved.

Testing that a message was sent requires a mock. Testing this way makes a bet that the edge (the API...the message sent) is more stable than the distant side effect and the path to it.

Rule: Test outgoing command messages by setting expectations on them being sent. Caveat: Break Rule if ide effects are stable and cheap... and close. The further away - the worse.

Rule: It is our job to ensure that mocks honor their contract. Ensure test doubles stay insync with the API

Gems that help you manage your mocks: * Bogus * Quacky * Rspec-Fire * Minitest-Firemock

Build Robust & Production Quality Applications - Lesson 8: - Mocks & Stubs

Further Reading

Check out this blog post by Martin Fowler. Here is an excert I found valuable:

Meszaros uses the term Test Double as the generic term for any kind of pretend object used in place of a real object for testing purposes. The name comes from the notion of a Stunt Double in movies. (One of his aims was to avoid using any name that was already widely used.) Meszaros then defined four particular kinds of double:

Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists. *Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example). *Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test. Stubs may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'. *Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive. Of these kinds of doubles, only mocks insist upon behavior verification. The other doubles can, and usually do, use state verification.

Also, check out this talk on Mocks

Build Robust & Production Quality Applications - Lesson 8: - Mocking

In previous lessons, extracted some of our logic into policy, domain and service objects. We can now update our TodosController spec to reflect these new objects, but we don't have to test the functionality of the service level object, for example - we just to make sure that the object can respond to the actions extracted to the Service Level process. We can accomplish this with mocking & stubbing. Ensuring that a message has been sent and the object can respond to that message. We are not concerned with the response for that message.

So far with stubbing (which is all we have done up until this point), we have always been asserting on values or the state of the system. With mocking, we asserting on communication. Mocks will always be in unit testing becasue you ar stubbing out your collaborators. If you have to create too many mocks, then you know that you are missing another layer of abstraction. Limiting your spec to focusing on one object allows you code smells like these.

Picking up from our last example, we would create a mock with the following. Please note that the trigger is after the assertion:

it "delegates to CreditDeduction to deduct credit" do
  credit_deduction = double("credit deduction")
  CreditDeduction.stub(:new).(alice).and_return(credit_deduction)

  expect(credit_deduction).to_recieve(:deduct_credit)
  post :create, todo: {name: "cook", description: "I like cooking!" }

end