Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Real method is called instead of SpyOn method in Nest JS unit test

I am implementing unit test in the framework NestJS using Jasmin. And I have a problem with testing one of my service. After long time of debugging I have no idea what's wrong with my code. It looks like my service does't use Spy function and uses real method.

I'm using these versions of packages:

package.json

"jasmine": "3.1.0"
"@nestjs/testing": "4.6.6"
"@nestjs/common": "4.6.6"
"typescript": "2.6.2"
"node": "8.8.1"

I had a problem during testing one method - prepareOrderDataOnCreate. In this method I invoke 2 methods from other services.

order.service.ts

@Component()
export class OrdersServiceComponent {
  constructor(
    private userService: UsersServiceComponent,
    private orderStatusService: OrderStatusServiceComponent,
    ...) {}

  async prepareOrderDataOnCreate(user): Promise<Order> {
    const status = await this.orderStatusService.getStatus();
    const user = await this.userService.getOne(user.id);

    return {
      ...
    };
  }
}

I mocked both services and created SpyOn for both methods to check if they toHaveBeenCalled. But when I run test get an error:

Error: : Expected a spy, but got Function.

After debugging I found out that unit test uses usersService.getOne not from SpyOn, but from mock file (see below user.service.mock.ts), despite the fact that orderStatusService.getStatus is used from SpyOn.

order.service.spec.ts

describe('OrdersServiceComponent', () => {
  let ordersService: OrdersServiceComponent;
  let usersService: UsersServiceMock;

  beforeEach(async() => {
    const module = await Test.createTestingModule({
      controllers: [OrdersController],
      components: [
        OrdersServiceComponent,
        {provide: UsersServiceComponent, useClass: UsersServiceMock},
        {provide: OrderStatusServiceComponent, useClass: OrderStatusServiceMock},
        ...
      ]
    }).compile();

    ordersService = module.get<OrdersServiceComponent>(OrdersServiceComponent);
    orderStatusService = module.get<OrderStatusServiceMock>(OrderStatusServiceComponent as any);
    usersService = module.get<UsersServiceMock>(UsersServiceComponent as any);
  });

  it('prepareOrderDataOnCreate method returns order on success', () => {
    spyOn(orderStatusService, 'getStatus').and.stub();
    spyOn(usersService, 'getOne').and.callFake(() => console.log('SPY'));

    ordersService.prepareOrderDataOnCreate({...}).then(res => {
      expect(usersService.getOne).toHaveBeenCalled();
      expect(orderStatusService.getStatus).toHaveBeenCalled();
    });
  });
});

Instead of Spy unit test uses this mock method:

user.service.mock.ts

@Component()
export class UsersServiceMock {
  async getOne(): Promise<any> {
    return Promise.resolve({});
  }
}

It's really strange, because in case when I invoked usersService.getOne() in this unit test before prepareOrderDataOnCreate() I got console.log from callFake.

UPDATE

I found out that when I run my test and invoke service method prepareOrderDataOnCreate, at first userService.getOne is Spy function and after any code inside the method userService.getOne is changed to real function.

async prepareOrderDataOnCreate(user): Promise<Order> {
    /*  this.userService.getOne is SPY FUNCTION */

    const status = await this.orderStatusService.getStatus();

    /*  this.userService.getOne is REAL MOCK FUNCTION */

    const user = await this.userService.getOne(user.id);

    ...
  }
like image 561
Iryna Yershova Avatar asked Aug 14 '18 13:08

Iryna Yershova


1 Answers

I am not a Jasmine expert, but I believe you have to keep references to your spies in variables and run on assertions on those spy objects. Bellow is what it could look like.

it('prepareOrderDataOnCreate method returns order on success', () => {
  const getStatusSpy = spyOn(orderStatusService, 'getStatus').and.stub();
  const getOneSpy = spyOn(usersService, 'getOne').and.callFake(() => console.log('SPY'));

  ordersService.prepareOrderDataOnCreate({...}).then(res => {
    expect(getOneSpy).toHaveBeenCalled();
    expect(getStatusSpy).toHaveBeenCalled();
  });
});

On another note, I would also encourage you to use async/await to make the code cleaner.

it('prepareOrderDataOnCreate method returns order on success', async () => {
  const getStatusSpy = spyOn(orderStatusService, 'getStatus').and.stub();
  const getOneSpy = spyOn(usersService, 'getOne').and.callFake(() => console.log('SPY'));

  await ordersService.prepareOrderDataOnCreate({...})

  expect(getOneSpy).toHaveBeenCalled();
  expect(getStatusSpy).toHaveBeenCalled();
  
});
like image 108
Antoine Viscardi Avatar answered Dec 31 '22 10:12

Antoine Viscardi