Does anyone know what is the correct way to test a service on NestJS using the Crud library?
This is my service test class:
describe('AddressService', () => {
let module: TestingModule;
let addressService: AddressService;
let addressRepositoryMock: MockType<Repository<Address>>;
const req: CrudRequest = { parsed: null, options: null };
const createDto: Address = {Omitted: ''};
const addressMock: Address = {Omitted: ''};
beforeEach(async () => {
module = await Test.createTestingModule({
providers: [
AddressService,
{
provide: getRepositoryToken(Address),
useFactory: repositoryMockFactory,
},
],
}).compile();
addressService = module.get<AddressService>(AddressService);
addressRepositoryMock = module.get(getRepositoryToken(Address));
});
afterEach(() => {
jest.resetAllMocks();
});
describe('create Address', () => {
it('should calls addressRepository.save() and returns the result', async () => {
addressRepositoryMock.save.mockReturnValue(addressMock);
expect(addressService.createOne).not.toHaveBeenCalled();
const result = await addressService.createOne(req, createDto);
expect(result).toEqual(addressMock);
});
});
});
This is my Service class:
#Injectable()
export class AddressService extends TypeOrmCrudService<Address> {
constructor(#InjectRepository(Address) repo) {
super(repo);
}
}
When the test runs, this is the error that happens:
FAIL src/modules/address/address.service.spec.ts
● AddressService › create Address › should calls addressRepository.save() and returns the result
TypeError: Cannot read property 'columns' of undefined
8 | export class AddressService extends TypeOrmCrudService<Address> {
9 | constructor(#InjectRepository(Address) repo) {
> 10 | super(repo);
| ^
11 | }
12 |
13 | public async findAddress(address: Address): Promise<Address> {
at AddressService.onInitMapEntityColumns (../node_modules/#nestjsx/crud-typeorm/src/typeorm-crud.service.ts:327:45)
at new TypeOrmCrudService (../node_modules/#nestjsx/crud-typeorm/src/typeorm-crud.service.ts:31:10)
at new AddressService (modules/address/address.service.ts:10:3)
at Injector.instantiateClass (../node_modules/#nestjs/core/injector/injector.js:276:19)
at callback (../node_modules/#nestjs/core/injector/injector.js:74:41)
at Injector.resolveConstructorParams (../node_modules/#nestjs/core/injector/injector.js:113:24)
at Injector.loadInstance (../node_modules/#nestjs/core/injector/injector.js:78:9)
at Injector.loadProvider (../node_modules/#nestjs/core/injector/injector.js:35:9)
at async Promise.all (index 3)
● AddressService › create Address › should calls addressRepository.save() and returns the result
TypeError: Cannot read property 'save' of undefined
56 | describe('create Address', () => {
57 | it('should calls addressRepository.save() and returns the result', async () => {
> 58 | addressRepositoryMock.save.mockReturnValue(addressMock);
| ^
59 | expect(addressService.createOne).not.toHaveBeenCalled();
60 | const result = await addressService.createOne(req, createDto);
61 | expect(result).toEqual(addressMock);
at Object.<anonymous> (modules/address/address.service.spec.ts:58:26)
Test Suites: 1 failed, 1 passed, 2 total
Thanks in advance!
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed sit amet nulla libero. Sed laoreet.
that's how minimal I could go to make the mocked repo work with the superclass' constructor:
const mockRepo = {
metadata: { columns: [], connection: { options: { type: '' } } }
}
I hope to find a better alternative :D
So, this has to do with how the CRUD service builds a mapper based on the repository that it was given. You'll need to have your mock repository work on an entity that has columns for CRUD to map against.
Disclaimer: Haven't done it myself, but I've read through the source code about it.
Related
I have already a mock class implemented as a part of my previous work, which I want to provide as a part of my Jest Unit Testing.
Let me explain in code, that's better:
My Controller code:
export class VpcController {
constructor(private readonly vpcService: VpcService) {}
#Get
list() {
return this.vpcService.list();
}
}
My controller Jest Unit Test:
class VpcServiceMockFactory {
private list() {
return jest.fn().mockResolvedValue([TEMPLATE_VPC]);
}
getMock() {
const repoService: RepositoryService = new RepositoryService();
const vpsServiceMock: VpcServiceMock = new VpcServiceMock(repoService);
vpsServiceMock.create(TEMPLATE_VPC.name);
// This works
// return {
// list: this.list(),
// }
// This does not work
return {
list: vpsServiceMock.list(),
get: vpsServiceMock.get(TEMPLATE_VPC.id),
// create: vpsServiceMock.create('new-vpc'),
// update: vpsServiceMock.update(TEMPLATE_VPC.id, 'updated-name'),
// delete: vpsServiceMock.delete(TEMPLATE_VPC.id),
}
}
}
describe('VpcControllerTest', () => {
let controller: VpcController;
let spyService: VpcService;
beforeEach(async () => {
// Mock Services
const MockVpcServiceProvider = {
provide: VpcService,
useFactory: () => new VpcServiceMockFactory().getMock()
}
// Class-unter-test instantiation
const module: TestingModule = await Test.createTestingModule({
controllers: [VpcController],
providers: [VpcService, MockVpcServiceProvider],
}).compile()
// Get the instance handlers
controller = module.get<VpcController>(VpcController);
spyService = module.get<VpcService>(VpcService);
});
it('Get collection of VPCs', async () => {
// Execute the method
const result = await controller.list();
// Assertion
expect(spyService.list).toHaveBeenCalled();
expect(result.length).toBe(1);
expect(result[0].name).toBe('zopa');
});
}
My VpcServiceMock class:
export class VpcServiceMock {
constructor(private repository: RepositoryService) {}
list() {
return this.repository.list<VpcModel>(VPC);
}
}
My RepositoryService class:
async list<T>(type: VPC): Promise<T[]> {
return <T[]>this.aSimpleJsonObject[type];
}
However when I am running it, it is showing this error:
● VpcControllerTest › Test-2: Get collection of VPCs
TypeError: this.vpcService.list is not a function
38 | #ApiForbiddenResponse({ description: 'Unauthorized Request' })
39 | list() {
> 40 | return this.vpcService.list();
| ^
41 | }
42 |
43 | #Get(':id')
at VpcController.list (src/vpc/vpc.controller.ts:40:28)
at Object.<anonymous> (src/vpc/vpc.controller.spec.ts:87:37)
So the only way I can make it work:
If I provide a mock implementation in the Jest Unit test's VpcServiceMockFactory class (like I showed in the commented out code there as // This works).
Definitely I am missing something here, which I am not able to figure out.
Got my issue.
Basically I missed the jest.fn().mockResolvedValue.
Modified to make it work:
class VpcServiceMockFactory {
async getMock() {
const repoService: RepositoryService = new RepositoryService();
const vpcServiceMock: VpcServiceMock = new VpcServiceMock(repoService);
await vpcServiceMock.create(TEMPLATE_VPC.name); // repository store initialization
return {
list: jest.fn().mockResolvedValue(vpcServiceMock.list()),
get: jest.fn().mockResolvedValue(vpcServiceMock.get(TEMPLATE_VPC.id)),
create: jest.fn().mockResolvedValue(vpcServiceMock.create('new-vpc')),
update: jest.fn().mockResolvedValue(vpcServiceMock.update(TEMPLATE_VPC.id, 'updated-name')),
delete: jest.fn().mockResolvedValue(vpcServiceMock.delete(TEMPLATE_VPC.id)),
}
}
}
I'm writing a toy app to learn more about Serverless Framework and AWS AppSync etc.
I'm trying to do TDD as much as possible. I'm using mock-apollo-client to mock the ApolloClient, and I've run into a problem. When trying to write a test to make sure the arguments to the query are passed, the test always returns a 401 Unauthorized error. It seems as though the real end point is still being called, because when a valid x-api-key is added to the instantiation of the ApolloClient, the test returns the real value from the AppSync server, and not the mock value I'm expecting. I'm using a mock, not spy, so I'm not expecting the real end point to actually be hit. Furthermore When I do add a valid x-api-key the test fails because the function is never called.
api › recipes › Given a valid recipe id › Should call query with the id as a param
expect(jest.fn()).toBeCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
I'm expected the test to fail, because the query currently isn't called with any arguments, but instead it fails because the mock function is never called.
What am I doing wrong?
Code File
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe (title:"Lemon Cheese Cake") {
title,
ingredients{
name,
amount,
unit
},
steps
}
}
`;
const gqlQuery = (title) => {
return client
.query({
query: GET_RECIPE_QUERY,
variables : { title }
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
Test file
import { createMockClient } from 'mock-apollo-client';
import { GET_RECIPE_QUERY, getRecipe } from './recipes';
const mockRecipe = {
title: 'Luke\'s Chocolate Orange',
ingredients: [
{
name: 'Orange',
amount: 1,
},
{
name: 'Chocolate',
amount: 250,
unit: 'grams',
},
],
steps: [
'Peel orange',
'Open chocolate',
'Eat chocolate',
'Throw orange away',
],
};
const mockClient = createMockClient();
const queryHandler = jest.fn().mockResolvedValue({data: {recipe: mockRecipe}});
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
it('Should call query with the id as a param', async () => {
const id = 'Luke\'s Chocolate Orange';
const result = await getRecipe(id);
expect(queryHandler).toBeCalledTimes(1);
expect(queryHandler).toBeCalledWith(id);
});
});
});
});
Packages
Versions
#apollo/client
3.5.10
graphql
16.3.0
#testing-library/jest-dom
5.16.2
#testing-library/react
12.1.4
#testing-library/user-event
13.5.0
jest
27.5.1
mock-apollo-client
1.2.0
mock-apollo-client always use the with ApolloProvider, so that you pass the mock apollo client via React context Provider to descendant components.
However, your code cannot pass the mock apollo client to the component in this way. Your code initiates requests directly from the Apollo Client. We need to intercept these GraphQL requests. There are several ways to do this such as msw. However, I'll continue to use the mock-apollo-client library to demonstrate.
You need to mock ApolloClient class of the #apollo/client module. We need to use Mocking Partials, we don't want to mock other things exported from #apollo/client. Since the mock-apollo-client library already provides createMockClient function to create mocked apollo client, we don't need to mock by ourself.
An working example:
recipes.ts:
import { ApolloClient, gql, InMemoryCache } from '#apollo/client';
const client = new ApolloClient({
uri: 'https://redacted.appsync-api.redacted.amazonaws.com/graphql',
headers: {
'x-api-key': 'key-redacted',
},
cache: new InMemoryCache(),
});
export const GET_RECIPE_QUERY = gql`
query {
getRecipe(title: "Lemon Cheese Cake") {
title
ingredients {
name
amount
unit
}
steps
}
}
`;
const gqlQuery = (title) => {
return client.query({
query: GET_RECIPE_QUERY,
variables: { title },
});
};
export const getRecipe = async (id) => {
const result = await gqlQuery(id);
return result.data.getRecipe;
};
recipes.test.ts:
import { createMockClient } from 'mock-apollo-client';
const mockRecipe = {
title: "Luke's Chocolate Orange",
ingredients: [
{ name: 'Orange', amount: 1, unit: 'abc' },
{ name: 'Chocolate', amount: 250, unit: 'grams' },
],
steps: ['Peel orange', 'Open chocolate', 'Eat chocolate', 'Throw orange away'],
};
const mockClient = createMockClient();
describe('api', () => {
describe('recipes', () => {
describe('Given a valid recipe id', () => {
beforeEach(() => {
jest.resetModules();
});
it('Should call query with the id as a param', async () => {
jest.doMock('#apollo/client', () => {
return {
...jest.requireActual('#apollo/client'),
ApolloClient: jest.fn(() => mockClient),
};
});
const queryHandler = jest.fn().mockResolvedValue({ data: { getRecipe: mockRecipe } });
const { GET_RECIPE_QUERY, getRecipe } = require('./recipes');
mockClient.setRequestHandler(GET_RECIPE_QUERY, queryHandler);
const title = "Luke's Chocolate Orange";
const result = await getRecipe(title);
expect(result).toEqual(mockRecipe);
expect(queryHandler).toBeCalledWith({ title });
});
});
});
});
Test result:
PASS src/stackoverflow/71612556/recipes.test.ts
api
recipes
Given a valid recipe id
✓ Should call query with the id as a param (91 ms)
------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files | 90.91 | 100 | 66.67 | 90.91 |
mocks | 75 | 100 | 0 | 75 |
handlers.js | 66.67 | 100 | 0 | 66.67 | 14
server.js | 100 | 100 | 100 | 100 |
stackoverflow/71612556 | 100 | 100 | 100 | 100 |
recipes.ts | 100 | 100 | 100 | 100 |
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.775 s
You can find the source code here
I'm trying to do this test, for a simple function, but I can't. I posted my test code and error.
I tried to do it in several different ways but I was not successful.
I'm using NestJS CLI and the test using jestJs
// My coding
createSession(login: string, password: string) {
const search: UserEntity = this.users.find(
(user: UserEntity) => user.cpf === login,
);
if (search) return search.senha === password;
else throw new HttpException('UNAUTHORIZED', HttpStatus.UNAUTHORIZED);
}
// My test
it('should retrieve getHello', async () => {
await expect(
service.createSession(mockLogin.login, mockLogin.password),
).rejects.toEqual(
new HttpException('UNAUTHORIZED', HttpStatus.UNAUTHORIZED),
);
});
// The error
● LoginService › Get service › should retrieve getHello
HttpException: UNAUTHORIZED
22 | );
23 | if (search) return search.senha === password;
> 24 | else throw new HttpException('UNAUTHORIZED', HttpStatus.UNAUTHORIZED);
| ^
25 | }
26 | }
27 |
I managed to solve it as follows:
it('should retrieve', async () => {
await expect(
service.createSession(mockLogin.login, mockLogin.password),
).rejects.toThrow();
});
There is code in our codebase like below:
#Validate(Param1)
async post(request, responseHandler) {
// some code
}
I Am trying to test the post function. But want to avoid evaluating the #Validate function. The Validate is a function in another module.
// validator.ts
export const Validate = () => {
// some code
}
How to? .
You could use jest.mock(moduleName, factory, options) create the mocked Validate decorator instead of using the real Validate decorator which may have a lot of validation rules.
E.g.
index.ts:
import { Validate } from './validator';
export class Controller {
#Validate('params')
async post(request, responseHandler) {
console.log('real post implementation');
}
}
validator.ts:
export const Validate = (params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('real validator decorator implementation');
// lots of validation
const rval = oFunc.apply(this, args);
return rval;
};
};
};
index.test.ts:
import { Validate } from './validator';
import { mocked } from 'ts-jest/utils';
jest.mock('./validator');
describe('63531414', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should pass', async () => {
mocked(Validate).mockImplementationOnce((params) => {
return (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
const oFunc = descriptor.value;
descriptor.value = function inner(...args: any[]) {
console.log('mocked validator decorator implementation');
const rval = oFunc.apply(this, args);
return rval;
};
};
});
const { Controller } = require('./');
const logSpy = jest.spyOn(console, 'log');
const ctrl = new Controller();
await ctrl.post({}, () => {});
expect(Validate).toBeCalledWith('params');
expect(logSpy).toBeCalledWith('real post implementation');
});
});
unit test result with coverage report:
PASS src/stackoverflow/63531414/index.test.ts (12.634s)
63531414
✓ should pass (154ms)
console.log node_modules/jest-mock/build/index.js:860
mocked validator decorator implementation
console.log node_modules/jest-mock/build/index.js:860
real post implementation
--------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files | 45.45 | 100 | 25 | 45.45 | |
index.ts | 100 | 100 | 100 | 100 | |
validator.ts | 14.29 | 100 | 0 | 14.29 | 2,3,4,5,7,8 |
--------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.354s
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/63531414
I am trying to figure out how to mock a call to Date.now with jest in my nestjs application.
I have a repository method that soft deletes a resource
async destroy(uuid: string): Promise<boolean> {
await this.userRepository.update({ userUUID: uuid }, { deletedDate: Date.now() });
return true;
}
to soft delete we just add a timestamp of when it was requested to be deleted
Following some discussions on here and other sites I came up with this test.
describe('destroy', () => {
it('should delete a user schemas in the user data store', async () => {
const getNow = () => Date.now();
jest
.spyOn(global.Date, 'now')
.mockImplementationOnce(() =>
Date.now().valueOf()
);
const targetResource = 'some-uuid';
const result = await service.destroy(targetResource);
expect(result).toBeTruthy();
expect(userRepositoryMock.update).toHaveBeenCalledWith({ userUUID: targetResource }, { deletedDate: getNow() });
});
});
I assumed that .spyOn(global.Date) mocked the entire global dat function , but the Date.now() in my repository is still returning the actual date rather than the mock.
My question is, is there a way to provide the mock return value of Date.now called in the repository from the test or should I just DI inject a DateProvider to the repository class which I can then mock from my test?
jest.spyOn(Date, 'now') should work.
E.g.
userService.ts:
import UserRepository from './userRepository';
class UserService {
private userRepository: UserRepository;
constructor(userRepository: UserRepository) {
this.userRepository = userRepository;
}
public async destroy(uuid: string): Promise<boolean> {
await this.userRepository.update({ userUUID: uuid }, { deletedDate: Date.now() });
return true;
}
}
export default UserService;
userRepository.ts:
class UserRepository {
public async update(where, updater) {
return 'real update';
}
}
export default UserRepository;
userService.test.ts:
import UserService from './userService';
describe('60204284', () => {
describe('#UserService', () => {
describe('#destroy', () => {
it('should soft delete user', async () => {
const mUserRepository = { update: jest.fn() };
const userService = new UserService(mUserRepository);
jest.spyOn(Date, 'now').mockReturnValueOnce(1000);
const actual = await userService.destroy('uuid-xxx');
expect(actual).toBeTruthy();
expect(mUserRepository.update).toBeCalledWith({ userUUID: 'uuid-xxx' }, { deletedDate: 1000 });
});
});
});
});
Unit test results with 100% coverage:
PASS stackoverflow/60204284/userService.test.ts
60204284
#UserService
#destroy
✓ should soft delete user (9ms)
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
userService.ts | 100 | 100 | 100 | 100 |
----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.572s, estimated 11s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60204284