What Is A Spy?

Photo by Chris Yang on Unsplash

 

Spies operate in international clandestine affairs—as well as in unit tests.

When we want to detect and assert, in a test, that we called a method on a service, how should we go about this? 

Let’s refer back to yesterday’s Register() method on a high-level business logic class:

  public async Task<Customer> Register(CustomerRegistration registration)
  {
     Validate(registration);
     var customer = registration.ToCustomer();
     await Repository.SaveCustomer(customer);
     return customer;
  }

How could we verify in a unit test that we

  1. called Repository.SaveCustomer() at all, and
  2. that we used the correct argument value?

In other words, we want a test to fail if we removed the Repository.SaveCustomer() line

  public async Task<Customer> Register(CustomerRegistration registration)
  {
     Validate(registration);
     var customer = registration.ToCustomer();
     // await Repository.SaveCustomer(customer); Commented out!
     return customer;
  }

or we replace the argument with an incorrect value:

  public async Task<Customer> Register(CustomerRegistration registration)
  {
     Validate(registration);
     var customer = registration.ToCustomer();
     await Repository.SaveCustomer(null);
     return customer;
  }

Like yesterday, we will again use a fake collaborator to help us. Yet this time, we need a Spy, not a Stub.

Spies record that we are calling them and also how we are calling them. Afterwards, we can use that information in a unit test verification step.

  [Fact]
  public void Given_New_Customer_When_Call_Register_Then_Save_Customer()
  {
     var spyRepository = new SpyCustomerRepository();
     var useCase = new RegisterNewCustomerUseCase(spyRepository);

     await useCase.Register(FredFlintstoneRego);

     spyRepository.WasSaveCustomerCalled.Should().BeTrue(); 
     spyRepository.PassedInCustomer.Should().BeEquivalentTo(FredFlintstone);
  }

The last two lines are asserting that

  1. Repository.SaveCustomer() was called, and
  2. The correct Customer instance (here: FredFlintstone) was the argument.

OK, but what does SpyCustomerRepository do?

Let’s examine the class definition:

  public class SpyCustomerRepository : ICustomerRepository
  {
     public Customer CustomerToReturn;
     public bool WasSaveCalled;
     public Customer PassedInCustomer;

     public SpyCustomerRepository(Customer customerToReturn = null)
     {
        CustomerToReturn = customerToReturn;
     } 

     public async Task<Customer> GetCustomer(string emailAddress)
     {
        return CustomerToReturn;
     }

     public async Task SaveCustomer(Customer customer)
     {
        WasSaveCustomerCalled = true;
        PassedInCustomer = customer;
     }
  }

There isn’t much to SpyCustomerRepository. All it does is capture in public class member variables whether we called SaveCustomer() and the passed-in Customer instance.

Hold on, isn’t this a mock?

No, not quite. Spies and mocks are very similar insofar that both help us assert unit tests. However, spies are more passive than mocks. Spies record the calling information while the unit test asserts against this recorded information from the spy. On the other hand, mocks perform the validation too.

So, when you want to verify the calling of a method against a service abstraction, you can employ a spy to record the details of this call in a unit test.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply