I am using NestJS with a serverless app (deployed to AWS Lambda). I now have a need to use middleware, or Interceptors as they are called in nest, but I'm struggling to get them to work. I have changed from using NestFactory.createApplicationContext to NestFactory.create, as per the docs, that's what wraps Controller methods with enhancers, e.g. Interceptors
I am registering the Interceptor in a module, so it should be globally available
const loggingInterceptorProvider = {
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
};
My bootstrap looks like so
export async function bootstrap(Module: any) {
if (app) return app;
app = await NestFactory.createApplicationContext(Module);
return await app.init();
}
Now the non-standard bit, because I am using a generic "builder" (library code), the builder is passed the controller name as a string, and it is then invoked, as such
// the Module is accessible in the bootstrap via a closure, not shown in this code
const app = await bootstrap();
const appController = app.get(Controller);
// functionName is a string
const controllerFunction = appController[functionName];
const boundControllerFunction = controllerFunction.bind(
appController,
);
const result = await boundControllerFunction(body);
I am not seeing any of my Interceptor logging output. Am I doing something wrong? Or is it the way I am invoking the Controller that is not working with Interceptors?
EDIT:
For completeness, this is the correct bootstrap function I use
let cachedApp: INestApplication;
export async function bootstrap(Module: any) {
if (cachedApp) return cachedApp;
cachedApp = await NestFactory.create(Module, {
bufferLogs: true,
logger: ['error', 'warn'],
});
await cachedApp.init();
return cachedApp;
}
It happens because you've called the controller method directly, bypassing the nestjs lifecycle. When nest js server handles the request it applies its internal mechanisms for running interceptors, validation pipes, and exception filters. If you call class method directly it will not be used.
In your case you can follow this section of nestjs documentation:
https://docs.nestjs.com/faq/serverless#example-integration
let server: Handler;
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
await app.init();
const expressApp = app.getHttpAdapter().getInstance();
return serverlessExpress({ app: expressApp });
}
export const handler: Handler = async (
event: any,
context: Context,
callback: Callback,
) => {
server = server ?? (await bootstrap());
return server(event, context, callback);
};
The "standalone application feature" from docs is useful if you want to call some service code, not a controller.
By the way, in the code snippet, you can see the variable server, they moved it outside of a handler function intentionally. Because in AWS lambdas it can be cached between different requests.
I found a/the way to do it, using the very poorly documented feature ExternalContextCreator. So basically the last code snippet I posted above, would become this
import { ExternalContextCreator } from '#nestjs/core/helpers/external-context-creator';
// the Module is accessible in the bootstrap via a closure, not shown in this code
const app = await bootstrap();
const appController = app.get(Controller);
// functionName is a string
const controllerFunction = appController[functionName];
const extContextCreator = app.get(ExternalContextCreator);
const boundControllerFunction = extContextCreator.create(
appController,
controllerFunction,
String(functionName),
);
const result = await boundControllerFunction(body);
Related
I am a beginner in writing tests and in jest also. I want to test that this function to be called and return the success promise. I write a unit test for function using jest. The function gets like parameter instance of class that I can't create because its constructor requires parameters that I haven't access. I have a lot of functions that have Session like parametrs. How can test function when you cant provide parametrs for it? Can I mock instance of class or function and handle it without parameter?
async initFlow(session: Session) {
const nextAtomId = session.userInput.getParam('NEXT_ATOM');
if (nextAtomId) {
const nextAtom = await AtomManager.findActiveAtom(nextAtomId);
if (!session.features.useTerms || ['beforeTerms', 'TermsAndConditions'].includes(nextAtom.type)) {
return AtomProcessor.processAtom(session, nextAtom);
}
}
const start = await AtomManager.getStartAtom(`${session.botId}`);
if (!start) {
throw new Error('Could not find start atom');
}
session.user = await UserManager.getGlobalUser(session); // getGlobalUser makes initUser under the hood.
return AtomProcessor.processAtom(session, start);
}
You can mock both AtomManager & UserManager and provide a mock session object when calling initFlow.
jest.mock("./path/to/AtomManager");
jest.mock("./path/to/UserManager");
it("works", async () => {
const mockSession = {
userInput: {
getParam: jest.fn(),
},
botId: "123",
};
const mockUser = "user123";
const mockStartAtom = "atom123";
AtomManager.getStartAtom.mockResolveValue(mockStartAtom);
UserManager.getGlobalUser.mockResolveValue(mockUser);
await initFlow(mockSession);
expect(mockSession.user).toBe(mockUser);
expect(AtomManager.getStartAtom).toHaveBeenCalledTimes(1);
expect(AtomManager.getStartAtom).toHaveBeenCalledWith(mockSession.botId);
expect(UserManager.getGlobalUser).toHaveBeenCalledTimes(1);
expect(UserManager.getGlobalUser).toHaveBeenCalledWith(mockSession);
expect(AtomProcessor.processAtom).toHaveBeenCalledTimes(1);
expect(AtomProcessor.processAtom).toHaveBeenCalledWith(mockSession, mockStartAtom);
});
The snippet above makes the following assertions:
AtomManager.getStartAtom is called once and it's called with the mock botId.
UserManager.getGlobalUser is called once and it's called with the mock session object.
UserManager.getGlobalUser has successfully added the user property on the passed session object.
AtomProcessor.processAtom is called once and it's called with the mock session and the mock start atom.
You can similarly the test other branches of code.
I am writing a typeScript program which hits an external API. In the process of writing tests for this program, I have been unable to correctly mock-out the dependency on the external API in a way that allows me to inspect the values passed to the API itself.
A simplified version of my code that hits the API is as follows:
const api = require("api-name")();
export class DataManager {
setup_api = async () => {
const email = "email#website.ext";
const password = "password";
try {
return api.login(email, password);
} catch (err) {
throw new Error("Failure to log in: " + err);
}
};
My test logic is as follows:
jest.mock("api-name", () => () => {
return {
login: jest.fn().mockImplementation(() => {
return "200 - OK. Log in successful.";
}),
};
});
import { DataManager } from "../../core/dataManager";
const api = require("api-name")();
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("200 - OK. Log in successful.");
expect(api.login).toHaveBeenCalledTimes(1);
});
});
What I find perplexing is that the test assertion which fails is only expect(api.login).toHaveBeenCalledTimes(1). Which means the API is being mocked, but I don't have access to the original mock. I think this is because the opening line of my test logic is replacing login with a NEW jest.fn() when called. Whether or not that's true, I don't know how to prevent it or to get access to the mock function-which I want to do because I am more concerned with the function being called with the correct values than it returning something specific.
I think my difficulty in mocking this library has to do with the way it's imported: const api = require("api-name")(); where I have to include an opening and closing parenthesis after the require statement. But I don't entirely know what that means, or what the implications of it are re:testing.
I came across an answer in this issue thread for ts-jest. Apparently, ts-jest does NOT "hoist" variables which follow the naming pattern mock*, as regular jest does. As a result, when you try to instantiate a named mock variable before using the factory parameter for jest.mock(), you get an error that you cannot access the mock variable before initialization.
Per the previously mentioned thread, the jest.doMock() method works in the same way as jest.mock(), save for the fact that it is not "hoisted" to the top of the file. Thus, you can create variables prior to mocking out the library.
Thus, a working solution is as follows:
const mockLogin = jest.fn().mockImplementation(() => {
return "Mock Login Method Called";
});
jest.doMock("api-name", () => () => {
return {
login: mockLogin,
};
});
import { DataManager } from "../../core/dataManager";
describe("DataManager.setup_api", () => {
it("should login to API with correct parameters", async () => {
//Arrange
let manager: DataManager = new DataManager();
//Act
const result = await manager.setup_api();
//Assert
expect(result).toEqual("Mock Login Method Called");
expect(mockLogin).toHaveBeenCalledWith("email#website.ext", "password");
});
});
Again, this is really only relevant when using ts-jest, as using babel to transform your jest typescript tests WILL support the correct hoisting behavior. This is subject to change in the future, with updates to ts-jest, but the jest.doMock() workaround seems good enough for the time being.
I'm testing an AuthenticationController using supertest. To do so, I am mocking my application using the same configuration than the one I use in my main file main.ts:
// authentication.controller.ts
describe("The AuthenticationController", () => {
let app: INestApplication;
beforeEach(async () => {
userData = {
...mockedUser,
};
const userRepository = {
create: jest.fn().mockResolvedValue(userData),
save: jest.fn().mockReturnValue(Promise.resolve()),
};
const module = await Test.createTestingModule({
controllers: [...],
providers: [...],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.init();
});
});
This mostly works, but whenever I am testing a controller that should not return a password or an id for example - because of the #Exclude() decorator in an entity definition - the test still returns it to me.
Testing the endpoint manually on Postman still works well.
Does anyone know what could cause that issue?
I just got an answer from one of the developers of NestJS on their official Discord: https://discord.com/invite/nestjs
It turns out the error came from the fact that when mocking the return value of create in my userRepository, I was actually returning an object instead of an instance of a class. Therefore, the following lines had to be replaced:
const userRepository = {
create: jest.fn().mockResolvedValue(userData),
save: jest.fn().mockReturnValue(Promise.resolve()),
};
By the following:
const userRepository = {
create: jest.fn().mockResolvedValue(new User(userData)),
save: jest.fn().mockReturnValue(Promise.resolve()),
};
By simply returning an object, the decorators are not taken into account, so a class instance must be returned.
I am testing an Interceptor that requires one service as a dependency. I need to test that a method from this service has been called. The code below is working but since my dependency is private I have to call it like this: service = interceptor['filtersService'];. Typescript doesn't like this and outputs a warning:
object access via string literals is disallowed
Is there another way to spy on a dependency?
describe('CreateClientFilterInterceptor', () => {
const FiltersServiceMock = jest.fn<Partial<FiltersService>, []>(() => ({
async create() {
return (await SubscriberFilterMock) as Filter;
},
}));
let interceptor: CreateClientFilterInterceptor;
let service;
beforeAll(async () => {
interceptor = new CreateClientFilterInterceptor(
new FiltersServiceMock() as FiltersService,
);
service = interceptor['filtersService'];
});
it('should call create method from Filter service', async done => {
spyOn(service, 'create').and.stub();
(await interceptor.intercept(
executionContext as ExecutionContext,
callHandler,
)).subscribe(() => {
expect(service.create).toHaveBeenCalled();
done();
});
});
});
Workaround
I did this workaround to get rid of the warning but it doesn't seem right to me.
const serviceName = 'filtersService';
const service = interceptor[serviceName];
Since you are creating the service yourself, you can first create an instance, save it in your service variable and then create the interceptor with it:
service = new FiltersServiceMock() as FiltersService;
interceptor = new CreateClientFilterInterceptor(service);
I don't know if I'm missing something in the docs, but I have this situation:
// test.js
import User from './user'
it("should load initial data", async() => {
const users = new User()
const user = await users.load()
})
// User.js
import Api from './api'
export default class User {
async load() {
const res = await Api.fetch() // prevent/mock this in testing
}
}
What is the Jest-way to prevent/mock the external Api module in User.js. I do not want User.js to make a real network request within the test.
Further to this, I'm looking for a more generic mocking solution, ie. say I'm testing in React Native, and I want to mock NativeModules.SettingsManager.settings.AppleLocale, for example. Lets say Api.fetch() calls the line above, and doesn't make a HTTP request
spyOn in combination with mock functions like mockImplementation will provide what you are looking for.
Here is a working example:
// ---- api.js ----
export const getData = () => {
return Promise.resolve('hi');
}
// ---- user.js ----
import { getData } from './api'
export default class User {
async load() {
return await getData(); // mock this call in user.test.js
}
}
// ---- user.test.js ----
import User from './user'
import * as Api from './api'; // import * so we can mock 'getData' on Api object
describe('User', () => {
it('should load initial data', async() => {
const mock = jest.spyOn(Api, 'getData'); // create a spy
mock.mockImplementation(() => Promise.resolve('hello')); // give it a mock implementation
const user = new User();
const result = await user.load();
expect(result).toBe('hello'); // SUCCESS, mock implementation called
mock.mockRestore(); // restore original implementation when we are done
});
});
If you need to mock responses to HTTP requests, then you should check out nock. It has a clean API that allows a lot of flexibility in creating HTTP responses to specific requests.