DEV Community

Teddy MORIN
Teddy MORIN

Posted on • Originally published at blog.scalablebackend.com on

How To Write Efficient Unit Tests with Prisma ORM

Formula

During my life as a full-stack developer, I tried out lots of ORM, ODM, and query builders. Some were fantastic, while others still give me nightmares (we see you Sequelize).

Current state

Among the fantastic tools, there is Prisma. It works perfectly, has a great DX, documentation, and much more. I encountered a single issue when working with it, unit testing.

While they have a documentation page on unit testing, with a great introduction, their method of mocking is unsatisfactory at best.

Unit testing with Prisma

Learn how to setup and run unit tests with Prisma Client

favicon prisma.io

At best, that allows you to mock, one by one, the responses for the requests you believe will be called. I think this is inefficient for a few reasons:

  • It requires too much setup, on every single test.

  • There is a very low level of trust in the mocks you define, as you are not sure Prisma will return the same data.

  • By defining these mocks yourself, you get further away from testing your app behavior, and closer to testing implementation details.

  • With new versions of Prisma, you are at risk of seeing your tests being obsolete.

Lets look at what application we want to test, which tests to write, and then define how to write unit tests.

Application structure

My backend applications are usually split into a few layers. If we take an example with a basic ExpressJS app, it has at least 3 layers: module, controller, and service.

Each set of functionality is separated into a dedicated module , which handles routing, validation, and passes the request to the controller.

The controller is where the business logic is found, and often makes use of services.

A service provides high-level functions that make requests to the database. This is where I use ORM such as Prisma.

What should be tested

I usually have two primary types of tests when working on a backend application (and many more depending on the context!). They are unit and end-to-end tests.

I try to avoid writing e2e tests when possible, as they have limitations like being slow, expensive, and giving late feedback. I usually write unit tests for low-level functions such as middlewares.

On the other hand, I dont test my services or controllers in complete isolation. In those cases, I feel like its not giving me enough value for the time it takes to write tests. Instead, I want to test my endpoint behavior as a whole.

To do it, I write my tests using a test version of my backend app. Outside dependencies, like Prisma, are mocked (in an efficient way) which allows me to simulate queries in isolation using tools like SuperTest.

In this context, I write unit tests for most use-cases. It includes validation (such as parameters), authorization (ensure youre connected with the right access), but I also verify I receive the right response, using the mocked version of Prisma. Depending on my app, there is even more use-case.

I also end up writing e2e tests, to ensure my successful response and database-dependent errors, are the expected ones, in a real environment.

Those tests may partially overlap with my unit tests, but this time using a real database. It gives me the quick and early feedback of unit tests while having the high confidence of e2e tests.

How to write those tests

In the following examples, I expect you to understand the basics of testing. That includes Jest, which is used as part of the examples.

Testing with Node.js: Understand and Choose the Right Tools

What are the tools you need for testing with Node.js and which ones to choose for your purpose?

favicon blog.scalablebackend.com

Middleware

Middlewares dont need a special environment to be tested in, they can be considered just like any other function.

Only thing is, they have a defined format. For Express, they must return a function that takes a request , response , and next function. Lets have a look at the following snippet:

import { Request, Response, NextFunction } from 'express';

type Validator = (req: Request, res: Response) => boolean;

export function validator(validate: Validator) {
  return (req: Request, _res: Response, next: NextFunction) => {
    const valid = validate(req);
    if (!valid) next(new Error());
    else next();
  };
}

Enter fullscreen mode Exit fullscreen mode

Validator takes a function that determines if a request is considered valid, by returning a boolean based on the received request. It returns a middleware, which will throw an error if the received request is determined as invalid.

With Jest, this middleware can be tested quite easily by calling it with different arguments, and verifying next has been called with the right element.

import validator from '../validator.middleware';

describe('validator', () => {
  const req = { params: { mock: 'true' } };
  const res = {};

  it('Should call next with an error if request is invalid', () => {
    const mockNext = jest.fn();
    validator((req) => req.params.mock === 'false')(req, res, mockNext);
    expect(mockNext).toHaveBeenCalledWith(new Error());
  });

  it('Should call next otherwise', () => {
    const mockNext = jest.fn();
    validator((req) => req.params.mock === 'true')(req, res, mockNext);
    expect(mockNext).toHaveBeenCalled();
  });
});

Enter fullscreen mode Exit fullscreen mode

Endpoints (Unit)

I already explained I use SuperTest for my endpoints. I also talked about using a test version of my backend app. To be more precise, I have a few helper functions, dedicated to bootstraping my backend during tests and managing mocked data.

The following snippet is a good example of a unit test. We demonstrate how we can test an endpoint dedicated to creating an article.

We use helpers to bootstrap our app, generate the tokens needed for our use case, and send the request with SuperTest.

import { Express } from 'express';
import supertest from 'supertest';

import { ArticleFixture, ServerMock, UserMock } from '../../../../testing';

describe('POST /article', () => {
  let app: Express;
  const tokens = UserMock.generateTokens();

  beforeAll(async () => {
    app = await ServerMock.createApp();
  });

  test("Should return error if user isn't authenticated", () => {
    return supertest(app).post('/article').send(ArticleFixture.articles.newArticle).expect(401);
  });

  test("Should return error if user doesn't have ADMIN role", () => {
    return supertest(app)
      .post('/article')
      .set('Authorization', `Bearer ${tokens.user}`)
      .send(ArticleFixture.articles.newArticle)
      .expect(401);
  });

  it('Should create article', () => {
    return supertest(app)
      .post('/article')
      .set('Authorization', `Bearer ${tokens.admin}`)
      .send(ArticleFixture.articles.newArticle)
      .expect(201);
  });
});

Enter fullscreen mode Exit fullscreen mode

Mocking with Prisma

But we still didnt solve the issue that comes with Prisma. In the above snippet, it looks like nothing is mocked.

There is a single solution if we want to write tests with no dependence to a database or heavy mocking: using an in-memory implementation of Prisma.

Introducing prismock. Disclaimer: I am indeed its creator.

prismock - npm

A mock for PrismaClient, dedicated to unit testing.. Latest version: 1.17.0, last published: 2 days ago. Start using prismock in your project by running `npm i prismock`. There are no other projects in the npm registry using prismock.

favicon npmjs.com



As there was no satisfying solution to efficiently write unit tests with Prisma, I decided to write my own solution.

It actually reads your schema.prisma and generates models based on it. It perfectly simulates Prismas API and store everything in-memory for fast, isolated, and retry-able unit tests.

Remember how I use a helper to build a test version of my backend app?

In production I build my app, using a genuine PrismaClient , which is then bootstrapped. During my test, I replace PrismaClient using dependency injection.

In the above snippet, its done as part of ServerMock.createApp(), which makes it virtually invisible when I write my tests.

Endpoints (E2E)

In a context where our article endpoint and authorization process are already tested, we could argue that its not mandatory to test authentication on every single endpoint during e2e tests.

For example, we could end up with the following test:

import supertest from 'supertest';

import { ArticleFixture } from '../../fixtures';
import { ArticleMock } from '../../mocks';
import E2EUtils from '../EndToEndUtils';

describe('POST /article', () => {
  let tokens: { admin: string };

  beforeAll(async () => {
    tokens = await E2EUtils.generateTokens();
  });

  it('Should return created article', async () => {
    return supertest('http://localhost')
      .post('/article')
      .set('Authorization', `Bearer ${tokens.admin}`)
      .send(ArticleFixture.articles.newArticle)
      .expect(201)
      .then((response) => {
        expect(response.body).toEqual({
          title: ArticleMock.articles.newArticle.title,
          content: ArticleMock.articles.newArticle.content,
          slug: ArticleMock.articles.newArticle.slug,
        });
      });
  });
});

Enter fullscreen mode Exit fullscreen mode

This test must be written in a different environment, where we have access to a seeded database, and our endpoint has been built and runs in a near-production environment.

Conclusion

In this context, we should cover the entirety of our codebase with unit tests, giving us fast and early feedback, with a strong trust in our test results.

Using an in-memory implementation of Prisma instead of manual mocks increases our confidence even more. Together with our testing strategy (defined in what should be tested ), we also end up with amazing productivity.

Finally, we write E2E tests exclusively for use-cases that make requests to our database. It does overlap with some unit tests, is slower, and more expensive with late feedback, but it eliminates the remaining grey areas.

We are then able to answer:

Are you confident in shipping your app to production?


Enterprise Grade Back-End Development with NodeJS

Learn how to write reliable backend applications using Node.JS & NestJS!

favicon scalablebackend.com

Photo by Roman Mager on Unsplash

Top comments (0)