Test Double

Test Double

Introduction

In a well-designed application,  SUT will not do everything that it required instead it will delegate to one or more collaborators to get things done. Managing these dependencies is one of the hard task and unit test focus on SUT, not other collaborators otherwise it will break the FIRST property. Fortunately, we have Test double techniques to help us manage the dependencies in Unit testing.

In general, SUT doesn’t know who are all its taking to because it knows only the contract of collaborators and the underlying implementation for these collaborators any time can change. We can use the technique called to Test Double to manage these dependencies effectively. The word ‘Test Double’ derived from ‘stunt double’. In movies, stunt double will replace with an actor to shoot risky stunts like fall from great heights, crash a car, etc. The stunt double is a highly trained individual who is capable to meet the specific requirements of the scene, but he/she may not be able to act as the actual actor in the normal scenes.

In unit testing, we can replace the real collaborators with Test Double (equivalent to stunt double). It helps us to control the state and behavior of SUT by providing fake or resembling collaborator. Since SUT uses the contracts for collaborators, it doesn’t differentiate whether it real or fake when it is interacting with Test Double, but we will able to complete all impossible tests without real collaborators.

Test Double

Test Double Types

Test double has few variations and we can use these variations for different scenarios. All variations are fake objects and in-memory representation of a collaborator.

Test Double 01.png

Test Stub

Stub provides canned answers to calls made during the test and it is not responding at all to anything outside what is programmed for the test. It has the controls over indirect inputs (input that flowing from a collaborator) to SUT.

Example:

Returning a list of products when SUT tells the product repository to get available products.

 

namespace Wingtip.Toys.ECommerce.Tests
{
    [TestClass]
    public class ProductControllerTest
    {
        [TestMethod()]
        public void It_should_returns_list_of_available_products()
        {
            //Arrange
            var expected = new List() {
                new Product { ProductID = 1, ProductName = "Tesla Model 3" },
                new Product { ProductID = 2, ProductName = "Mahindra XUV" },
                new Product { ProductID =3, ProductName = "Honda City" },
                new Product { ProductID =4, ProductName = "Nisson Sunny" }
            };
            var stubProductRepository = new Mock();
            stubProductRepository.Setup(p=>p.GetAll()).Returns(expected);
            var sut = new ProductController(stubProductRepository.Object);

            //Act
            var actual = (ICollection)((JsonResult)sut.Index()).Value;

            //Assert
            CollectionAssert.AreEquivalent(expected,actual);
        }
        
    }
}

Test Spy

Test Spy is a test stub, in addition to that it captures the indirect outputs (data which going from SUT to a collaborator) and it saves them for later verification.  This test double can be applied to real collaborators with a wrapper.

Example:

Recording email count which sent by an email service when order successfully processed.

 

[TestMethod]
public void It_should_trigger_the_email_when_order_process_successfully_completed()
{
    //Arrange
    var mockCartRepository = new Mock();
    mockCartRepository.Setup(m => m.GetAll());
    var mockPaymentProvider = new Mock();
    mockPaymentProvider.Setup(m => m.Pay());
    var mockOrderRepository = new Mock();
    var order = new Order();
    mockOrderRepository.Setup(m => m.Update(order)).Returns(true);
    var spyNotificationService = new Mock();
    var emailMessage = new EmailMessage();
    var mockLogger = new Mock();
    spyNotificationService.Setup(m => m.SetLogger(mockLogger.Object));
    spyNotificationService.Setup(m => m.Send(emailMessage));

    spyNotificationService.Object.SetLogger(mockLogger.Object);

    var sut = new OrderService(mockCartRepository.Object,
        mockPaymentProvider.Object,
        mockOrderRepository.Object,
        spyNotificationService.Object,
        mockLogger.Object);

    //Act
    sut.Process();
    sut.Process();

    //Assert
    spyNotificationService.Verify(m => m.Send(It.IsAny()),Times.Exactly(2));

}

Test Mock

Test Mock is a test stub, in addition to that, it verifies the interaction between SUT and collaborators. It observes the indirect outputs (data which going from SUT to a collaborator) and verifies them. This test double will participate in the test assertion to verify the SUT interaction with other collaborators.

Example:

Processing the order successfully and verifies the interaction with payment service, order repository, notification service, and logger.

 

[TestMethod]
public void It_should_process_the_order_successfully()
{
    //Arrange
    var mockCartRepository = new Mock<ICartRepository>();
    mockCartRepository.Setup(m => m.GetAll());

    var mockPaymentService = new Mock<IPaymentService>();
    mockPaymentService.Setup(m => m.Pay());

    var mockOrderRepository = new Mock<IOrderRepository>();
    var order = new Order();
    mockOrderRepository.Setup(m=>m.Update(order)).Returns(true);

    var mockNotificationService = new Mock<INotificationService>();
    var emailMessage = new EmailMessage();
    var mockLogger = Mock.Of<ILogger>();
    mockNotificationService.Setup(m => m.SetLogger(mockLogger));
    mockNotificationService.Setup(m => m.Send(emailMessage));
    mockNotificationService.Object.SetLogger(mockLogger);
    
    var sut = new OrderService(mockCartRepository.Object,
        mockPaymentService.Object,
        mockOrderRepository.Object, 
        mockNotificationService.Object,
        mockLogger);

    //Act
    var actual = sut.Process();

    //Assert
    Assert.IsTrue(actual);

    mockPaymentService.Verify(m=>m.Pay());
    mockOrderRepository.Verify(m=>m.Update(It.IsAny<Order>()));
    mockNotificationService.Verify(m => m.SetLogger(It.IsAny<ILogger>()));
    mockNotificationService.Verify(m=>m.Send(It.IsAny<IMessage>()));
}

Test Fake

Test fake is like a real implementation, but it implements a much simpler way. It purely designed for testing, so it is not suitable for production.

Example:

In memory repository to test CRUD operations of SUT.

 

public class FakeProductRepository : IProductRepository
{
    private readonly List<Product> _products = new List<Product>();
    private int _idCount;
    public Product GetById(int id)
    {
        return _products.First(x => x.ProductID == id);
    }

    public Product Add(Product t)
    {
        t.ProductID = ++_idCount;
        _products.Add(t);
        return t;
    }

    public bool Update(Product t)
    {
        _products.Remove(t);
        _products.Add(t);
        return true;
    }

    public bool Delete(int id)
    {
        var product = _products.First(x=>x.ProductID == id);
        _products.Remove(product);
        return true;
    }

    public IEnumerable<Product> GetAll()
    {
        return _products;
    }
}

 

Dummy Objects

Dummy Objects are around SUT method, but it never used inside the method. These objects are used as fillers in the parameter list.

Example:

Passing the Logger object to logs error while testing the success email mail scenario with email service.

Leave a Reply

Your email address will not be published. Required fields are marked *