DEV Community

Cover image for The Role of Mocks and Spies in Unit Testing
Dany Paredes
Dany Paredes

Posted on • Originally published at danywalls.com

The Role of Mocks and Spies in Unit Testing

A few days ago, one friend was writing tests for an Angular App, his code had two dependencies, and want to test his code, and my answer was to Spy and Mock the dependencies.

When uses Mock or Spy? Both are confusing concepts, similar, but each has a scenario and use case.

The best way to explain is with a basic scenario explaining each use case.

Scenario

I have the Invoice class, with two dependencies, used to perform actions to get the total and processInvoice.

export class Invoice {
  id: number;
  processed: boolean = false;

  constructor(private tax: TaxCalculation, private exportInvoice: ExportInvoiceLibrary) {
    this.id = Math.floor(Math.random() * 1000000);
  }

  public total(value: number): number {
    return value * this.tax.getTaxRate()
  }

  public processInvoice(): boolean {
    this.exportInvoice.sendToGovernment(this.id)
    this.processed = true;
    return this.processed;
  }

}
Enter fullscreen mode Exit fullscreen mode

The tax calculation class provides the tax rate.

export class TaxCalculation {
  rate = 2;

  getTaxRate() {
    return this.rate;
  }
}
Enter fullscreen mode Exit fullscreen mode

ExportInvoiceLibrary is a library or code, and we don't want o know what he does behind it.

export class ExportInvoiceLibrary {
  sendToGovernment(invoiceId: number): boolean {
    console.log(invoiceId);
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

We can test the code by providing the actual instance ExportInvoiceLibrary and taxCalculation.

import {ExportInvoiceLibrary, Invoice, TaxCalculation} from './invoice';

describe('invoice process', () => {
  let invoice: Invoice;
  let taxCalculation: TaxCalculation;
  let exportInvoiceLibrary: ExportInvoiceLibrary;

  beforeEach(() => {
    exportInvoiceLibrary = new ExportInvoiceLibrary();
    taxCalculation = new TaxCalculation();
    invoice = new Invoice(taxCalculation, exportInvoiceLibrary);
  });

  it('should create an invoice', () => {
    expect(invoice).toBeTruthy();
  })

  it('should get total with tax calculation', () => {
    const total = invoice.total(2);
    expect(total).toEqual(4);
  })

  it('should process the invoice', () => {
    let result = invoice.processInvoice();
    expect(result).toBeTruthy();
  })

});
Enter fullscreen mode Exit fullscreen mode

The test works, but something makes noise.

  • We are calling the current instance of both dependencies. What happens if each one makes an HTTP request or does a complex process?

  • If the taxCalculation number changes, it breaks my code.

  • The exportInvoiceLibrary is out of my control, and I don't care how and what he does. Sure it is called with the expected parameters.

The Mock and Spy come to solve each scenario.

Mock

In Jasmine, a "mock" is a simulated object used to simulate the behavior of a dependency. For example, I want to affect the behavior of the taxCalculation and expect my application to interact with the service as expected.

We create a mock of taxCalculation and configure the mock to return a specific value when calling the getTaxRate function. The mock allows us to control the behavior and ensure that our code appropriately uses the result of the service function.

Let's do it:

import {ExportInvoiceLibrary, Invoice, TaxCalculation} from './invoice';
import createSpyObj = jasmine.createSpyObj;
import SpyObj = jasmine.SpyObj;

describe('invoice process', () => {
  let invoice: Invoice;
  //Using SpyObj, define the type of TaxCalculation 
  let mockTaxCalculation: SpyObj<TaxCalculation>;
  let exportInvoiceLibrary: ExportInvoiceLibrary;
  //declare a value for TAX_CALCULATION_RATE;
  const TAX_CALCULTATION_RATE = 2;

  //Change the behavior OF GetTaxRate to return the TAX_CALCULATION_rATE
  mockTaxCalculation = createSpyObj<TaxCalculation>(['getTaxRate'])
  mockTaxCalculation.getTaxRate.and.returnValue(TAX_CALCULTATION_RATE);

  beforeEach(() => {
    exportInvoiceLibrary = new ExportInvoiceLibrary();
     //inject the mockTaxCalculation
    invoice = new Invoice(mockTaxCalculation, exportInvoiceLibrary);
  });


  it('should get total with tax calculation', () => {
      //test expects to interact with the internal getTaxRate mock and return the static value.
    const total = invoice.total(9);
    expect(total).toEqual(TAX_CALCULTATION_RATE * 9);
  })
}
Enter fullscreen mode Exit fullscreen mode

We mock the method and change the behavior. Next, we play with the spy.

Spy

A "spy", on the other hand, is a particular type of mock for monitoring the behavior of a function or class. For example, the processInvoice call the function sendToGovernment. We can observe how to call the sendToGovernment method, and the arguments pass to it.

It is useful when we want to ensure that a function is being called correctly in your code, but you don't need to control the result.

Unlike a regular mock service, a completely fake object, a spy service delegates call to the real object and record information about those calls, such as the arguments passed and the values returned.

Here is an example of how to use a spy with exportInvoiceLibrary :

 it('should process the invoice', () => {
    spyOn(exportInvoiceLibrary, 'sendToGovernment');
    let result = invoice.processInvoice();
    expect(exportInvoiceLibrary.sendToGovernment).toHaveBeenCalledTimes(1)
    expect(result).toBeTruthy();
  })
Enter fullscreen mode Exit fullscreen mode

In this example, we use the spyOn function to create a spy for the exportInvoiceLibrary and spy the method called sendToGovernment, which we have not configured to do anything in particular.

Finally, we use the toHaveBeenCalled matchers to verify that the spy sendToGovernment was called with the expected arguments and returned the expected value.

Summary

We learn the differences between Mock and Spy with a real scenario. Remember, the mock simulates dependency behavior and controls the result returned when calling the dependency.

In contrast, the spy help to monitor the behavior and verify that call is correct.

Photo by Lee Jiyong on Unsplash

Top comments (0)