Automating Tests with Prisma and GitHub Actions: My Workflow

––– views

4 min read

Testing is something people don't really enjoy, myself included, but I find it very important, the best time to start testing is when you add your first logic, the second-best time is now.

The project that I'm working on is using Prisma, typescript and of Nexus, most of our mutations are done with queues so testing these was incredibly challenging. I'm still not sure if I got the right solution, but it does the job pretty well.

Before I get into the solution let me give you a brief explanation of what Github actions are and how they are helpful.

"GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform", basically it allows you to create a set of predetermined actions that will happen when a certain event occurs, in my case, it's whenever something is pushed/pulled to the development branch.

Workflow file example

How it works is that you create a workflow file that will have a set of jobs running from the top down. Below you see my workflow file.

YAML node.js.yaml

# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
# TIP: in order to use env vars defined in github secrets you need to set them here like this
env:
SG_SENDGRID_API_KEY: ${{ secrets.SG_SENDGRID_API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: |
if [ -e yarn.lock ]; then
yarn install --frozen-lockfile
elif [ -e package-lock.json ]; then
npm ci
else
npm install --legacy-peer-deps
fi
- name: Redis Server in GitHub Actions
uses: supercharge/redis-github-action@1.4.0
- run: npm ci
- run: npm run build --if-present
- run: npm test

As you can see from the comments in the code block, it goes through the whole process of installing a clean version of the dependencies then it runs it in the specified node version in my case that is 14. x, ⁠After it finishes the installation of deps, I add a Redis plugin, which is another GitHub action, I use this for the queues that are run on some of the mutations that I'm testing.

After that, it runs a build, and finally, it runs the tests. Below you find a summary of all the workflow

A visual depiction of what is being written about

Environment variables with Github Actions

This setup uses many different environment variables in order to add an env var go to the settings tab click on secrets then actions.

A visual depiction of what is being written about

Test example

My tests are not mocked in any way, everytime tests need to run a new environment is created replicating the production one, I even connect to our test database and call the mutations/queries, modifying data along the way, in this way I can be sure that everything is tested in a way a real user would do it.

Here is the graphql client, I always authenticate the user, as all of my logic is auth-protected. I am using graphql-request package.

TS client.ts

export const graphQLClient = async (url: string, isAdmin: boolean) => {
let graphQLClient;
const user = {
email: SETTINGS.email,
password: SETTINGS.password
}
try {
const response: any = await request(url, signIn, { ...user })
const token = response.signIn.token
graphQLClient = new GraphQLClient(url, {
headers: {
authorization: `Bearer ${token}`,
},
})
} catch (err) {
logger.error("Error creating a graphql client in test env", err)
}
return graphQLClient
}

And here is a test case for placing order

TS canAddToCart.test.ts

test('can add to cart', async (done) => {
const client = await graphQLClient(config.url, false)
const { user, order } = await findUserAndCreateOrder();
const { addToCart, randomSku } = await addItemToCart({client, order, user});
const items = addToCart.items;
const addedItem = items.find(({ item }) => item.id === randomSku.id)
const updatedOrder = await calculateOrderTotal(order.id)
expect(updatedOrder.totalPrice).toBe(addToCart.totalPrice)
expect(addedItem.item.id).toBe(randomSku.id);
await deleteOrder(order.id)
done()
}, 50000)

I firstly generate the client, then I have these 2 util functions findUserAndCreateOrder and addItemToCart

💡
⁠findUserAndCreateOrder - finds the user based on the token and creates an empty order for it

I then proceed to find the random item that was added to the cart and calculate the order total price with another util function. Which are then compared to the actual response I get from the place order mutation. ⁠

Most of the testing I've seen is done in a mocked environment, or at least some part of it is mocked, I prefer to test against a real environment (or as close to real as I can get). ⁠ ⁠That's it, hope you learned something from it, let me know if you're interested in my whole testing process. ⁠⁠ ⁠

To comment please authenticate