Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to unit test controller #1209

Closed
mukeshrawat02 opened this issue Oct 18, 2018 · 4 comments
Closed

How to unit test controller #1209

mukeshrawat02 opened this issue Oct 18, 2018 · 4 comments

Comments

@mukeshrawat02
Copy link

@mukeshrawat02 mukeshrawat02 commented Oct 18, 2018

I am getting issues while unit testing my controller. For maximum coverage I wanted to unit test controller and respective services and would like to mock external dependencies like mongoose connection etc.
I am getting error "Nest can't resolve dependencies of my service". I already tried suggestions mentioned by @adrien2p below, but didn't get any luck:
#194 (comment)

Please find my code below:

export const deviceProviders = [
    {
        provide: 'devices',
        useFactory: (connection: Connection) => connection.model('devices', DeviceSchema),
        inject: ['DbConnectionToken'],
    },
];
export class DeviceService extends BaseService {
    constructor(@InjectModel('devices') private readonly _deviceModel: Model<Device>) {
        super();
    }

    async getDevices(group): Promise<any> {
        try {
            return await this._deviceModel.find({ Group: group }).exec();
        } catch (error) {
            return Promise.reject(error);
        }
    }
}
@Controller()
export class DeviceController {
    constructor(private readonly deviceService: DeviceService) {
    }

   @Get(':group')
   async getDevices(@Res() response, @Param('group') group): Promise<any> {
        try {
            const result = await this.deviceService.getDevices(group);
            return response.send(result);
        }
        catch (err) {
            return response.status(422).send(err);
        }
    }
}
@Module({
    imports: [MongooseModule.forFeature([{ name: 'devices', schema: DeviceSchema }])],
    controllers: [DeviceController],
    components: [DeviceService, ...deviceProviders],
})
export class DeviceModule { }
describe('DeviceController', () => {
    let deviceController: DeviceController;
    let deviceService: DeviceService;

    const response = {
        send: (body?: any) => { },
        status: (code: number) => response,
    };

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            controllers: [DeviceController],
            components: [DeviceService, ...deviceProviders],
        }).compile();

        deviceService = module.get<DeviceService>(DeviceService);
        deviceController = module.get<DeviceController>(DeviceController);
    });

    describe('getDevices()', () => {
        it('should return an array of devices', async () => {
            const result = [{
                Group: 'group_abc',
                DeviceId: 'device_abc',
            },
            {
                Group: 'group_xyz',
                DeviceId: 'device_xyz',
            }];
            jest.spyOn(deviceService, 'getDevices').mockImplementation(() => result);

            expect(await deviceController.getDevices(response, null)).toBe(result);
        });
    });
});

When I am running my code above, I am getting two errors:

Nest can't resolve dependencies of the DeviceService (?). Please make sure that the argument at index [0] is available in the current context.

Cannot spyOn on a primitive value; undefined given

@andrew-yustyk

This comment has been minimized.

Copy link
Contributor

@andrew-yustyk andrew-yustyk commented Oct 19, 2018

The simplest way is to mock DeviceService when you want to test DeviceController.
Try replace real DeviceService with your mock implementation when you compile testing module.
Something like this:

 // create mock class or you can use some package like sinon for this
class DeviceServiceMock extends BaseService {
    async getDevices(group): Promise<any> {
        return []; 
    }
}

describe('DeviceController', () => {
    let deviceController: DeviceController;
    let deviceService: DeviceService;

    const response = {
        send: (body?: any) => { },
        status: (code: number) => response,
    };

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            controllers: [DeviceController],
            components: [{ 
              provide: DeviceService,
              useValue: new DeviceServiceMock() // or alternatively useClass: DeviceServiceMock
            }],
        }).compile();

        deviceService = module.get<DeviceService>(DeviceService);
        deviceController = module.get<DeviceController>(DeviceController);
    });

    describe('getDevices()', () => {
        it('should return an array of devices', async () => {
            const result = [{
                Group: 'group_abc',
                DeviceId: 'device_abc',
            },
            {
                Group: 'group_xyz',
                DeviceId: 'device_xyz',
            }];
            jest.spyOn(deviceService, 'getDevices').mockImplementation(() => result);

            expect(await deviceController.getDevices(response, null)).toBe(result);
        });
    });
});
@andrew-yustyk

This comment has been minimized.

Copy link
Contributor

@andrew-yustyk andrew-yustyk commented Oct 19, 2018

Small notes

async getDevices(group): Promise<any> {
        try {
            return await this._deviceModel.find({ Group: group }).exec();
        } catch (error) {
            return Promise.reject(error);
        }
    }

you shouldn't use return await ... in async functions, because async function will return Promise in any case, you can just return already existing one instead of value. The result will be same in both cases:

return await this._deviceModel.find({ Group: group }).exec();
return this._deviceModel.find({ Group: group }).exec();

As for the your question, it's very hard to load all dependencies tree in the tests almost in all cases. Also when you load all dependencies, you need to be sure that all of them works without any errors. Because when your test fails, you need to know where something went wrong - in the tested class or in hes dependency.
That's why it is better to replace all dependencies for tested class class with mock implementations.
In some cases you might need to keep the real dependencies for tested class, but I don't know why may be required to keep dependencies of dependencies :)
E.g. in your case, you can keep DeviceService if it's required, but you should mock 'devices' anyway:

// there must be DeviceModelMock implementation

const deviceProvidersMock = [
    {
        provide: 'devices',
        useValue: DeviceModelMock,
    },
];

beforeEach(async () => {
        const module = await Test.createTestingModule({
            controllers: [DeviceController],
            components: [DeviceService, ...deviceProvidersMock],
        }).compile()
    });
@kamilmysliwiec

This comment has been minimized.

Copy link
Member

@kamilmysliwiec kamilmysliwiec commented Oct 19, 2018

Thanks @andrew-yustyk for a huge explanation.

@mukeshrawat02, additionally, I would not recommend using @Res() directly, it makes testing part tougher. Also, please use StackOverflow for such questions next time

@lock

This comment has been minimized.

Copy link

@lock lock bot commented Sep 24, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Sep 24, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
3 participants
You can’t perform that action at this time.