How do I unit test a custom CacheInterceptor from NestJS? - unit-testing

I wrote an own CacheInterceptor to cache POST requests as well and take the Accept-Language header into account. Of course I want to unit test it, but I don't know how to properly do so, since the trackBy method needs an ExecutionContext and the method uses the httpAdapterHost and reflector fields.
Has anybody done this before and knows how to achieve full test coverage?
EDIT: Here is the code of the CacheInterceptor
import {
CACHE_KEY_METADATA,
CacheInterceptor,
ExecutionContext,
Injectable,
} from '#nestjs/common';
import { createHash } from 'crypto';
#Injectable()
export class MyCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const cacheMetadata = this.reflector.get(CACHE_KEY_METADATA, context.getHandler());
const request = context.switchToHttp().getRequest();
return [
cacheMetadata,
httpAdapter.getRequestUrl(request),
JSON.stringify(request.body),
request.headers['accept-language'],
]
.reduce(
(hash, somethingToHash) => (
hash.update(
somethingToHash
? Buffer.from(somethingToHash)
: Buffer.alloc(0)
)
),
createHash('md5'),
)
.digest('hex');
}
}

Please bear in mind that the following example is testing the interceptor in isolation. Some tweaks may be needed for your use case, but the overall approach should be valid.
I would inject the cache and reflector dependencies using the constructor:
#Injectable()
export class MyCacheInterceptor extends CacheInterceptor {
constructor(
#Inject(CACHE_MANAGER) protected readonly cacheManager: Cache,
#Inject(Reflector) protected readonly reflector: Reflector
) {
super(cacheManager, reflector);
}
trackBy(context: ExecutionContext): string | undefined {
// ...
// ...
Your tests could look like:
describe("MyCacheInterceptor", () => {
let interceptor: MyCacheInterceptor;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [CacheModule.register()],
providers: [
{ provide: CACHE_MANAGER, useValue: {} },
{ provide: Reflector, useValue: { get: () => "hello" } },
MyCacheInterceptor,
],
}).compile();
// see issue: https://github.com/nestjs/nest/issues/8076
module.createNestApplication();
interceptor = module.get(MyCacheInterceptor);
});
it("creates", () => {
expect(interceptor).toBeTruthy();
});
it("tracks something", () => {
const mockExecutionContext: ExecutionContext = createMock<ExecutionContext>(
{
getHandler: () => ({}),
switchToHttp: () => ({
getRequest: () => ({
url: "/test-url",
originalUrl: "/test-url",
method: "GET",
body: {
someKey: "someValue",
},
headers: {
"accept-language": "en",
},
}),
}),
}
);
const result = interceptor.trackBy(mockExecutionContext);
expect(result).toBe("d4f8ad8ba612cda9a5fda09cc244120c");
});
});
There is a way of mocking httpAdapterHost (as well as cacheManager and reflector):
(interceptor["httpAdapterHost"] as any) = {
httpAdapter: { getRequestUrl: () => "hello" },
};
I consider this an anti-pattern, because you shouldn't be mocking/spying on internal methods and properties. However, if you check this GitHub issue, you'll see that there isn't a good or proper way of mocking an HttpAdapterHost, so in this case it may be a good rule to break.
createMock comes from #golevelup/ts-jest

Related

How to test NestJS Controller if it has a lot of dependent services?

I have a trouble with Testing Nest JS Controller because I cannot realise how to make a Testing Module with all the dependencies. I've already tried Mocks but still it's not working.
Here's how the controller I want to test looks like
calculator.controller.ts
#Controller('/calculator')
export class CalculatorController {
constructor(
#Inject(HISTORY_SERVICE)
private historyService: HistoryService,
#Inject(CACHE_SERVICE)
private readonly cacheService: CacheService,
#Inject(CALCULATOR_SERVICE)
private readonly calculatorService: CalculatorService,
) {}
#Get()
getResult(#Query() expressionDto: ExpressionDto): Promise<ClientResponseDto> {
const expression: string = expressionDto.expression;
const response: Promise<ClientResponseDto> = this.cacheService
.checkInCache(expression)
.then((response) => {
if (!response) {
const calculationResult =
this.calculatorService.getResult(expression);
const clientDto = this.historyService
.create({
expression,
result: calculationResult,
})
.then((dbResponse) => {
return this.cacheService.setToCache(dbResponse);
});
return clientDto;
}
return this.historyService.create({ expression, result: response });
});
return response;
}
}
And this is how it's spec looked like before mocks implementation
controller.spec.ts
let calculatorController: CalculatorController;
let calculatorService: CalculatorService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [HistoryModule],
controllers: [CalculatorController],
providers: [
CalculatorService,
],
})
.useMocker(() => createMock())
.compile();
calculatorController =
moduleRef.get<CalculatorController>(CalculatorController);
calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
jest.clearAllMocks();
});
describe('Calculator Controller', () => {
it('should be defined', () => {
expect(calculatorController).toBeDefined();
});
it('should have all methods', () => {
expect(calculatorController.getResult).toBeDefined();
expect(calculatorController.getResult(calculatorStub().request)).toBe(
typeof Promise,
);
});
});
And this test failed when calling getResult function cause inside this Function firstly I call CacheService to check data in Cache. So at this moment test failed telling that
TypeError: this.cacheService.checkInCache(...).then is not a function
24 | const response: Promise<ClientResponseDto> = this.cacheService
25 | .checkInCache(expression)
> 26 | .then((response) => {
| ^
I started to think that the problem is Testing module somehow doesn't have access to the Cache Service, so I added mock to the providers like this
let calculatorController: CalculatorController;
let calculatorService: CalculatorService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [HistoryModule],
controllers: [CalculatorController],
providers: [
CalculatorService,
{
provide: CacheService,
useValue: {
checkInCache: jest.fn().mockResolvedValue(Promise<null>),
},
},
],
})
.useMocker(() => createMock())
.compile();
calculatorController =
moduleRef.get<CalculatorController>(CalculatorController);
calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
jest.clearAllMocks();
});
But now tests don't even run cause I have Nest dependencies problems
Nest can't resolve dependencies of the CalculatorController (HISTORY_SERVICE, ?,
CALCULATOR_SERVICE). Please make sure that the argument dependency at index [1] is
available in the RootTestModule context.
What is the issue and how is it possible to solve this problem?
Generally speaking, when unit testing a service or a controller, you want to provide mocks for the controller's or service's dependencies. Most of the time, these are going to be objects with the same method names but the methods are set to be jest.fn() or similar for other mock libraries. You'll want to use custom providers to create the mock providers that will be injected. Taking your controller above, you'll want the setup of your test to look something like this:
describe('CaclulatorController', () => {
let controller: CalculatorController;
let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
beforeAll(async () => {
const modRef = await Test.createTestModule({
controller: [CalculatorController],
providers: [
{
provide: CALCULATOR_SERVICE,
useValue: {
getResult: jest.fn(),
},
},
{
provide: CACHE_SERVICE,
useValue: {
checkInCache: jest.fn(),
setToCache: jest.fn(),
},
},
{
provide: HISTORY_SERVICE,
useValue: {
create: jest.fn(),
},
},
]
}).compile();
controller = app.get(CalculatorController);
service = app.get(CALCULATOR_SERVICE);
cache = app.get(CACHE_SERVICE);
history = app.get(HISTORY_SERVICE);
});
Okay that's a lot to look at at once, so let's step through the big parts and explain what's going on here. The first this I do is set up local variables to reference during the test for the class that I'm testing (CalculatorController) and the dependencies of the class so I can modify them as necessary. Next, I use a Pick<T, K> generic with the jest.MockedOject<T> generic to tell Typescript that "This class has been mocked by jest, and I only am worried about these methods of it" so later on when I use cache. I'll get some intellisense for the checkInCache and setToCache methods, and they'll have jest's mock function types.
In the beforeAll I set up the initial mocks for the dependencies, you can also set return values here using the appropriate mockReturnValue or mockResolvedValue methods.
Now that the mocks and dependencies are set up, we can actually write a test. My approach is to use a describe block per method and its per variation of the method's outcome and branches. I'll write a single branch to show you and let you work out the rest from there.
describe('getResult', () => {
it('should get no response from the cache and perform a full caclulation', async () => {
cache.checkInCache.mockResolvedValueOnce(undefined);
service.getResult.mockResolvedValueOnce(calculationResult);
histoy.create.mockResolvedValueOnce(dbResult);
cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
});
})
This should cover the case where there's no value in the cache and the full set of steps has to be taken. By using mockResolvedValueOnce we ensure that the methods don't return if called more than once as that's most likely not the expected case here, and we're making sure to return promsies as you use .thens. You might want to look into async/await syntax to help clean that up.
putting the two snippets together we have the following:
describe('CaclulatorController', () => {
let controller: CalculatorController;
let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
beforeAll(async () => {
const modRef = await Test.createTestModule({
controller: [CalculatorController],
providers: [
{
provide: CALCULATOR_SERVICE,
useValue: {
getResult: jest.fn(),
},
},
{
provide: CACHE_SERVICE,
useValue: {
checkInCache: jest.fn(),
setToCache: jest.fn(),
},
},
{
provide: HISTORY_SERVICE,
useValue: {
create: jest.fn(),
},
},
]
}).compile();
controller = app.get(CalculatorController);
service = app.get(CALCULATOR_SERVICE);
cache = app.get(CACHE_SERVICE);
history = app.get(HISTORY_SERVICE);
});
describe('getResult', () => {
it('should get no response from the cache and perform a full caclulation', async () => {
cache.checkInCache.mockResolvedValueOnce(undefined);
service.getResult.mockResolvedValueOnce(calculationResult);
histoy.create.mockResolvedValueOnce(dbResult);
cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
});
});
});
That should be enough to get you started on testing the rest of your controller. If you need more test setup examples, there's an entire GitHub repository of them with different setups

Override behavior of a stubbed function using JEST not working as expected

I have a test class that tests behavior of various HTTP methods in a Nest controller class. I am using Jest manual mocks to stub the behavior of various functions in the service class so that I do not have to rely on actual dependencies/services, eg. snowflake. I have a top level jest.mock() defined as follows which initializes the mocked version of the service class instead of the actual service class.The mocked service class is created inside mocks folder adjacent to the actual service class.
I am redefining the behavior of one of the mocked functions in the 'error scenario' describe block as shown in the code snippet below, for testing the error scenario . The test scenario : 'throws an error' is failing as it is still picking up the default mocked behavior. Any pointers or help is appreciated.
In short, I want to be able to define different mocked behavior for a single function of the same mocked class for various test scenarios.
Thanks
jest.mock('#modules/shipment-summary/shipment-summary.service');
describe('ShipmentSummaryController', () => {
let shipmentSummaryController: ShipmentSummaryController;
let shipmentSummaryService: ShipmentSummaryService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [],
controllers: [ShipmentSummaryController],
providers: [ShipmentSummaryService],
}).compile();
shipmentSummaryController = moduleRef.get<ShipmentSummaryController>(
ShipmentSummaryController,
);
shipmentSummaryService = moduleRef.get<ShipmentSummaryService>(
ShipmentSummaryService,
);
jest.clearAllMocks();
});
//All the tests inside this describe block work as expected
describe('valid shipment-mode scenario', () => {
describe('valid shipment modes for tenant', () => {
let modes: ShipmentMode[];
beforeEach(async () => {
modes = await shipmentSummaryController.getAllShipmentModes('256');
});
test('calls the service fn. with the correct arg', () => {
expect(shipmentSummaryService.getAvailableShipmentModes).toBeCalledWith(
'256',
);
});
test('all available shipment modes for 256 are returned', () => {
expect(modes).toEqual(validModeDropdown());
});
});
});
// redefining behavior of getAllshipmentModes() is not working
describe('error scenario', () => {
let modes: ShipmentMode[] = []
beforeEach(async () => {
modes = await shipmentSummaryController.getAllShipmentModes('256');
});
beforeAll(() => {
jest.clearAllMocks();
jest.mock('#modules/shipment-summary/shipment-summary.service.ts', () => {
return {
getAvailableShipmentModes: () => {
throw new Error('Test error');
},
}
});
});
test('throws an error', () => {
expect(() => shipmentSummaryController.getAllShipmentModes('256')).toThrow();
})
})
});
My mocked service class is as follows:
export const ShipmentSummaryService = jest.fn().mockReturnValue({
// Fn. to be mocked differently per test scenario.
getAvailableShipmentModes: jest.fn().mockResolvedValue(validModeDropdown()),
});
There are many ways of accomplishing this. The Nest docs outline a number of them. However, one of my preferred ways, useValue, is not as clear as it could be, so I'll added it here.
This example will also use jest in order to spy on a mock, changing its behavior depending on the test.
Imagine these two simple resources
Injectable();
export class SimpleService {
public sayHello(): string {
return "Hello, world!";
}
}
#Controller()
export class SimpleController {
constructor(
#Inject(SimpleService) private readonly simpleService: SimpleService
) {}
#Get()
public controllerSaysHello(): string {
return this.simpleService.sayHello();
}
}
Your tests could look something like this
describe("SimpleController", () => {
let controller: SimpleController;
const mockReturnValue = "Goodbye, world..",
mockSimpleService: SimpleService = {
sayHello: () => mockReturnValue,
};
beforeEach(() => {
jest.restoreAllMocks();
});
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
SimpleController,
{ provide: SimpleService, useValue: mockSimpleService },
],
}).compile();
controller = module.get(SimpleController);
});
test("default mockSimpleService", () => {
const result = controller.controllerSaysHello();
expect(result).toBe(mockReturnValue);
});
test("spied on mockSimpleService", () => {
const differentReturnValue = "Hallo!";
jest
.spyOn(mockSimpleService, "sayHello")
.mockReturnValue(differentReturnValue);
const result = controller.controllerSaysHello();
expect(result).toBe(differentReturnValue);
});
});

How to mock an imported function into a test suite in NestJs?

I want to write a unit test for my payment service but I'm receiving this error:
source.subscribe is not a function
at ./node_modules/rxjs/src/internal/lastValueFrom.ts:60:12
This is my service
import { HttpService } from '#nestjs/axios';
import { Injectable } from '#nestjs/common';
import { lastValueFrom } from 'rxjs';
import { PaymentInfo } from 'src/utils/types/paymentInfo';
#Injectable()
export class PaymentsService {
constructor(private readonly httpService: HttpService) {}
private createHeaderWithAuth(auth, contentType = 'application/json') {
return {
headers: {
authorization: auth.replace('Bearer', '').trim(),
'Content-Type': contentType,
},
};
}
async makePayment(auth: string, paymentInfo: PaymentInfo) {
const configs = this.createHeaderWithAuth(auth);
const response = await lastValueFrom(
await this.httpService.post(
`${process.env.PAYMENT_URL}/transaction/pay`,
paymentInfo,
configs
)
).catch((error) => {
console.log(error);
throw new Error(error.response.data.message);
});
return response.data;
}
}
So with a bit of searching and tinkering found out that this is caused by my import of a rxjs function to resolve the observable setted by axios.
I've searched ways to mock this function so I can properly test my service. But none of them gave me a solution, the questions i found only revolved around functions with modules, but these have none since is imported from a third party lib.
This is my test suite:
describe('Payments Service', () => {
let service: PaymentsService;
let mockedHttpService = {
post: jest
.fn()
.mockImplementation(
async (
url: string,
paymentInfo: PaymentInfo,
header = mockedHeader
) => {
return { mockedSuccessfulResponse };
}
),
get: jest
.fn()
.mockImplementation(async (url: string, header = mockedHeader) => {
return { ...mockedSuccessfulResponse, data: mockedUserCards };
}),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PaymentsService,
{
provide: HttpService,
useValue: mockedHttpService,
},
],
}).compile();
service = module.get<PaymentsService>(PaymentsService);
});
describe('Initialize', () => {
it('should define service', () => {
expect(service).toBeDefined();
});
describe('makePayment', () => {
it('should make a payment', async () => {
const payment = await service.makePayment(mockedAuth, mockedPaymentInfo);
expect(mockedHttpService.post).toHaveBeenCalledWith(
`${process.env.PAYMENT_URL}/transaction/pay`,
mockedPaymentInfo,
mockedHeader
);
expect(payment).toBe(mockedSuccessfulResponse);
});
});
});
Ps.: I removed the mocked objects to reduce the amount of code to read
you should use the of operator from rxjs, and drop the async keyword. Like:
.mockImplementation(
(
url: string,
paymentInfo: PaymentInfo,
header = mockedHeader
) => {
return of({ mockedSuccessfulResponse });
}
otherwise lastValueFrom won't receive an observable object.

Mocking axios in a vue3 typescript unit test using jest and vue-test-utils2 (Solved)

My component calls
this.axios.get()
when being mounted and passes a vuex-store variable to the api. The api returns an array as the response and the component displays some of the returned data after exchanging a loading-element with the real content.
In my unit test I want to simulate the result of the axios-request, wait for the transition between the loading- and the content-element and then finally check the validity of the content. However, the test fails and outputs:
Cannot read property 'get' of undefined
and highlights the get on this.axios.
Here is what I'm expecting to work (based on this guide):
... some imports etc. ...
const mockAxios = { whatIExpectToGet };
jest.mock("axios", () => ({
get: jest.fn(() => mockAxios)
}));
it("description of the test", async () => {
const wrapper = mount(MyComponent);
... code continues ...
Of course I'm accesssing axios via this and not directly like they do in the guide. But, since I can't find any mention of anything related to that, I assume that's irrelevant?
I also tried to mock axios myself like so:
... imports etc. ...
const axios = {
get: Promise.resolve({ whatIExpectToGet })
};
it("description of the test", async () => {
const wrapper = mount(MyComponent, {
global: {
mocks: [ axios ]
}
});
... code continues ...
Apparently people with similar problems used localVue.use() to inject stuff, but that's no longer supported.
Could someone be so kind and smart as to point me into the right direction, please?
Thank you.
-------------------> SOLUTION <-------------------
Thanks to tony 19 this question is already solved.
I ended up using an async function to mock axios because Promise.resolve() wasn't working for me:
import { shallowMount, flushPromises } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
describe("MyComponent.vue", () => {
const axios = {
get: async () => ({
data: { expectedData }
})
};
it("test description", async () => {
const wrapper = shallowMount(MyComponent, {
global: {
mocks: {
axios: axios
}
}
} as any);
expect(wrapper.html()).toContain("some_string_i_display_while_loading");
await flushPromises();
expect(wrapper.html()).toContain("some_string_i_display_after_getting_the_response");
});
});
Using global.mocks to mock axios is the right approach, but your attempt incorrectly used an array when it should've been an object:
const wrapper = mount(MyComponent, {
global: {
// mocks: [ axios ] ❌
mocks: { axios } ✅
}
})
Note axios.get() resolves to an axios.Response object, which stores the response data in its data property, so your mock should do the same.
Here's a full example:
// MyComponent.vue
export default {
mounted() {
this.axios.get('foo').then(resp => this.foo = resp.data)
}
}
// MyComponent.spec.js
it('gets foo', () => {
const wrapper = mount(MyComponent, {
global: {
mocks: {
axios: {
get: Promise.resolve({ data: { foo: true }})
// OR use an async function, which internally returns a Promise
get: async () => ({ data: { foo: true }})
}
}
}
}
})

Angular2, jasmine: Component changes not seen in test spec function

I'm still writing tests for my Angular app. I've a test that modifies an Org object, saves the changes, and then proves that the changes have been kept. However, the test isn't seeing the changes.
My mock Org service that saves the changes:
#Injectable()
export class MockOrgService {
constructor() { }
public save(org: Org): Observable<Org> {
let savedOrg: Org = new Org(org);
savedOrg.address2 = 'Saved with id: ' + org.id;
return Observable.of(savedOrg);
}
}
My mock router:
beforeEach(async(() => {
routeStub = { data: Observable.of( { org: org1 } ), snapshot: {} } ;
TestBed.configureTestingModule({
imports: [ FormsModule, RouterTestingModule ],
providers : [
{ provide: DialogService, useClass: MockDialogService },
{ provide: GlobalsService, useClass: MockGlobalsService },
{ provide: OrgService, useClass: MockOrgService },
{ provide: ActivatedRoute, useValue: routeStub }
],
declarations: [ OrgDetailComponent ],
})
.compileComponents();
}));
My component function being tested:
private gotoParent(): void {
this.router.navigate(['../'], { relativeTo: this.route });
}
public save(): void {
this.error = null;
let that = this;
this.orgService
.save(that.org)
.subscribe(
(org: Org): void => {
that.org = org;
that.savedOrg = new Org(org);
that.gotoParent();
},
error => this.error = error
);
}
My test:
it('responds to the Save click by saving the Org and refilling the component', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
comp.org.id = 2;
comp.org.name = 'Another Org';
let elButton = fixture.debugElement.query(By.css('#save'));
elButton.nativeElement.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(comp.error).toBeNull();
expect(comp.savedOrg.id).toEqual(2);
expect(comp.savedOrg.name).toEqual('Another Org');
expect(routeStub).toHaveBeenCalledWith(['../']);
});
});
When I use breakpoints I see that the OrgService.save() is called when click() is run, and that in the component save() function the that.savedOrg is being set. But when the test gets into the expect() functions comp.savedOrg is at its original value. It is as though there are two component instances.
FWIW, after setting, or not setting, my savedOrg my function then tries to route. I instead get an error:
Error: Expected a spy, but got Object({ data: ScalarObservable({ _isScalar: true, value: Object({ org: Org({ id: 2, [SNIP]
I'm not sure what I'm supposed to do to tell that the "goToParent" routing has been called.
Thanks in advance for help,
Jerome.
I figured out the "not seen in test spec function" issue. I am missing a line, right after the first whenStable(), which should be:
comp = fixture.componentInstance;
That makes everything sync OK. Now I must figure out how to make route testing work. That's another job.
Jerome.