Iryna Yershova
Iryna Yershova

Reputation: 141

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

    ...
  }

Upvotes: 14

Views: 2744

Answers (1)

Antoine Viscardi
Antoine Viscardi

Reputation: 890

I am not a Jasmine expert, but I believe you have to keep references to your spies in variables and run your 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();
  });
});

You can also use async/await, for a cleaner style.

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

Upvotes: 1

Related Questions