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: ...,
;
}.places.create.mockResolvedValue(record);
prismaMock
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 */
.exports = {
modulepreset: "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;
-
dbPath
sets the path of where our test SQLite database will live,prisma/test-[time].sqlite
-
dbPath
is then is used to set theDATABASE_URL
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";
.mock("./db.server"); jest
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,
clientjob: { 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:
-
maxWorkers
flag: Add--maxWorkers=[num]
, example:npm run test -- --maxWorkers=3
-
runInBand
flag: Add--runInBand
which runs test in sync, example:npm run test -- --runInBand
-
Remove ts-node:
ts-node
is slow, you can try swapping toswc
with it’s Jest support
My app is fairly chunky, and in my testing of speeds the results are as followed:
- Running as-is: 47 seconds
- Running with maxWorkers=4: 38 seconds
- Running with runInBand: 73 seconds