gnikyt Code ramblings.

Shopify Remix, Prisma, and Jest

Introduction #

Shopify has (once again) changed up their app framework and templates. This time, they’re utilizing Remix which is a React-based web framework, and Prisma which is an ORM.

Recently, I developed a large loyalty application utlizing the new framework. Because this app is critical to the merchant’s shop, I wanted to ensure the business logic was working correctly.

For my needs, I wanted to do both unit tests on specific code and integration tests. But, I was disappointed to see the framework setup offered by Shopify did not have testing built-in, nor any docs on how someone could get going on testing.

With Prisma, you can mock the database returns from Prisma (example findMany) out of the box for unit tests.

const record = {
  id: 1,
  title: "Example",
  isDeleted: false,
  deletedAt: ...,
  createdAt: ...,
  updatedAt: ...,
};
prismaMock.places.create.mockResolvedValue(record);

const result = await PlacesRepository(prismaMock).create(record.title); // returns a model
expect(result.toJSON()).toEqual(record);

However, in terms of integration tests, a setup would be required to actually setup Prisma to communicate with a test database. It took a while to get working properly so I thought I would share the process.

Testing Setup #

Note: This assumes you are using Remix with Typescript.

Dependencies #

npm install --save-dev jest ts-jest

Scripts #

Open package.json and add the following to the end of your scripts object:

/* package.json */
"test": "jest",
"test:coverage": "jest --coverage",
"test:db:generate": "prisma generate",
"test:db:deploy": "prisma migrate deploy",
"test:db:reset": "prisma db push --force-reset --accept-data-loss --skip-generate"

These commands will be utilized later.

Jest #

Open (or create) jest.config.js and input:

/* jest.config.js */
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  setupFilesAfterEnv: ["./app/mocks.ts"],
};

Git Ignore #

Add the following to your .gitignore:

/prisma/*.sqlite
/prisma/*.sqlite-journal
/coverage

Schema #

Update your prisma/schema.prisma file by changing the datasource.url value to env("DATABASE_URL):

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Mocks Setup #

Prisma #

mocks.prisma.ts #

Create a file app/mocks.prisma.ts with the following contents:

// eslint-disable-next-line import/no-extraneous-dependencies
import { afterAll, afterEach, beforeEach } from "@jest/globals";
import type { PrismaClient } from "@prisma/client";
import { execSync } from "child_process";
import { unlink } from "fs/promises";
import { existsSync } from "fs";
import * as path from "path";

// Path to generated types
const typesPath = path.resolve("node_modules", ".prisma", "client", "index.d.ts");

/**
 * Run an NPM command.
 *
 * @param script - Script to run.
 */
function npm(script: string) {
  execSync(["npm", "run", script].join(" "), {
    env: {
      ...process.env,
      NODE_ENV: "test",
    },
  });
}

/**
 * Generates the types, creates the database.
 */
async function createDatabase(client: PrismaClient) {
  if (!existsSync(typesPath)) {
    npm("test:db:generate");
  }

  npm("test:db:deploy");
  await client.$connect();
}

/**
 * Destroy the database.
 *
 * @param client - Prisma client.
 */
async function destroyDatabase(client: PrismaClient) {
  // Disconnects Prisma
  await client.$disconnect();

  // Remove database
  try {
    const dbPath = (process.env.DATABASE_URL as string).replace("file:", "");
    await Promise.allSettled([unlink(dbPath), unlink(`${dbPath}-journal`)]);
  } catch (e) {
    // Do nothing, we don't care
  }
}

/**
 * Resets the database in between each test.
 */
// function resetDatabase() {
//   npm("test:db:reset");
// }

/**
 * Seeds the database with initial data.
 *
 * @param client - Prisma client.
 */
function seedDatabase(client: PrismaClient) {
  // Seed shop session
  await client.session.create({
    data: {
      id: "example.myshopify.com_id",
      shop: "example.myshopify.com",
      accessToken: "token",
      state: "",
      isOnline: false,
    },
  ]);
}

/**
 * To be used on test suites which need this Prisma setup.
 */
export function prismaHooks(client: PrismaClient) {
  beforeEach(async () => {
    await createDatabase(client);
    await seedDatabase(client);
  });
  afterEach(() => destroyDatabase(client));
}

What this file does is utilize’s Jest’s global hooks (beforeAll, afterAll, etc) to setup and teardown the database for each test suite, if you invoke prismaHooks.

Before each test is ran, the database creation process is triggered and the database is seeded. After each test is ran, the database deletion process is triggered. Finally, if types have not yet been generated by Prisma, it will be done once only.

db.server.ts #

Create a file app/__mocks__/db.server.ts with the following content. This will be used by Jest to replace app/db.server.ts with this file.

import { PrismaClient } from "@prisma/client";
import * as path from "path";

// Database path to ../prisma/test-[time].sqlite
const dbPath = path.resolve(__dirname, "..", "..", "prisma", `test-${new Date().getTime()}.sqlite`);

// Set database URL override
process.env.DATABASE_URL = `file:${dbPath}`;

// Prisma client
const prisma = new PrismaClient();
export default prisma;

This ensures a unique database for each test suite.

Jest #

Create a file app/mocks.ts with the following contents to tell Jest to use the mock Prisma setup.

/* app/mocks.ts */
import { jest } from "@jest/globals";

jest.mock("./db.server");

Usage #

You are now ready to do integration tests!

/* app/order/processor.test.ts */
import { describe, expect, it } from "@jest/globals";
import db from "../db.server";
import { prismaHooks } from "../mocks.prisma";
import { ShopifyClient, ShopifyFixture } from "../mocks.clients";
// etc...

/* just an example... */

describe("order processor", () => {
  // Add our hooks for this suite
  prismaMocks(db);

  it("should insert order", async () => {
    const orderId = OrderId("gid://shopify/Order/1");
    const customerId = CustomerId("gid://shopify/Customer/1");
    const product1 = ProductId("gid://shopify/Product/1");
    const prodcut2 = ProductId("gid://shopify/Product/2");

    // Job data
    const jobData = {
      order: {
        id: orderId.toInt(),
        admin_graphql_id: orderId.toGql(),
        tags: [],
        total_price: "115.00",
        customer: {
          id: customerId.toInt(),
        },
        line_items: [
          {
            product_id: p1.toInt(),
            price: "100.00",
            quantity: 1,
          },
          {
            product_id: p2.toInt(),
            price: "15.00",
            quantity: 1,
          },
        ],
      },
      shop: "example.myshopify.com",
    };

    // Shopify GraphQL mock client
    const client = ShopifyClient(
      ShopifyFixture("product-no-tag", "product/no-override-tag"),
      ShopifyFixture("product-with-tag", "product/with-override-tag"),
      ShopifyFixture("add-tag", "tags-add-customer"),
    );

    // Run the processor
    const [retOrderId, retOustomerId, retSum]: JobReturn = await processor({
      db,
      client,
      job: { data: jobData },
    });
    expect(retOrderId.isSame(orderId)).toBe(true);
    expect(retCustomerId.isSame(customerId)).toBe(true);
    expect(retSum.isSame(Cents(3000)));
  });
});

Now, running npm run test will give us a testing result!

npm run test

> test:coverage
> jest

 PASS  app/order/processor.test.ts (10.12 s)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        10.44 s

Success! I hope this helps you run integrations tests with Prisma and Jest.

Tests running slow? #

There are several results on the web of people having issues of Jest running slow for Typescript. There are several solutions offered. Example:

  1. maxWorkers flag: Add --maxWorkers=[num], example: npm run test -- --maxWorkers=3
  2. runInBand flag: Add --runInBand which runs test in sync, example: npm run test -- --runInBand
  3. Remove ts-node: ts-node is slow, you can try swapping to swc with it’s Jest support

My app is fairly chunky, and in my testing of speeds the results are as followed: