I'm trying to test microservice and his client controller
// client
@Controller()
export class EnvironmentRpcClientController {
@Client({ transport: Transport.TCP, port: ENVIRONMENT_APP_CONFIG.rpcPort })
private client: ClientProxy;
public get<T extends Object>(moduleName?: string): Observable<T> {
const pattern = ENVIRONMENT_RPC_CONFIG.actions.get;
const data = moduleName;
return this.client.send<T>(pattern, data);
}
}
test (EnvironmentAppModule
is main microservice module with RPC server)
import { Test } from '@nestjs/testing';
import { NestFactory } from '@nestjs/core';
import 'rxjs/add/operator/finally';
import { ENVIRONMENT_APP_CONFIG } from '../src/config';
import { EnvironmentAppModule } from '../src/module';
import { EnvironmentRpcClientController } from '../src/rpc';
describe('environment', () => {
let serverApp: any;
let environmentRpcClientController: EnvironmentRpcClientController;
beforeEach(done => {
serverApp = NestFactory.createMicroservice(EnvironmentAppModule, { port: ENVIRONMENT_APP_CONFIG.rpcPort });
serverApp.listen(() => {
(<any> serverApp).logger.log(`TEST RPC server is listening on port ${ENVIRONMENT_APP_CONFIG.rpcPort}`);
Test.createTestingModule({
controllers: [ EnvironmentRpcClientController ],
});
environmentRpcClientController = Test.get(EnvironmentRpcClientController);
done();
});
});
afterEach(done => {
serverApp.server.server.close(() => done());
});
it('should request envinroment data from service', done => {
environmentRpcClientController.get()
.finally(() => done())
.subscribe(
config => console.log('config', config)
);
});
});
and as result
TypeError: Cannot read property 'send' of null
this.client
in EnvironmentRpcClientController.get()
is null.
Also i'm forced to use private fields access hacks like serverApp.server.server.close()
to shutdown RPC server after each test and before new one, public method for server close will be cool.
Hi @artaommahe,
The module returned by Test.createTestingModule()
is not part of serverApp
application instance. It is a new one. There are two different module instances now. Possibility to test microservices clients is something definitely needed, I'll provide it asap.
However, the close()
method is available since 2.1.1
.
The module returned by Test.createTestingModule() is not part of serverApp application instance. It is a new one
i know ) i'm trying to call EnvironmentRpcClientController
's method that belongs to test module and does not have any other dependency (only get()
method to call rpc action). And this.client
is null here.
createTestingModule()
doesn't create clients and http / ws / rpc servers.
Hey @artaommahe,
Try to use something like this to init your test module :
import {INestApplication} from '@nestjs/common/interfaces/nest-application.interface';
import {NestFactory} from '@nestjs/core';
import {UserModule} from './user.module';
import * as supertest from 'supertest';
import * as superagent from 'superagent';
import {expect} from 'chai';
import {Application} from 'express';
import {ExpressAdapter} from '@nestjs/core/adapters/express-adapter';
describe('UserModule', () => {
let instance: Application;
let application: INestApplication;
beforeEach(() => {
instance = ExpressAdapter.create();
application = NestFactory.create(UserModule, instance);
application.init();
});
describe('#endpoint /users', () => {
it('should expose GET method', done => {
supertest(instance)
.get('/users')
.end((error, response: superagent.Response) => {
expect(response.status).to.not.be.equal(404);
done();
});
});
it('should expose POST method', done => {
supertest(instance)
.post('/users')
.send(JSON.stringify({ nickname: 'nickname' }))
.end((error, response: superagent.Response) => {
expect(response.status).to.not.be.equal(404);
done();
});
});
});
});
@ThomRick it works for express request, but i need to test via RPC endpoint :)
@artaommahe, it seems it's not an issue. We just don't have any tcp testing tool right now. I'll try to create one in the future
@artaommahe my bad but I'm thinking about this and you probably test wrong way...
In fact for the Client part you need to run it in a Controller that listen to HTTP request to send message to the Server part of the microservice.
For now the only way to test that you call the send method of the Client in your controller is to use supertest (or equivalent) to call the controller that will send a message.
I have a passing test code next :
Controller code :
import {Client, ClientProxy} from '@nestjs/microservices';
import {Controller, Get, Req, Res} from '@nestjs/common';
import {Transport} from '@nestjs/microservices/enums';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
@Controller()
export class EnvironmentRpcClientController {
@Client({ transport: Transport.TCP, port: 3000 })
private client: ClientProxy;
@Get()
public get<T extends Object>(@Query('moduleName') moduleName: string, @Res() response): void {
const pattern = { cmd: 'get' };
this.client.send<T>(pattern, moduleName)
.subscribe(result => response.json({ data: result }));
}
}
Test code :
import 'rxjs/add/operator/finally';
import {EnvironmentRpcClientController} from './environment-rcp-client.controller';
import * as sinon from 'sinon';
import {ClientTCP} from '@nestjs/microservices/client/client-tcp';
import {expect} from 'chai';
import {NestFactory} from '@nestjs/core';
import {Module} from '@nestjs/common';
import {INestApplication} from '@nestjs/common/interfaces/nest-application.interface';
import {ExpressAdapter} from '@nestjs/core/adapters/express-adapter';
import * as supertest from 'supertest';
import 'rxjs/add/observable/of';
import {Observable} from 'rxjs/Observable';
describe('environment', () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => sandbox = sinon.sandbox.create());
afterEach(() => sandbox.restore());
@Module({
controllers: [
EnvironmentRpcClientController
]
})
class TestingAppModule {}
let instance = ExpressAdapter.create();
let application: INestApplication;
beforeEach(() => {
application = NestFactory.create(TestingAppModule, instance);
application.init();
});
let sendStub;
beforeEach(() => {
sendStub = sandbox.stub(ClientTCP.prototype, 'send').callsFake(() => Observable.of('data') );
});
it('should request environment data from service', () => {
return supertest(instance)
.get('/?moduleName=moduleName')
.then(response => {
expect(sendStub.calledOnce).to.be.true;
expect(response.body).to.be.deep.equal({ data: 'data' });
});
});
});
@kamilmysliwiec I'll push the code on my testing mocha repository ;-)
@artaommahe my bad again !
I didn't understand what you want to do exactly ><
In fact I was thinking that you would have a HTTP endpoint to activate your microservice client but it was not the purpose...
I followed the @kamilmysliwiec documentation about the microservice client with the custom endpoint but it explains how to use client microservice through HTTP server not with RPC server
I don't know about RPC server but the thing is I don't how you can call your microservice client controller with an external event / message (like an http request for example)
As @kamilmysliwiec said it needs microservice test tool to create and get the controller instance...
But I found the way !
The clients just need to be setup in the Test.createTestingMethod() :
import { NestContainer, InstanceWrapper } from '@nestjs/core/injector/container';
import { ModuleMetadata } from '@nestjs/common/interfaces/modules/module-metadata.interface';
import { Module } from '@nestjs/common/utils/decorators/module.decorator';
import { DependenciesScanner } from '@nestjs/core/scanner';
import { InstanceLoader } from '@nestjs/core/injector/instance-loader';
import { Metatype } from '@nestjs/common/interfaces/metatype.interface';
import { Logger } from '@nestjs/common/services/logger.service';
import { NestEnvironment } from '@nestjs/common/enums/nest-environment.enum';
import {MicroservicesModule} from '@nestjs/microservices/microservices-module';
export class Test {
private static container = new NestContainer();
private static scanner = new DependenciesScanner(Test.container);
private static instanceLoader = new InstanceLoader(Test.container);
public static createTestingModule(metadata: ModuleMetadata) {
this.init();
const module = this.createModule(metadata);
this.scanner.scan(module);
this.instanceLoader.createInstancesOfDependencies();
// !!! ADD setup clients to create @Client() instances !!!
MicroservicesModule.setupClients(this.container);
}
public static get<T>(metatype: Metatype<T>): T {
const modules = this.container.getModules();
return this.findInstanceByPrototype<T>(metatype, modules);
}
private static init() {
Logger.setMode(NestEnvironment.TEST);
this.restart();
}
public static restart() {
this.container.clear();
}
private static findInstanceByPrototype<T>(metatype: Metatype<T>, modules) {
for (const [ _, module ] of modules) {
const dependencies = new Map([ ...module.components, ...module.routes ]);
const instanceWrapper = dependencies.get(metatype.name);
if (instanceWrapper) {
return (instanceWrapper as InstanceWrapper<any>).instance;
}
}
return null;
}
private static createModule(metadata) {
class TestModule {}
Module(metadata)(TestModule);
return TestModule;
}
}
That gives the following controller code :
import {Client, ClientProxy} from '@nestjs/microservices';
import {Controller} from '@nestjs/common';
import {Transport} from '@nestjs/microservices/enums';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import {Observable} from 'rxjs/Observable';
@Controller()
export class EnvironmentRpcClientController {
@Client({ transport: Transport.TCP, port: 3000 })
private client: ClientProxy;
public get<T extends Object>(moduleName?: string): Observable<T> {
console.log(this.client);
const pattern = { cmd: 'get' };
return this.client.send<T>(pattern, moduleName);
}
}
And the test :
import 'rxjs/add/operator/finally';
import {EnvironmentRpcClientController} from './environment-rcp-client.controller';
import * as sinon from 'sinon';
import {ClientTCP} from '@nestjs/microservices/client/client-tcp';
import {expect} from 'chai';
import 'rxjs/add/observable/of';
import {Observable} from 'rxjs/Observable';
import {Test} from './testing';
describe('environment', () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => sandbox = sinon.sandbox.create());
afterEach(() => sandbox.restore());
beforeEach(() => {
Test.createTestingModule({
controllers: [
EnvironmentRpcClientController
]
});
});
let controller: EnvironmentRpcClientController;
beforeEach(() => {
controller = Test.get(EnvironmentRpcClientController);
});
let sendStub;
beforeEach(() => {
sendStub = sandbox.stub(ClientTCP.prototype, 'send').callsFake(() => Observable.of('data') );
});
it('should request environment data from service', done => {
controller.get('test')
.subscribe(result => {
expect(sendStub.calledOnce).to.be.true;
expect(result).to.be.deep.equal('data');
done();
});
});
});
@kamilmysliwiec sorry for spamming but I think the Test.createTestingModule() should use the setupModules() method of the NestApplication() or equivalent. Maybe it should be promoted in an other public class because of the use of the nearly same way in NestMicroservice().
In NestApplication class :
public setupModules() {
SocketModule.setup(this.container, this.config);
MiddlewaresModule.setup(this.container);
MicroservicesModule.setupClients(this.container);
}
In NestMicroservice class :
public setupModules() {
SocketModule.setup(this.container, this.config);
MicroservicesModule.setupClients(this.container);
this.setupListeners();
this.isInitialized = true;
}
public setupListeners() {
MicroservicesModule.setupListeners(this.container, this.server);
}
In fact this method is called by init() or listen() and create all the instances (controller / component / middleware / micro service clients / socket)
In NestApplication class:
public init() {
this.setupModules();
...
}
...
public listen(port: number, callback?: () => void) {
(!this.isInitialized) && this.init();
...
}
In NestMicroservice class :
public listen(callback: () => void) {
(!this.isInitialized) && this.setupModules();
...
}
By the way it's a little be disturbing to have to call init() or listen() methods in a unit tests...
@ThomRick
I don't how you can call your microservice client controller with an external event / message (like an http request for example)
service without any public endpoint that has some tasks by interval, including RPC calls ) RPC client can be defined in service for this purpose.
Thx for your analysis, will wait for nest
update for this tests :)
Hey guys,
In the nearest release it'd be possible to override components from imported modules. It should help with testing those scenarios.
Hello,
More about this feature is here
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.
Most helpful comment
Hey guys,
In the nearest release it'd be possible to override components from imported modules. It should help with testing those scenarios.