DEVELOPMENT by Pedro Resende

Understanding Test-Driven Development (TDD)

and its Application in Next.js

Post by Pedro Resende on the 30 of March of 2024 at 19:00

TDD Cycle banner

Today I've decided to write a little blog post, about what Test-Driven Development (TDD), and how to apply it on a Next.js application, we'll explore what TDD is and how to apply it specifically in the context of building applications with Next.js, a popular React framework for building server-side rendered and static websites.

Test-Driven Development (TDD) is a software development methodology that prioritizes writing tests before writing the actual code. It follows a simple cycle: write a failing test, write the minimum code to pass the test, refactor the code to improve its design without changing its behavior, and repeat.

What is Test-Driven Development?

TDD is a development process where developers write tests for a feature before writing the code to implement that feature. The cycle typically consists of three steps:

  1. Write a failing test: Start by writing a test that specifies the behavior you want from your code. Since you haven't implemented the feature yet, this test should fail initially.

  2. Write the minimum code to pass the test: Implement the feature just enough to make the test pass. You're not concerned with writing perfect or optimized code at this stage; the goal is to make the test green.

  3. Refactor the code: Once the test passes, refactor your code to improve its design, readability, and performance while ensuring that all tests still pass. This step ensures that your codebase remains maintainable and scalable.

Applying TDD in Next.js

Next.js is a powerful framework for building React applications, providing features like server-side rendering, static site generation, and API routes out of the box. Here's how you can apply TDD principles in Next.js development:

1. Set Up Your Project

Start by setting up a new Next.js project using create-next-app or any other preferred method.

npx create-next-app my-next-app
cd my-next-app

2. Install Testing Dependencies

Next.js works well with popular testing libraries like Jest and React Testing Library. Install these dependencies along with any other testing utilities you may need:

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @types/jest
npm init jest@latest

Edit you jest.config.ts|js file and replace the content by the following

import type { Config } from 'jest'
import nextJest from 'next/jest.js'
 
const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})
 
// Add any custom config to be passed to Jest
const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
 
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

Finally to run the tests, run the command npm run test.

3. Write Tests for Your Components or Features

Following TDD principles, start by writing tests for the components or features you want to implement. These tests should define the expected behavior of your code.

// Example test file: components/Button.test.tsx

import { act, render, screen } from "@testing-library/react"
import "@testing-library/jest-dom"
import Button from "./Button"

describe("Button", () => {
  it("should render correctly", () => {
    render(<Button>Hello</Button>)

    const button = screen.getByText("Hello")

    expect(button).toBeInTheDocument()
  })
})

// Write more tests for different scenarios...

4. Write the Minimum Code to Pass the Tests

Once you have failing tests in place, implement the minimum code necessary to make those tests pass. This may involve creating or updating components, pages, or utility functions.

// Example component: components/Button.tsx

type ButtonProps = {
  children: React.ReactElement
}

const Button = ({ children }: ButtonProps) => (
  <button>{children}</button>
)

export default Button

5. Refactor and Iterate

After the tests pass, take the time to refactor your code. This step ensures that your code remains clean, maintainable, and scalable. Refactoring may involve optimizing performance, improving code readability, or removing duplication.

6. Repeat the Cycle

Continue the TDD cycle by writing more tests for additional features or edge cases, implementing the minimum code to make those tests pass, and refactoring as needed. This iterative process helps ensure that your codebase remains robust and reliable over time.

A good example of a second test could be

  it('should trigger onClick correctly', () => {
    const onClick = jest.fn()

    render(<Button onClick={onClick}>Hello</Button>)

    const button = screen.getByText('Hello')

    act(() => {
      fireEvent.click(button)
    })

    expect(onClick).toHaveBeenCalled()
  })

and the production code, would update to

type ButtonProps = {
  onClick: () => void
  children: React.ReactElement
}

const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
)

export default Button

Benefits of TDD in Next.js Development

Applying TDD principles in Next.js development offers several benefits:

  • - Faster Feedback: TDD encourages writing tests early in the development process, providing immediate feedback on the correctness of your code.
  • - Improved Code Quality: By focusing on writing tests and implementing only the necessary code, TDD often leads to cleaner, more modular codebases.
  • - Better Design: TDD promotes a design-first approach, where you think about the interface and behavior of your code before writing implementation details.
  • - Increased Confidence: With comprehensive test coverage, you can refactor code with confidence, knowing that existing functionality won't break unexpectedly.

Conclusion

Test-Driven Development is a powerful methodology for building robust, maintainable software. When applied to Next.js development, TDD can help you write better code, catch bugs early, and deliver high-quality applications with confidence. By following the TDD cycle—write failing tests, write minimum code to pass tests, and refactor—you can streamline your development process and create more reliable Next.js applications.