Nest Js Jest Unit Testing: Providing a mock implementation not used from controller - jestjs

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)),
}
}
}

Related

NestJS with Prisma Transactions

I'm trying to use a Prisma transaction in a NestJS project and I can't figure out a clean way to accomplish the following:
Have a service that will call other services and have all of them bound to a transaction. Eg:
#Injectable()
export class OrdersService {
constructor(private prismaService: PrismaService, ...) {}
async someFn() {
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
await this.userService.update() // This will perform an update using prismaService internally
await this.otherService.delete() // Again, it'll use prismaService
}
}
}
In this case, both user and other services will use their own prisma service and won't be bound to the Transaction.
Is there a way to accomplish this without passing the prismaServiceBoundToTx to each method?
The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing $on, $connect, $disconnect, $use and the $transaction methods.
If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:
**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
const userService = new UserService(prismaServiceBoundToTransaction)
const otherService = new OtherService(prismaServiceBoundToTransaction)
//Following calls will use prismaServiceBoundToTransaction internally
await userService.update()
await otherService.delete()
}
Of course above only works, if UserService and OtherService are stateless.
So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction.
All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!
export interface PrismaClientWithCustomTransaction
extends Readonly<Prisma.TransactionClient> {
interactiveTransaction<F>(
fn: (prisma: Prisma.TransactionClient) => Promise<F>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
}
): Promise<F>;
}
We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient
export class TransactionalPrismaClient<
T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
U = 'log' extends keyof T
? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
? Prisma.GetEvents<T['log']>
: never
: never,
GlobalReject extends
| Prisma.RejectOnNotFound
| Prisma.RejectPerOperation
| false
| undefined = 'rejectOnNotFound' extends keyof T
? T['rejectOnNotFound']
: false
> implements PrismaClientWithCustomTransaction
{
constructor(private readonly transactionalClient: Prisma.TransactionClient) {}
$executeRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): PrismaPromise<number> {
return this.transactionalClient.$executeRaw(query, ...values);
}
$executeRawUnsafe<T = unknown>(
query: string,
...values: any[]
): PrismaPromise<number> {
return this.transactionalClient.$executeRawUnsafe(query, ...values);
}
$queryRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): PrismaPromise<T> {
return this.transactionalClient.$queryRaw(query, ...values);
}
$queryRawUnsafe<T = unknown>(
query: string,
...values: any[]
): PrismaPromise<T> {
return this.transactionalClient.$queryRawUnsafe(query, ...values);
}
get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
return this.transactionalClient.otherEntity;
}
get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
return this.transactionalClient.userEntity;
}
async interactiveTransaction<F>(
fn: (prisma: Prisma.TransactionClient) => Promise<F>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
}
): Promise<F> {
return await fn(this.transactionalClient);
}
}
And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.
#Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, PrismaClientWithCustomTransaction
{
private readonly logger = new ConsoleLogger(PrismaService.name);
async onModuleInit() {
this.logger.log('Trying to connect to db.');
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
interactiveTransaction<R>(
fn: (prisma: Prisma.TransactionClient) => Promise<R>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
},
numRetries = 1
): Promise<R> {
let result: Promise<R> | null = null;
for (let i = 0; i < numRetries; i++) {
try {
result = this.$transaction(fn, options);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
//TODO?
} else {
throw e;
}
}
if (result != null) {
return result;
}
}
throw new Error(
'No result in transaction after maximum number of retries.'
);
}
}
Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:
providers: [
{
provide: 'PRISMA_SERVICE_TOKEN',
useClass: PrismaService,
},
],
exportt class UserService{
constructor(#Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}
Alright so now we can do the following:
#Injectable()
export class OrdersService {
constructor( #Inject('PRISMA_SERVICE_TOKEN')
private readonly prisma: PrismaClientWithCustomTransaction, ...) {}
async someFn() {
return await this.prisma.interactiveTransaction(
async (client) => {
//You can still use client directly, if you dont need nested transaction logic
return client.userEntity.create(...)
//Or create services for nested usage
const transactionalClient = new TransactionalPrismaClient(client);
const userService = new UserService(transactionalClient);
return userService.createUser(...);
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
);
}
}
And if you need the $on, $connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.

How to mock prisma with jest-mock

I use prisma to interact with my database and i would like to use jest-mock to mock the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false
brands.test.ts
import { PrismaService } from "#services/mysql.service";
import { mocked } from "jest-mock";
import faker from "#faker-js/faker";
import { GetBrands } from "./brand";
jest.mock("#services/mysql.service");
/**
* #group unit
*/
describe("Brand", () => {
afterAll(async () => {});
const mockedPrismaService = mocked(PrismaService, true);
it("should get a list of brands", async () => {
const mockedData = [
{
id: faker.datatype.uuid(),
name: faker.datatype.string(),
image: {
source: "some_source",
dtype: "some_dtype",
},
},
];
//#ts-ignore - because of relational data mockedData.image
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const [response, error] = await GetBrands();
console.log(response, error);
});
});
mysql.service.ts
import mysql from "mysql2/promise";
import { Config } from "#config";
import { PrismaClient, Prisma } from "#prisma/client";
export const MySQLEscape = mysql.escape;
export const MySQLPreparedStatement = mysql.format;
export const PrismaService = new PrismaClient({});
export const PrismaHelper = Prisma;
However when i run this test i get the following error.
TypeError: Cannot read properties of undefined (reading 'brand')
Factory Mock
One option is to option use the factory approach when mocking your client.
jest.mock("#services/mysql.service", () => ({
PrismaService: {
brand: {
findMany: jest.fn(() => { })
}
},
}));
Then within your test, you can mock the findMany function to return your test data, then call the function being tested.
const mockedData = [...];
PrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
const result = await GetBrands();
It's a bit cumbersome, but it works.
Note that in my example, I've implemented GetBrands as follows:
import { PrismaService } from "#services/mysql.service"
export const GetBrands = async () => {
const data = await PrismaService.brand.findMany();
return data;
}
Your example
In your example, you're using automatic mocking, and I'm not too familiar with it so I'm not sure how to get it working.
What seems to be happening to cause the error is your PrismaService is undefined when it's imported here:
import { PrismaService } from "#services/mysql.service";
And then calling the mocked function with an undefined parameter returns undefined:
const mockedPrismaService = mocked(undefined, true); // returns undefined
And finally, calling the following is what throws the error:
mockedPrismaService.brand.findMany.mockResolvedValueOnce(mockedData);
// TypeError: Cannot read properties of undefined (reading 'brand')
I would have thought something like this would be what you're after, but this throws an error:
jest.mock("#services/mysql.service", () => ({
PrismaService: mocked(PrismaService, true)
}));
// 6 |
// 7 | jest.mock("#services/mysql.service", () => ({
//> 8 | PrismaService: mocked(PrismaClient, true)
// | ^
// 9 | }));
Check out the docs
Might be worth checking out the Prismas documentation on unit testing, as they suggest a couple of pretty different approaches.

Create mocked service (object) with one method returning a value

In an Angular environment, how can I very easily create in a Jest environment a mocked service for a service object returning a specific value? This could be via Jest of ng-mocks, etc.
An oversimplified example:
// beforeEach:
// setup an Angular component with a service component myMockService
// Test 1:
// fake "myMockService.doSomething" to return value 10
// expect(myComponent.getBalance()).toEqual( "Your balance: 10");
// Test 2:
// fake "myMockService.doSomething" to return value 20
// expect(myComponent.getBalance()).toEqual( "Your balance: 20");
I have studied the Jest and ng-mocks docs but didn't find a very easy approach. Below you find 2 working approaches. Can you improve the version?
My simplified Angular component:
#Component({
selector: 'app-servicecounter',
templateUrl: './servicecounter.component.html'
})
export class ServicecounterComponent {
private myValue: number = 1;
constructor(private counterService: CounterService) { }
public doSomething(): void {
// ...
myValue = this.counterService.getCount();
}
}
This is the simplified service:
#Injectable()
export class CounterService {
private count = 0;
constructor() { }
public getCount(): Observable<number> {
return this.count;
}
public increment(): void {
this.count++;
}
public decrement(): void {
this.count--;
}
public reset(newCount: number): void {
this.count = newCount;
}
}
Try 1: a working solution: with 'jest.genMockFromModule'.
The disadvantage is that I can only create a returnValue only at the start of each series of tests, so at beforeEach setup time.
beforeEach(async () => {
mockCounterService = jest.genMockFromModule( './counterservice.service');
mockCounterService.getCount = jest.fn( () => 3);
mockCounterService.reset = jest.fn(); // it was called, I had to mock.fn it.
await TestBed.configureTestingModule({
declarations: [ServicecounterComponent],
providers: [ { provide: CounterService, useValue: mockCounterService }],
}).compileComponents();
fixture = TestBed.createComponent(ServicecounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('shows the count', () => {
setFieldValue(fixture, 'reset-input', String(currentCount));
click(fixture, 'reset-button');
expect(mockCounterService.getCount()).toEqual( 3);
expect( mockCounterService.getCount).toBeCalled();
});
Try 2: replace 'jest.genMockFromModule' with 'jest.createMockFromModule': works equally well.
The disadvantage is still that I can create a returnValue only at the start of each series of tests, so at beforeEach setup time.
Try 3: create a mock object upfront: didn't work
jest.mock( "./counterservice.service");
beforeEach(async () => {
// Create fake
mockCounterService = new CounterService();
(mockCounterService.getCount() as jest.Mock).mockReturnValue( 0);
await TestBed.configureTestingModule({
declarations: [ServicecounterComponent],
providers: [{ provide: CounterService, useValue: mockCounterService }],
}).compileComponents();
fixture = TestBed.createComponent(ServicecounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('shows the count', () => {
// do something that will trigger the mockCountService getCount method.
expect(mockCounterService.getCount).toEqual( 0);
});
This doesn't work, giving the error:
> (mockCounterService.getCount() as jest.Mock).mockReturnValue( 0);
> Cannot read property 'mockReturnValue' of undefined
Try 4: with .fn(). The disadvantage is that the original class may change, then the test object MUST change.
beforeEach(async () => {
mockCounterService = {
getCount: jest.fn().mockReturnValue( 0),
increment: jest.fn,
decrement: jest.fn(),
reset: jest.fn
};
await TestBed.configureTestingModule({
declarations: [ServicecounterComponent],
providers: [{ provide: CounterService, useValue: mockCounterService }],
}).compileComponents();
});
it( '... ', () => {
// ...
expect(mockCounterService.reset).toHaveBeenCalled();
});
This time, the error is:
> Matcher error: received value must be a mock or spy function ...
> expect(mockCounterService.reset).toHaveBeenCalled();
Can you help improving this way of working?
You need to use MockBuilder to mock the service, and MockInstance to customize it.
Also getCount is an observable, therefore its mock should return Subject, which we can manipulate.
// to reset MockInstance customizations after tests
MockInstance.scope();
// to use jest.fn on all mocks https://ng-mocks.sudo.eu/extra/auto-spy
beforeEach(() => ngMocks.autoSpy('jest'));
afterEach(() => ngMocks.autoSpy('reset'));
beforeEach(() => MockBuilder(ServicecounterComponent, CounterService));
it('testing', () => {
// this is our control of observable of getCount
const getCount$ = new Subject<number>();
// now we need to return it when getCount is called
const getCount = MockInstance(CounterService, 'getCount', jest.fn())
.mockReturnValue(getCount$);
// now we can use it in our test.
const fixture = MockRender(ServicecounterComponent);
ngMocks.click('.reset-button');
expect(getCount).toHaveBeenCalled();
getCount$.next(3);
expect(ngMocks.formatText(fixture)).toContain('3');
});

Jest SpyOn __mocks__ module

We mock our modules using __mocks__. I'd like to spyOn a module function in my test but it doesn't seem to be working. Related question, I'd also like to override a mocked module function for a test (ie throw an exception). How can I do this?
├──__mocks__
| └──DB-Utils.js
| └──controllers
| └──myController.js
├──node_modules
__mocks__/DB-Utils.js:
const { MyController } = require('./controllers/myController');
module.exports = {
MyController,
};
__mocks__/controllers/myController.js:
class MyController {
async setAvailability(id, availability) {
return true;
}
}
module.exports = {
MyController,
};
test.spec.js:
const { MyController } = require('DB-Utils');
const myController = new MyController();
describe('Register Tests', () => {
fit('myController setAvailability', async () => {
---code that calls a class that ends up calling myController.setAvailability---
expect(myController.setAvailability).toHaveBeenCalledWith('foo', 'bar');
});
});
My tests pass in that the mock myController is called, however it fails the toHaveBeenCalledWith with an error of Number of calls: 0
How can I spy setAvailability?
For the related question I'd also like to be able to do something like the following:
describe('Register Tests', () => {
fit('myController setAvailability throws', async () => {
jest.spyOn(myController, 'setAvailability').mockImplementation(() => {
throw new Error()
});
expect(---code that calls a class that ends up calling myController.setAvailability---).toThrow();
});
});

can't get the Jest provided ESM example to run

I'm just trying to get the ES6 Class Mocks example provided by Jest to run green.
here's my code repo
it's taken me way to long to even get to this point, but the tests still fail with
TypeError: SoundPlayer.mockClear is not a function
system under test
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
the test
import {jest} from '#jest/globals';
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
beforeEach(() => {
SoundPlayer.mockClear();
mockPlaySoundFile.mockClear();
});
it('The consumer should be able to call new() on SoundPlayer', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
// Ensure constructor created the object:
expect(soundPlayerConsumer).toBeTruthy();
});
it('We can check if the consumer called the class constructor', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(SoundPlayer).toHaveBeenCalledTimes(1);
});
it('We can check if the consumer called a method on the class instance', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
const coolSoundFileName = 'song.mp3';
soundPlayerConsumer.playSomethingCool();
expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});
system under test dependency
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}

Resources