Integration Tests Using Cypress Next and Prisma

In this article, I’ll walk you through setting up a Next + Prisma + Cypress testing environment. My goal is to keep things as simple as possible, so we’ll use SQLite as the database and React with inline styles for the frontend. Create a New Next.js Project Run the following command to create a Next.js app: npx create-next-app@latest Press Enter on all prompts to accept the default settings. We’ll be using JavaScript (not TypeScript) and won’t include Tailwind CSS. Once the setup is complete, open the project in your IDE (e.g., VS Code). Install Prisma Next, install Prisma as a development dependency: npm install prisma --save-dev Then, initialize Prisma with SQLite as the database provider: npx prisma init --datasource-provider sqlite Configure the Database After running the command, a .env file will be created in your project. Open it and you’ll see the database connection string: DATABASE_URL="file:./dev.db" This tells Prisma to use a local SQLite database file (dev.db). Define the Database Schema Open the prisma/schema.prisma file and replace its contents with the following: generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Book { id Int @id @default(autoincrement()) title String @unique author String? } This defines a simple Book model with: An id that auto-increments. A title field that must be unique. An optional author field. Run Initial Migration To apply the schema to your database, run: npx prisma migrate dev --name init This will create the database file (dev.db) and apply the initial schema. Create Server Actions for Books First, create a new file to handle database interactions: /lib/actions.js 'use server'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export const createBook = async (data) => { return await prisma.book.create({ data }); }; export const getBooks = async () => { return await prisma.book.findMany({}); }; export const deleteBook = async (id) => { return await prisma.book.delete({ where: { id } }); }; Clean Up Next.js Default Files To simplify the project, delete all files inside the /app/ directory except page.js and layout.js. Create a Simple Books Interface /app/books.jsx 'use client'; import { createBook, deleteBook } from '@/lib/actions'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; const Book = ({ book, destroy }) => ( X   Title: {book.title}, Author: {book.author} ); const Books = ({ books }) => { const [title, setTitle] = useState(''); const [author, setAuthor] = useState(''); const [error, setError] = useState(false); const { refresh } = useRouter(); const create = async () => { try { await createBook({ title, author }); setError(false); refresh(); } catch (e) { setError(true); } }; const destroy = async (id) => { await deleteBook(id); refresh(); }; return ( setTitle(e.target.value)} name="title" /> Title setAuthor(e.target.value)} name="author" /> Author {error && Invalid data provided} Add new book {books.map((book) => ( destroy(book.id)} key={book.id} /> ))} ); }; export default Books; Connect UI to the Server /app/page.js import { getBooks } from '@/lib/actions'; import Books from './books'; const Home = async () => { const books = await getBooks(); return ( Books ); }; export default Home; Update the Root Layout /app/layout.js export default function Layout({ children }) { return ( {children} ); } Running the Application To start your app, run the following command: npm run dev This will launch your Next.js application at http://localhost:3000. In this simple form, you can easily add and remove books. The model makes sure the title is unique and isn’t left blank. If anything goes wrong, you’ll see an error message. Installing and Setting Up Cypress To add Cypress to your project, run the following command: npm install cypress --save-dev Next, initialize Cypress and generate the default files and folders by running: npx cypress open When the Cypress window appears: Select E2E Testing as your default configuration. Click Continue to confirm the setup. Once the setup is complete, close both the Next.js development server and the Cypress window for now. Setting Up the Test Environment To ensure our tests run safely and efficiently, setting up a dedicated test environment is essential. Using a separate test database helps protect your development data from accidental changes, as tests often c

Mar 18, 2025 - 00:08
 0
Integration Tests Using Cypress Next and Prisma

In this article, I’ll walk you through setting up a Next + Prisma + Cypress testing environment. My goal is to keep things as simple as possible, so we’ll use SQLite as the database and React with inline styles for the frontend.

Create a New Next.js Project

Run the following command to create a Next.js app:

npx create-next-app@latest

Press Enter on all prompts to accept the default settings. We’ll be using JavaScript (not TypeScript) and won’t include Tailwind CSS.

Once the setup is complete, open the project in your IDE (e.g., VS Code).

Install Prisma

Next, install Prisma as a development dependency:

npm install prisma --save-dev

Then, initialize Prisma with SQLite as the database provider:

npx prisma init --datasource-provider sqlite

Configure the Database

After running the command, a .env file will be created in your project. Open it and you’ll see the database connection string:

DATABASE_URL="file:./dev.db"

This tells Prisma to use a local SQLite database file (dev.db).

Define the Database Schema

Open the prisma/schema.prisma file and replace its contents with the following:

generator client {
  provider = "prisma-client-js"
}

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

model Book {
  id     Int     @id @default(autoincrement())
  title  String  @unique
  author String?
}

This defines a simple Book model with:

  • An id that auto-increments.
  • A title field that must be unique.
  • An optional author field.

Run Initial Migration

To apply the schema to your database, run:

npx prisma migrate dev --name init

This will create the database file (dev.db) and apply the initial schema.

Create Server Actions for Books

First, create a new file to handle database interactions:

/lib/actions.js

'use server';

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const createBook = async (data) => {
  return await prisma.book.create({ data });
};

export const getBooks = async () => {
  return await prisma.book.findMany({});
};

export const deleteBook = async (id) => {
  return await prisma.book.delete({ where: { id } });
};

Clean Up Next.js Default Files

To simplify the project, delete all files inside the /app/ directory except page.js and layout.js.

Create a Simple Books Interface

/app/books.jsx

'use client';

import { createBook, deleteBook } from '@/lib/actions';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

const Book = ({ book, destroy }) => (
  
  •   Title: {book.title}, Author: {book.author}
  • ); const Books = ({ books }) => { const [title, setTitle] = useState(''); const [author, setAuthor] = useState(''); const [error, setError] = useState(false); const { refresh } = useRouter(); const create = async () => { try { await createBook({ title, author }); setError(false); refresh(); } catch (e) { setError(true); } }; const destroy = async (id) => { await deleteBook(id); refresh(); }; return (
    setTitle(e.target.value)} name="title" />
    setAuthor(e.target.value)} name="author" />
    {error &&

    Invalid data provided}

      {books.map((book) => ( destroy(book.id)} key={book.id} /> ))}
    ); }; export default Books;

    Connect UI to the Server

    /app/page.js

    import { getBooks } from '@/lib/actions';
    import Books from './books';
    
    const Home = async () => {
      const books = await getBooks();
    
      return (
        <>
          

    Books

    ); }; export default Home;

    Update the Root Layout

    /app/layout.js

    export default function Layout({ children }) {
      return (
        
          {children}
        
      );
    }
    

    Running the Application

    To start your app, run the following command:

    npm run dev

    This will launch your Next.js application at http://localhost:3000.

    Simple UI

    In this simple form, you can easily add and remove books. The model makes sure the title is unique and isn’t left blank. If anything goes wrong, you’ll see an error message.

    Installing and Setting Up Cypress

    To add Cypress to your project, run the following command:

    npm install cypress --save-dev

    Next, initialize Cypress and generate the default files and folders by running:

    npx cypress open

    When the Cypress window appears:

    Cypress UI

    Select E2E Testing as your default configuration.
    Click Continue to confirm the setup.

    Once the setup is complete, close both the Next.js development server and the Cypress window for now.

    Setting Up the Test Environment

    To ensure our tests run safely and efficiently, setting up a dedicated test environment is essential. Using a separate test database helps protect your development data from accidental changes, as tests often create, update, or delete records. This isolation also ensures tests start with a clean state, making them more consistent and reliable.

    Step 1: Install dotenv-cli

    We'll use dotenv-cli to manage environment variables for our test setup.

    Run the following command:

    npm install dotenv-cli --save-dev

    Step 2: Create a .env.test File

    Next, create a new file called .env.test in your project's root directory.

    Add the following content to define your test database URL:

    DATABASE_URL="file:./test.db"
    

    Since we're using SQLite, this configuration points to a separate database file called test.db — keeping your development data safe.

    Creating a Database Cleaner

    Since we currently have only one model (Book), we’ll clean just that for now. As more models are added, this cleaner can be expanded.

    Create the file /cypress/tasks/clean.js:

    const { PrismaClient } = require('@prisma/client');
    
    const prisma = new PrismaClient();
    
    const clean = async () => {
      return await prisma.book.deleteMany({});
    };
    
    module.exports = clean;
    
    

    Since we don’t use TypeScript, we must follow the CommonJS module system (e.g., module.exports) instead of ES module syntax (e.g., export default).

    Creating a Test Data Factory

    We'll create a simple task for adding test data. In our tests, we'll pass an object to this task to generate the necessary database entities.

    Create the file /cypress/tasks/createTestBook.js:

    const { PrismaClient } = require('@prisma/client');
    
    const prisma = new PrismaClient();
    
    const createTestBook = async ({
      title = 'Death Note',
      author = 'unknown',
    } = {}) => {
      return await prisma.book.create({
        data: { title, author },
      });
    };
    
    module.exports = createTestBook;
    
    

    Adding Tasks to Cypress Configuration

    Now, let’s register these tasks in the Cypress config file.

    /cypress.config.js:

    const { defineConfig } = require("cypress");
    const clean = require('./cypress/tasks/clean');
    const createTestBook = require('./cypress/tasks/createTestBook');
    
    module.exports = defineConfig({
      e2e: {
        baseUrl: 'http://localhost:3001',
        setupNodeEvents(on, config) {
          on('task', {
            clean,
            createTestBook,
          });
        },
      },
    });
    
    

    Adding Scripts to package.json

    We need some helpful scripts to simplify the process of:

    • Running the server on a separate port with the test environment.
    • Running Cypress tests.
    • Applying migrations on the test database.

    In your package.json, add the following scripts:

    "test:cypress": "dotenv -e .env.test -- cypress open",
    "test:server": "dotenv -e .env.test -- next dev -p 3001",
    "test:migrate": "dotenv -e .env.test -- npx prisma migrate deploy"
    

    Creating the Test Database

    Finally, run the following command to apply your migrations to the test database:

    npm run test:migrate

    This will create the test.db file, ensuring your test environment is properly configured.

    Running the Test Server and Cypress

    We need to run the test server and Cypress simultaneously. To do this, open two separate terminals and run the following commands:

    npm run test:server

    npm run test:cypress

    Creating the First Test

    Now let's write our first Cypress test.

    Create the file /cypress/e2e/books.cy.js and add the following code:

    describe('Book', () => {
      beforeEach(() => {
        cy.task('clean')
        cy.task('createTestBook')
        cy.task('createTestBook', { title: 'Memoirs of a Geisha' })
        cy.visit('/')
      })
    
      it('can be created', () => {
        cy.get('[name="title"]').type('Necronomicon')
        cy.get('[name="author"]').type('Abdul Alhazred')
        cy.contains('Add new book').click()
        cy.contains('Title: Necronomicon').should('exist')
      })
    })
    
    

    What Happens in This Test?

    • The test database is cleaned before each test run.
    • Two books are created — one with the default name and one titled Memoirs of a Geisha.
    • The test visits the homepage.
    • It types a new book's title and author.
    • The new book is added to the database, and the test checks that it’s displayed on the page.

    Running the Test

    With both the test server and Cypress running, you should see something like this:

    Successful run

    That’s it — it works!

    Adding More Tests

    Now let's expand our test suite by adding two more specs — one to test invalid input and another to test book deletion.

    Here’s the updated /cypress/e2e/books.cy.js file:

    describe('Book', () => {
      beforeEach(() => {
        cy.task('clean')
        cy.task('createTestBook')
        cy.task('createTestBook', { title: 'Memoirs of a Geisha' })
        cy.visit('/')
      })
    
      it('can be created', () => {
        cy.get('[name="title"]').type('Necronomicon')
        cy.get('[name="author"]').type('Abdul Alhazred')
        cy.contains('Add new book').click()
        cy.contains('Title: Necronomicon').should('exist')
      })
    
      it('can be deleted', () => {
        cy.contains('X').click()
        cy.contains('Title: Death Note').should('not.exist')
        cy.contains('X').click()
        cy.contains('Title: Memoirs of a Geisha').should('not.exist')
      })
    
      it('cannot be created with an invalid title', () => {
        cy.get('[name="title"]').type('Death Note')
        cy.contains('Add new book').click()
        cy.contains('Invalid data provided').should('exist')
      })
    })
    
    

    So our tests check the following things:

    • Book Creation: Verifies that a new book can be added successfully.
    • Book Deletion: Ensures books can be removed from the database.
    • Invalid Input Handling: Confirms that attempting to create a book with an existing title shows an error message.

    Running the Tests

    With both the test server and Cypress running, check the Cypress UI to confirm that all tests pass successfully.

    Successful tests run

    Wrapping Up

    In this article, we’ve built a simple yet effective testing setup using Next, Prisma, and Cypress. While this is a basic configuration, it provides a solid foundation that can be expanded to meet your project's specific needs.

    Feel free to reach out if you have any questions or suggestions. I'd love to hear your thoughts!