Reputation:
I would like to log the incoming requests and outgoing responses for my API. I created a request interceptor and a response interceptor as described here
https://docs.nestjs.com/interceptors
So the request interceptor only logs the request object
@Injectable()
export class RequestInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(RequestInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { originalUrl, method, params, query, body } = context.switchToHttp().getRequest();
this.logger.debug({ originalUrl, method, params, query, body }, this.intercept.name);
return next.handle();
}
}
and the response interceptor waits for the outgoing response and logs the status code and response object later on
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(ResponseInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { statusCode } = context.switchToHttp().getResponse();
return next.handle().pipe(
tap((responseData: any) =>
this.logger.debug({ statusCode, responseData }, this.intercept.name),
),
);
}
}
I would like to test them but unfortunately have almost no experience in testing. I tried to start with the request interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn().mockReturnThis(),
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn(),
};
describe('RequestInterceptor', () => {
let interceptor: RequestInterceptor;
beforeEach(() => {
interceptor = new RequestInterceptor();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const requestInterception: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
requestInterception.subscribe({
next: value => {
// ... ??? ...
},
error: error => {
throw error;
},
complete: () => {
done();
},
});
});
});
});
I currently don't know what to pass into the next callback but when I try to run the test as it is it says that the requestInterception variable is undefined. So the test fails before reaching the next callback. So the error message I get is
TypeError: Cannot read property 'subscribe' of undefined
I also tried to test the response interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getResponse: jest.fn().mockReturnThis()
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn()
};
describe("ResponseInterceptor", () => {
let interceptor: ResponseInterceptor;
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
describe("intercept", () => {
it("should fetch the statuscode and response data", (done: any) => {
const responseInterception: Observable<any> = interceptor.intercept(
executionContext,
nextCallHander
);
responseInterception.subscribe({
next: value => {
// ...
},
error: error => {
throw error;
},
complete: () => {
done();
}
});
});
});
});
This time I get an error at the interceptor
TypeError: Cannot read property 'pipe' of undefined
Would some mind helping me to test those two interceptors properly?
Thanks in advance
Upvotes: 5
Views: 7853
Reputation: 70570
Testing interceptors can be one of the most challenging parts of testing a NestJS application because of the ExecutionContext
and returning the correct value from next
.
Let's start with the ExecutionContext
:
You've got an all right set up with your current context, the important thing is that you have a switchToHttp()
method if you are using HTTP (like you are) and that whatever is returned by switchToHttp()
has a getResponse()
or getRequest()
method (or both if both are used). From there, the getRequest()
or getResponse()
methods should return values that are used from the req and res, such as res.statusCode
or req.originalUrl
. I like having incoming and outgoing on the same interceptor, so often my context
objects will look something like this:
const context = {
switchToHttp: jest.fn(() => ({
getRequest: () => ({
originalUrl: '/',
method: 'GET',
params: undefined,
query: undefined,
body: undefined,
}),
getResponse: () => ({
statusCode: 200,
}),
})),
// method I needed recently so I figured I'd add it in
getType: jest.fn(() => 'http')
}
This just keeps the context light and easy to work with. Of course you can always replace the values with more complex ones as you need for logging purposes.
Now for the fun part, the CallHandler
object. The CallHandler
has a handle()
function that returns an observable. At the very least, this means that your next
object needs to look something like this:
const next = {
handle: () => of()
}
But that's pretty basic and doesn't help much with logging responses or working with response mapping. To make the handler function more robust we can always do something like
const next = {
handle: jest.fn(() => of(myDataObject)),
}
Now if needed you can override the function via Jest, but in general this is enough. Now your next.handle()
will return an Observable and will be pipable via RxJS operators.
Now for testing the Observable, you're just about right with the subscribe you're working with, which is great! One of the tests can look like this:
describe('ResponseInterceptor', () => {
let interceptor: ResponseInterceptor;
let loggerSpy = jest.spyOn(Logger.prototype, 'debug');
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
afterEach(() => {
loggerSpy.resetMock();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const responseInterceptor: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
responseInterceptor.subscribe({
next: value => {
// expect the logger to have two parameters, the data, and the intercept function name
expect(loggerSpy).toBeCalledWith({statusCode: 200, responseData: value}, 'intercept');
},
error: error => {
throw error;
},
complete: () => {
// only logging one request
expect(loggerSpy).toBeCalledTimes(1);
done();
},
});
});
});
});
Where executionContext
and callHandler
are from the values we set up above.
A similar idea could be done with the RequestInterceptor
, but only logging in the complete
portion of the observer (the subscribe callback) as there are no data points returned inherently (though it would still work either way due to how observables work).
If you would like to see a real-world example (albeit one with a mock creation library), you can check out my code for a logging package I'm working on.
Upvotes: 17