Integration tests focus on testing how separate parts of the program work together. In the context of applications using a database, integration tests usually require a database to be available and contain data that is convenient to the scenarios intended to be tested.
One way to simulate a real world environment is to use Docker to encapsulate a database and some test data. This can be spun up and torn down with the tests and so operate as an isolated environment away from your production databases.
Note: This blog post offers a comprehensive guide on setting up an integration testing environment and writing integration tests against a real database, providing valuable insights for those looking to explore this topic.
Prerequisites
This guide assumes you have Docker and Docker Compose installed on your machine as well as Jest
setup in your project.
The following ecommerce schema will be used throughout the guide. This varies from the traditional User
and Post
models used in other parts of the docs, mainly because it is unlikely you will be running integration tests against your blog.
schema.prisma
1// Can have 1 customer2// Can have many order details3model CustomerOrder {4 id Int @id @default(autoincrement())5 createdAt DateTime @default(now())6 customer Customer @relation(fields: [customerId], references: [id])7 customerId Int8 orderDetails OrderDetails[]9}1011// Can have 1 order12// Can have many products13model OrderDetails {14 id Int @id @default(autoincrement())15 products Product @relation(fields: [productId], references: [id])16 productId Int17 order CustomerOrder @relation(fields: [orderId], references: [id])18 orderId Int19 total Decimal20 quantity Int21}2223// Can have many order details24// Can have 1 category25model Product {26 id Int @id @default(autoincrement())27 name String28 description String29 price Decimal30 sku Int31 orderDetails OrderDetails[]32 category Category @relation(fields: [categoryId], references: [id])33 categoryId Int34}3536// Can have many products37model Category {38 id Int @id @default(autoincrement())39 name String40 products Product[]41}4243// Can have many orders44model Customer {45 id Int @id @default(autoincrement())46 email String @unique47 address String?48 name String?49 orders CustomerOrder[]50}
The guide uses a singleton pattern for Prisma Client setup. Refer to the singleton docs for a walk through of how to set that up.
Add Docker to your project
With Docker and Docker compose both installed on your machine you can use them in your project.
- Begin by creating a
docker-compose.yml
file at your projects root. Here you will add a Postgres image and specify the environments credentials.
docker-compose.yml
1# Set the version of docker compose to use2version: '3.9'34# The containers that compose the project5services:6 db:7 image: postgres:138 restart: always9 container_name: integration-tests-prisma10 ports:11 - '5433:5432'12 environment:13 POSTGRES_USER: prisma14 POSTGRES_PASSWORD: prisma15 POSTGRES_DB: tests
Note: The compose version used here (
3.9
) is the latest at the time of writing, if you are following along be sure to use the same version for consistency.
The docker-compose.yml
file defines the following:
- The Postgres image (
postgres
) and version tag (:13
). This will be downloaded if you do not have it locally available. - The port
5433
is mapped to the internal (Postgres default) port5432
. This will be the port number the database is exposed on externally. - The database user credentials are set and the database given a name.
- To connect to the database in the container, create a new connection string with the credentials defined in the
docker-compose.yml
file. For example:
.env.test
1DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"
The above .env.test
file is used as part of a multiple .env
file setup. Checkout the using multiple .env files. section to learn more about setting up your project with multiple .env
files
- To create the container in a detached state so that you can continue to use the terminal tab, run the following command:
$docker-compose up -d
Next you can check that the database has been created by executing a
psql
command inside the container. Make a note of the container id.docker psHide CLI resultsCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES1322e42d833f postgres:13 "docker-entrypoint.s…" 2 seconds ago Up 1 second 0.0.0.0:5433->5432/tcp integration-tests-prisma
Note: The container id is unique to each container, you will see a different id displayed.
Using the container id from the previous step, run
psql
in the container, login with the created user and check the database is created:docker exec -it 1322e42d833f psql -U prisma testsHide CLI resultstests=# \lList of databasesName | Owner | Encoding | Collate | Ctype | Access privilegespostgres | prisma | UTF8 | en_US.utf8 | en_US.utf8 |template0 | prisma | UTF8 | en_US.utf8 | en_US.utf8 | =c/prisma +| | | | | prisma=CTc/prismatemplate1 | prisma | UTF8 | en_US.utf8 | en_US.utf8 | =c/prisma +| | | | | prisma=CTc/prismatests | prisma | UTF8 | en_US.utf8 | en_US.utf8 |(4 rows)
Integration testing
Integration tests will be run against a database in a dedicated test environment instead of the production or development environments.
The flow of operations
The flow for running said tests goes as follows:
- Start the container and create the database
- Migrate the schema
- Run the tests
- Destroy the container
Each test suite will seed the database before all the test are run. After all the tests in the suite have finished, the data from all the tables will be dropped and the connection terminated.
The function to test
The ecommerce application you are testing has a function which creates an order. This function does the following:
- Accepts input about the customer making the order
- Accepts input about the product being ordered
- Checks if the customer has an existing account
- Checks if the product is in stock
- Returns an "Out of stock" message if the product doesn't exist
- Creates an account if the customer doesn't exist in the database
- Create the order
An example of how such a function might look can be seen below:
create-order.ts
1import prisma from '../client'23export interface Customer {4 id?: number5 name?: string6 email: string7 address?: string8}910export interface OrderInput {11 customer: Customer12 productId: number13 quantity: number14}1516/**17 * Creates an order with customer.18 * @param input The order parameters19 */20export async function createOrder(input: OrderInput) {21 const { productId, quantity, customer } = input22 const { name, email, address } = customer2324 // Get the product25 const product = await prisma.product.findUnique({26 where: {27 id: productId,28 },29 })3031 // If the product is null its out of stock, return error.32 if (!product) return new Error('Out of stock')3334 // If the customer is new then create the record, otherwise connect via their unique email35 await prisma.customerOrder.create({36 data: {37 customer: {38 connectOrCreate: {39 create: {40 name,41 email,42 address,43 },44 where: {45 email,46 },47 },48 },49 orderDetails: {50 create: {51 total: product.price,52 quantity,53 products: {54 connect: {55 id: product.id,56 },57 },58 },59 },60 },61 })62}
The test suite
The following tests will check if the createOrder
function works as it should do. They will test:
- Creating a new order with a new customer
- Creating an order with an existing customer
- Show an "Out of stock" error message if a product doesn't exist
Before the test suite is run the database is seeded with data. After the test suite has finished a deleteMany
is used to clear the database of its data.
Using deleteMany
may suffice in situations where you know ahead of time how your schema is structured. This is because the operations need to be executed in the correct order according to how the model relations are setup.
However, this doesn't scale as well as having a more generic solution that maps over your models and performs a truncate on them. For those scenarios and examples of using raw SQL queries see Deleting all data with raw SQL / TRUNCATE
__tests__/create-order.ts
1import prisma from '../src/client'2import { createOrder, Customer, OrderInput } from '../src/functions/index'34beforeAll(async () => {5 // create product categories6 await prisma.category.createMany({7 data: [{ name: 'Wand' }, { name: 'Broomstick' }],8 })910 console.log('✨ 2 categories successfully created!')1112 // create products13 await prisma.product.createMany({14 data: [15 {16 name: 'Holly, 11", phoenix feather',17 description: 'Harry Potters wand',18 price: 100,19 sku: 1,20 categoryId: 1,21 },22 {23 name: 'Nimbus 2000',24 description: 'Harry Potters broom',25 price: 500,26 sku: 2,27 categoryId: 2,28 },29 ],30 })3132 console.log('✨ 2 products successfully created!')3334 // create the customer35 await prisma.customer.create({36 data: {37 name: 'Harry Potter',38 email: 'harry@hogwarts.io',39 address: '4 Privet Drive',40 },41 })4243 console.log('✨ 1 customer successfully created!')44})4546afterAll(async () => {47 const deleteOrderDetails = prisma.orderDetails.deleteMany()48 const deleteProduct = prisma.product.deleteMany()49 const deleteCategory = prisma.category.deleteMany()50 const deleteCustomerOrder = prisma.customerOrder.deleteMany()51 const deleteCustomer = prisma.customer.deleteMany()5253 await prisma.$transaction([54 deleteOrderDetails,55 deleteProduct,56 deleteCategory,57 deleteCustomerOrder,58 deleteCustomer,59 ])6061 await prisma.$disconnect()62})6364it('should create 1 new customer with 1 order', async () => {65 // The new customers details66 const customer: Customer = {67 id: 2,68 name: 'Hermione Granger',69 email: 'hermione@hogwarts.io',70 address: '2 Hampstead Heath',71 }72 // The new orders details73 const order: OrderInput = {74 customer,75 productId: 1,76 quantity: 1,77 }7879 // Create the order and customer80 await createOrder(order)8182 // Check if the new customer was created by filtering on unique email field83 const newCustomer = await prisma.customer.findUnique({84 where: {85 email: customer.email,86 },87 })8889 // Check if the new order was created by filtering on unique email field of the customer90 const newOrder = await prisma.customerOrder.findFirst({91 where: {92 customer: {93 email: customer.email,94 },95 },96 })9798 // Expect the new customer to have been created and match the input99 expect(newCustomer).toEqual(customer)100 // Expect the new order to have been created and contain the new customer101 expect(newOrder).toHaveProperty('customerId', 2)102})103104it('should create 1 order with an existing customer', async () => {105 // The existing customers email106 const customer: Customer = {107 email: 'harry@hogwarts.io',108 }109 // The new orders details110 const order: OrderInput = {111 customer,112 productId: 1,113 quantity: 1,114 }115116 // Create the order and connect the existing customer117 await createOrder(order)118119 // Check if the new order was created by filtering on unique email field of the customer120 const newOrder = await prisma.customerOrder.findFirst({121 where: {122 customer: {123 email: customer.email,124 },125 },126 })127128 // Expect the new order to have been created and contain the existing customer with an id of 1 (Harry Potter from the seed script)129 expect(newOrder).toHaveProperty('customerId', 1)130})131132it("should show 'Out of stock' message if productId doesn't exit", async () => {133 // The existing customers email134 const customer: Customer = {135 email: 'harry@hogwarts.io',136 }137 // The new orders details138 const order: OrderInput = {139 customer,140 productId: 3,141 quantity: 1,142 }143144 // The productId supplied doesn't exit so the function should return an "Out of stock" message145 await expect(createOrder(order)).resolves.toEqual(new Error('Out of stock'))146})
Running the tests
This setup isolates a real world scenario so that you can test your applications functionality against real data in a controlled environment.
You can add some scripts to your projects package.json
file which will setup the database and run the tests, then afterwards manually destroy the container.
package.json
1 "scripts": {2 "docker:up": "docker-compose up -d",3 "docker:down": "docker-compose down",4 "test": "yarn docker:up && yarn prisma migrate deploy && jest -i"5 },
The test
script does the following:
- Runs
docker-compose up -d
to create the container with the Postgres image and database. - Applies the migrations found in
./prisma/migrations/
directory to the database, this creates the tables in the container's database. - Executes the tests.
Once you are satisfied you can run yarn docker:down
to destroy the container, its database and any test data.