What Is A Stub?
During unit testing, we want to avoid making network calls. However, our class-under-test may interact with objects tied to remote services, like databases. So how do we ensure that we make no network calls during unit testing? Easy. We temporarily replace these networked collaborators with fake, local ones.
Stubs are a ‘fake collaborator’—often just called fakes or mocks even though those terms have a specific meaning in the same context.
A Stub helps us get through a unit test. In other words, if we didn’t have the stub, we would not be able to run the unit test. Often stubs are query functions that supply us with needed canned data.
Let’s look at an example.
This listing contains two methods: A public Register() method, registring a new customer in a system. Register() first calls Validate(). The listing for Validate is also provided.
public async Task<Customer> Register(CustomerRegistration registration) { Validate(registration); var customer = registration.ToCustomer(); await Repository.SaveCustomer(customer); return customer; } private void Validate(CustomerRegistration registration) { if (registration == null) throw new MissingCustomerRegistration(); registration.Validate(); var existingCustomer = Repository.GetCustomer(registration.EmailAddress); if (existingCustomer != null) throw new CustomerAlreadyExists(); }
To unit test Register(), we will need a fake collaborator for Repository. The implementation of Repository, used in production, makes calls to a database. As mentioned, this is undesirable for unit testing. Instead, we will use a Stub for Repository, which is of type ICustomerRepository, defined as
public interface ICustomerRepository { Task<Customer> GetCustomer(string emailAddress); Task Save(Customer customer); }
In this instance, we would use a stub when we encounter the line
var existingCustomer = Repository.GetCustomer(registration.EmailAddress);
Depending on what we are testing for, we would want the stub behaviour to differ. We can test two distinct scenarios:
Repository.GetCustomer() returns
- a null, indicating the Customer was not found, or
- a Customer instance, indicting a customer was found.
Here is a hand-coded manual implementation of such a stub:
public class StubCustomerRepository : ICustomerRepository { public Customer CustomerToReturn; public StubCustomerRepository(Customer customerToReturn = null) { CustomerToReturn = customerToReturn; } public async Task<Customer> GetCustomer(string emailAddress) { return CustomerToReturn; } public async Task Save(Customer customer) { // Nothing to do. } }
Below we are using StubCustomerRepository in a unit test verifying that a CustomerAlreadyExists exception is thrown whenever Repository.GetCustomer() returns a Customer instance:
[Fact] public void Given_Get_Customer_From_Repository_When_Call_Register_Then_Throw_CustomerAlreadyExists_Exception() { var stubCustomerRepository = new StubCustomerRepository(customerToReturn: FredFlintstone); var useCase = new RegisterNewCustomerUseCase(stubCustomerRepository ); Action register = () => useCase.Register(FredFlintstoneRego); register.Should().ThrowExactly<CustomerAlreadyExists>(); }
At test setup time, we prime StubCustomerRepository with Customer FredFlintstone. Now when the test calls Repository.GetCustomer(), our stub instance, StubCustomerRepository, will return FredFlintstone. Take another look at the code for StubCustomerRepository: Firstly, we prime the stub by passing the constructor a Customer instance which we store inside the stub. Later, when we call GetCustomer(emailAddress) we return the stored Customer instance. Our stub ignores the emailAddress parameter—only the real collaborator, ie, the database is interested in using the emailAddress to seach for the Customer. On the other hand, to make our stub work, we only want it to regurgitate the Customer instance set when we constructed the stub.
Stubs are fake collaborators that help us get through our unit tests. Often they return canned data needed for the test.
Leave a Reply
Want to join the discussion?Feel free to contribute!