Skip to content

Instantly share code, notes, and snippets.

@rinogo
Last active December 3, 2023 14:52
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rinogo/443875a738fc05f95dc740315afa6f77 to your computer and use it in GitHub Desktop.
Save rinogo/443875a738fc05f95dc740315afa6f77 to your computer and use it in GitHub Desktop.
Integration Testing a NestJS Service that leverages Prisma

Most examples I've seen of how to test a Prisma-injected NestJS Service (e.g. prisma-sample in testing-nestjs) are for "end to end" testing. They access the database, performing actual queries and then rolling back the results if necessary.

However, what if you just want to perform integration testing without actually hitting the database? This is the solution I came up with.

More information (and potentially alternatives) at this Stack Overflow Question.

import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from '../prisma.service';
import { JobService } from './job.service';
import * as crypto from 'crypto';
describe('JobService', () => {
let service: JobService;
let prisma: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [JobService, PrismaService],
}).compile();
service = module.get<JobService>(JobService);
//Get a reference to the module's `PrismaService` and save it for usage in our tests.
prisma = module.get<PrismaService>(PrismaService);
});
it('should get the first job for "steve"', async () => {
//Use `jest.fn()` to mock the specific Prisma function that is used in the function under test (`getFirstJob()`). This will cause the next call to `prisma.name.findMany()` to return the data we've provided. The actual database won't be accessed.
prisma.name.findMany = jest.fn().mockReturnValueOnce([
{ id: 0, name: 'developer' },
{ id: 10, name: 'architect' },
{ id: 13, name: 'dog walker' }
]);
//Check the return value of our function
expect(await service.getFirstJob("steve")).toBe('developer');
//Above, we use `mockReturnValueOnce()` to manually specify what the database query would return. This is great, but what happens if the actual query used in `getFirstJob()` changes? We would expect the result of `getFirstJob()` to change, too. But since we've hard-wired the result of the database query, this won't happen. To alert us in case the query does change, we use a custom matcher to verify that the underlying query hasn't changed.
expect(prisma.name.findMany).toHaveBeenCalledWithObjectMatchingHash('db0110285c148c77943f996a17cbaf27');
});
});
//Typescript definition for `.toHaveBeenCalledWithObjectMatchingHash()`.
declare global {
namespace jest {
interface Matchers<R> {
toHaveBeenCalledWithObjectMatchingHash(expected: string): CustomMatcherResult;
}
}
}
//Ah, the terrifyingly-named `.toHaveBeenCalledWithObjectMatchingHash()` custom matcher. This matcher asserts that the `received` function (`prisma.name.findMany` in our example above) is called with an object whose "JSON hash" matches `expected`. "JSON hash" means that we first convert the object to JSON and then take its MD5 hash. Note that since we're not using this hash for anything other than tracking data consistency in a test, MD5 is suitable; there's no need for SHA-1 or some other alternative.
expect.extend({toHaveBeenCalledWithObjectMatchingHash(received, expected) {
const isSpy = (received: any) =>
received != null &&
received.calls != null &&
typeof received.calls.all === 'function' &&
typeof received.calls.count === 'function';
const receivedIsSpy = isSpy(received);
const receivedName = receivedIsSpy ? 'spy' : received.getMockName();
const calls = receivedIsSpy
? received.calls.all().map((x: any) => x.args)
: received.mock.calls;
if(calls.length === 0) {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the function was not called.`,
};
}
if(calls[0].length === 0) {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the function was called, but not with any arguments.`,
};
}
const md5Hash = crypto.createHash('md5');
const receivedHash = md5Hash.update(JSON.stringify(calls[0][0])).digest('hex');
const pass = receivedHash === expected;
if(pass) {
return {
pass: true,
message: () => `expected the function to not be called with an object that hashes to '${expected}'. Instead, the passed object hashes to the same value.`,
};
} else {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the passed object hashes to '${receivedHash}'.`,
};
}
}});
@nudge-sangbeomhan
Copy link

Hello, I found your gists from stackoverflow.
Do we need to extend toHaveBeenCalledWithObjectMatchingHash method?

@rinogo
Copy link
Author

rinogo commented Jul 20, 2022

Hi, @backend-sangbeom - I'm not sure I understand your question. You don't need to use toHaveBeenCalledWithObjectMatchingHash(). Its purpose is essentially to ensure that the queries use in our test don't change. If the query changes, it will change the hash, which (if using this function), will cause the test to fail.

A failure of this type (the hash of the parameters changed) is much more helpful than allowing the test to fail for some other arbitrary reason.

I hope this helps! :) Please let me know if it doesn't.

@reeshabhranjan
Copy link

Hey @rinogo, I was trying out the automock library for nestjs. Do you know how we can use that and https://www.prisma.io/docs/guides/testing/unit-testing#mocking-prisma-client to achieve the functionality you did above? My main confusion is when do I use Test.createTestingModule and when do I use automock? Are they supposed to be used with each other?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment