How and When to Use Mocks or Spies on Unit Testing

When Use Mock and Spy with Jasmine

A few days ago, my friend was writing tests for an Angular App with two dependencies and wanted to test his code. I suggested that he should Spy and Mock the dependencies.

When deciding whether to use a Mock or a Spy, it is important to consider the context, as both have similar yet distinct scenarios and use cases.

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;
  }

}

The tax calculation class provides the tax rate.

export class TaxCalculation {
  rate = 2;

  getTaxRate() {
    return this.rate;
  }
}

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;
  }
}

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();
  })

});

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.

WAIT JUST A MOMENT!

I understand that testing in Angular can be intimidating for many developers, myself included. Would you like to learn about Angular testing, from the basics to the advanced aspects, and discover which elements are crucial for real-world situations?

Take a look at "Conscious Angular Testing" by Decoded Frontend – now with a special discount!

The course clearly explains things like Testbed, setting up tests, lifecycle hooks, and adding dependencies. It teaches you how to write non-fragile tests, deal with standalone components, content projection, inject testing, services, components with dependencies and more.
After this, I am no longer scared of writing tests in Angular.

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);
  })
}

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();
  })

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.