Unit Testing of functions with method decorator with mocha and sinon

Viewed 926

I have a class method like this:

export class MyClass {

        @myDecorator()
        public async createItem(itemId: string, itemOptions: ItemOption[]): Promise<boolean> {

                // ...
                // return await create Item
        }
}

I defined the decorator in another file like this:

export function myDecorator() {
        return function check(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
                const newDescriptor = Object.assign({}, descriptor);
                // do something ...
                return newDescriptor;
        }
}

Now I want to unit test the createItem method not the decorator, only the method. But as the decorator is kind of heavy in what it does (there is even a database access in it) I need to mock it so that it doesn't get executed in the unit test.

I'm using mocha and sinon in my unit tests and already tried the following to mock the decorator:

const decorator = require('./decorator/MyDecorator');
describe('test createItem method', function () {

        it('should return valid result', async function () {
                // 1. try
                decorator.myDecorator = () => sinon.fake();
                // 2. try
                const decoratorStub = sinon.stub(decorator, 'myDecorator');
                decoratorStub.return(true);

                // rest of test 
                const myClass = new MyClass();
                let result = await myClass.createItem(itemId, options);
                expect(result).to.be.equal(true);
        }
}

Both of the tries were not successfull. the decorator was executed anyways but I actually just want to skip it.

How can I achieve this?

2 Answers

I answer 1 hour too late. X)

But I have alternative solution using another package: rewire.

I add console.log to the decorator to show that the fake decorator run.

// File: decorator.ts
export function myDecorator() {
  return function check(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const newDescriptor = Object.assign({}, descriptor);
    const method = newDescriptor.value;
    newDescriptor.value = () => {
      console.log('Real Decorator Run');
      return method.apply(this);
    }
    return newDescriptor;
  }
}

This is simplified MyClass.

// File: MyClass.ts
import { myDecorator } from './decorator';

export class MyClass {
  @myDecorator()
  public async createItem(): Promise<boolean> {
    console.log('createItem Called');
    return true;
  }
}

This is test file.

// File: test.ts
import sinon from 'sinon';
import { expect } from 'chai';
import rewire from 'rewire';

import * as decorator from './decorator';

describe('test createItem method', function () {
  it('should return valid result', async function () {
    // 1. Stub myDecorator.
    const decoratorStub = sinon.stub(decorator, 'myDecorator');
    decoratorStub.returns(function check(a, b, c: PropertyDescriptor) {
      const method = c.value;
      c.value = () => {
        console.log('Fake Decorator');
        return method.apply(this);
      }
      return c;
    });

    // 2. Rewire MyClass.
    const { MyClass } = rewire('./MyClass');

    // 3. Test MyClass.
    const myClass = new MyClass();
    const result = await myClass.createItem();
    expect(result).to.be.equal(true);
  });
});

When I run using terminal:

$ npx nyc ts-mocha test.ts


  test createItem method
Fake Decorator
createItem Called
    ✓ should return valid result (146ms)


  1 passing (149ms)

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |   52.94 |      100 |      40 |      50 |                   
 MyClass.ts   |     100 |      100 |     100 |     100 |                   
 decorator.ts |   11.11 |      100 |       0 |    12.5 | 3-10              
--------------|---------|----------|---------|---------|-------------------

You need to use an additional package proxyquire to stub a standalone function imported from a package. We can use this package to stub myDecorator factory function.

E.g.

index.ts:

import { myDecorator } from './decorator/MyDecorator';

interface ItemOption {}

export class MyClass {
  @myDecorator()
  public async createItem(itemId: string, itemOptions: ItemOption[]): Promise<boolean> {
    return true;
  }
}

./decorator/MyDecorator.ts:

export function myDecorator() {
  return function check(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const newDescriptor = Object.assign({}, descriptor);
    console.log('real implementation, do heavy and side-effect work');
    return newDescriptor;
  };
}

index.test.ts:

import proxyquire from 'proxyquire';
import sinon from 'sinon';

describe('67413500', () => {
  it('should pass', async () => {
    const myDecoratorStub = sinon.stub().callsFake(() => {
      return function check(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const newDescriptor = Object.assign({}, descriptor);
        console.log('fake implementation, skip heavy and side-effect work');
        return newDescriptor;
      };
    });
    const { MyClass } = proxyquire('./', {
      './decorator/MyDecorator': {
        myDecorator: myDecoratorStub,
      },
    });
    const cls = new MyClass();
    const actual = await cls.createItem();
    sinon.assert.match(actual, true);
  });
});

unit test result:

  67413500
fake implementation, skip heavy and side-effect work
    ✓ should pass (1637ms)


  1 passing (2s)

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |   55.56 |      100 |   33.33 |   55.56 |                   
 67413500           |     100 |      100 |     100 |     100 |                   
  index.ts          |     100 |      100 |     100 |     100 |                   
 67413500/decorator |      20 |      100 |       0 |      20 |                   
  MyDecorator.ts    |      20 |      100 |       0 |      20 | 2-5               
--------------------|---------|----------|---------|---------|-------------------
Related