The stubbing spy

Do you really need a stub or do you better use a spy? Or is it a spy object with some stubbed specifications? Let's see some examples using pyDoubles:

You can choose a stub object to prepare the scenario and then, assert on the returned value of the system under test.

  1.  
  2. def test_using_a_stub(self):
  3. USERID = 20
  4. collaborator = stub(Collaborator())
  5. when(collaborator.one_arg_method).with_args(
  6. USERID).then_return(SUCCESS)
  7. sut = SystemUnderTests(collaborator)
  8.  
  9. result = sut.exercise_method()
  10.  
  11. assert_that(result, equal_to(OK))
  12.  

The test above says: "I don't really care what the SUT does internally as long as the returned value is the expected, but it might need help from a collaborator, so I set it up, just in case."
Note the "might" part of the sentence. It is not necessary to specify the possible arguments in the call to the collaborator. This tests is more robust and still serves the same:

  1.  
  2. def test_stub_simplification(self):
  3. collaborator = stub(Collaborator())
  4. when(collaborator.one_arg_method).then_return(SUCCESS)
  5. sut = SystemUnderTests(collaborator)
  6.  
  7. result = sut.exercise_method()
  8.  
  9. assert_that(result, equal_to(OK))
  10.  

Now, let's replace the stub with a spy object, assuming we don't need to specify any stub behavior:

  1.  
  2. def test_if_arguments_are_important_check_them_out(self):
  3. USERID = 20
  4. collaborator = spy(Collaborator())
  5. sut = SystemUnderTests(collaborator)
  6.  
  7. result = sut.exercise_method()
  8.  
  9. assert_that_method(collaborator.one_arg_method
  10. ).was_called().with_args(USERID)
  11. assert_that(result, equal_to(OK))
  12.  

The test above means: "The SUT needs to call its collaborator at least once in order to complete the operation. I want to make sure that happens apart from getting the expected value."
Depending on the problem, we can have just one test with the 2 asserts, or maybe 2 tests, with one assert each.

We didn't need to define any stub method, but what if we need it?:

  1.  
  2. def test_if_arguments_are_important_check_them_out(self):
  3. USERID = 20
  4. collaborator = spy(Collaborator())
  5. when(collaborator.one_arg_method).then_return(SUCCESS)
  6. sut = SystemUnderTests(collaborator)
  7.  
  8. result = sut.exercise_method()
  9.  
  10. assert_that_method(collaborator.one_arg_method
  11. ).was_called().with_args(USERID)
  12. assert_that(result, equal_to(OK))
  13.  

We don't tell the stub what arguments is going to receive. That is not important. The stub part is intended to prepare the scenario. The easier the scenario setup is, the better. We do assert on the arguments at the end of the execution.

Conclusion:
If calling the collaborator is a critical part of the action, use a spy and make sure arguments are passed in as they should. If you need to stub out a method in the spy object, do not specify the arguments in the stub definition. In the stub definition, just tell the framework the returned value and later on, assert on the arguments once the system under test execution has finished

Reason: If you specify arguments in the stub definition and also don't assert at the end, you need to debug the failing test to find out that maybe some argument wasn't passed in.
It is more efficient to let the doubles framework tell you what was wrong 🙂

Enjoyed reading this post?
Subscribe to the RSS feed and have all new posts delivered straight to you.
  • Alberto Peña

    I do not agree 🙂

    For me, “If calling the collaborator is a critical part of the action” you should use a mock. Then, if your test becomes too complex because of that mock, the test is telling you something about your design. I think that using a spy in that situation hides a design smell.

    Disclaimer: Obviously, it depends on the problem 😛

  • http://www.carlosble.com Carlos Ble

    Let’s change “critical” for “necessary” or “important”. My point here is not “Spy Vs Mock”, but rather “Spy Vs Stub”. In the scenarios we are facing in some tests, using mocks would make the tests more fragile. Not complex but just fragile. The mock would say… “it is important that only that call be made” while the spy would say “it is important to confirm that call, but I don’t care about anything else”, which is pretty much what we want 🙂

    Thanks for your feedback.