Dany Paredes
Danywalls | Angular ♥ Web

Follow

Danywalls | Angular ♥ Web

Follow
The Role of Mocks and Spies in Unit Testing

Photo by Lee Jiyong on Unsplash

The Role of Mocks and Spies in Unit Testing

Choosing the Right Testing Strategy with Jasmine

Dany Paredes's photo
Dany Paredes
·Dec 30, 2022·

4 min read

Play this article

Table of contents

  • Scenario
  • Testing
  • Mock
  • Spy
  • Summary

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

When to use a Mock or a 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;
  }

}

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.

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.

Did you find this article valuable?

Support Dany Paredes by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this