Published 2 months ago

Robust Integration Testing with Next.js, Prisma, and Cypress

Software Development
Robust Integration Testing with Next.js, Prisma, and Cypress

Robust Integration Testing with Next.js, Prisma, and Cypress

This article details setting up a comprehensive testing environment for a Next.js application using Prisma for database interaction and Cypress for end-to-end testing. We'll leverage SQLite for simplicity, focusing on clear, practical steps for both beginners and experienced developers.

Setting Up the Next.js Project

Begin by creating a new Next.js application:

npx create-next-app@latest

Accept the default settings. We'll use JavaScript and forgo Tailwind CSS for this example.

Integrating Prisma

Install Prisma as a development dependency:

npm install prisma --save-dev

Initialize Prisma with SQLite:

npx prisma init --datasource-provider sqlite

This creates a .env file containing the database connection string:

DATABASE_URL="file:./dev.db"

Defining the Database Schema

Open prisma/schema.prisma and define your data model. Here, we create a simple Book model:

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?
}

Initial Database Migration

Apply the schema to your database:

npx prisma migrate dev --name init

Server Actions for Database Interactions

Create /lib/actions.js to handle database operations:

'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 } });
};

Simplifying the Next.js UI

For simplicity, remove all files from the /app/ directory except page.js and layout.js.

Creating the Books Interface

Create /app/books.jsx to build the user interface for managing books:

'use client';

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

const Book = ({ book, destroy }) => (
  <li>
    <button onClick={destroy}>X</button>  
    <b>Title:</b> {book.title}, <b>Author:</b> {book.author}
  </li>
);

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 (
    <main>
      <div>
        <input onChange={(e) => setTitle(e.target.value)} name="title" />
        <label> Title </label>
      </div>
      <div>
        <input onChange={(e) => setAuthor(e.target.value)} name="author" />
        <label> Author </label>
      </div>
      {error && <p style={{ color: 'red' }}>Invalid data provided</p>}
      <br />
      <button onClick={create}>Add new book</button>
      <ul>
        {books.map((book) => (
          <Book book={book} destroy={() => destroy(book.id)} key={book.id} />
        ))}
      </ul>
    </main>
  );
};

export default Books;

Connecting the UI to the Server

In /app/page.js, fetch and display the books:

import { getBooks } from '@/lib/actions';
import Books from './books';

const Home = async () => {
  const books = await getBooks();

  return (
    <>
      <h1>Books</h1>
      <Books books={books} />
    <>
  );
};

export default Home;

Updating the Root Layout

Create or update /app/layout.js:

export default function Layout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Running the Application

Start the Next.js application:

npm run dev

Access the application at http://localhost:3000.

/uploads/image_6fddc9f525.png

Setting Up Cypress for End-to-End Testing

Install Cypress:

npm install cypress --save-dev

Initialize Cypress:

npx cypress open

Select E2E testing and follow the setup instructions. Close both the Next.js and Cypress windows after the setup is complete.

/uploads/image_76d3c0793b.png

Configuring the Test Environment

Using a separate test database is crucial for robust testing. It prevents accidental data modification and ensures test consistency.

Installing dotenv-cli

Install dotenv-cli:

npm install dotenv-cli --save-dev

Creating .env.test

Create a .env.test file with the following content:

DATABASE_URL="file:./test.db"

Database Cleaner and Test Data Factory

Create /cypress/tasks/clean.js to clear the test database:

const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

const clean = async () => {
  return await prisma.book.deleteMany({});
};

module.exports = clean;

Create /cypress/tasks/createTestBook.js to add test data:

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;

Registering Cypress Tasks

Update cypress.config.js to register the tasks:

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 npm Scripts

Add these scripts to package.json:

{
  "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 and Migrating the Test Database

Run the migration for the test database:

npm run test:migrate

Running the Test Server and Cypress

Run npm run test:server in one terminal and npm run test:cypress in another.

Writing Your First Cypress Test

Create /cypress/e2e/books.cy.js:

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')
  })
})

This test cleans the database, adds two books, visits the homepage, adds a new book, and verifies its presence.

Expanding the Test Suite

Expand /cypress/e2e/books.cy.js with more tests for deletion and invalid input handling:

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')
  })
})
/uploads/image_7042a17041.png /uploads/image_dae2f34f84.png

Conclusion

This guide provides a solid foundation for integration testing in a Next.js, Prisma, and Cypress environment. Remember to adapt this setup to your specific project requirements and expand your test suite to cover all critical functionalities.

Hashtags: #Nextjs # Prisma # Cypress # IntegrationTesting # EndToEndTesting # E2ETesting # JavaScript # Testing # SoftwareTesting # DatabaseTesting # Sqlite # Nodejs

Related Articles

thumb_nail_Unveiling the Haiku License: A Fair Code Revolution

Software Development

Unveiling the Haiku License: A Fair Code Revolution

Dive into the innovative Haiku License, a game-changer in open-source licensing that balances open access with fair compensation for developers. Learn about its features, challenges, and potential to reshape the software development landscape. Explore now!

Read More
thumb_nail_Leetcode - 1. Two Sum

Software Development

Leetcode - 1. Two Sum

Master LeetCode's Two Sum problem! Learn two efficient JavaScript solutions: the optimal hash map approach and a practical two-pointer technique. Improve your coding skills today!

Read More
thumb_nail_The Future of Digital Credentials in 2025: Trends, Challenges, and Opportunities

Business, Software Development

The Future of Digital Credentials in 2025: Trends, Challenges, and Opportunities

Digital credentials are transforming industries in 2025! Learn about blockchain's role, industry adoption trends, privacy enhancements, and the challenges and opportunities shaping this exciting field. Discover how AI and emerging technologies are revolutionizing identity verification and workforce management. Explore the future of digital credentials today!

Read More
Your Job, Your Community
logo
© All rights reserved 2024