Week 13

Introduction to Testing

Testing React Components with RTL

Chapter 3

Practice

Assignment

Front end Track

Why do we test?

You've been building your portfolio for several weeks. It works, you've tested it manually, you've refreshed the browser dozens of times. So why would you need automated tests?

Here's a scenario: you refactor your filterProjects utility to support a new feature. You think nothing is broken. But three components silently depend on the exact output shape of that function, and now two of them render nothing. You find out when a reviewer points it out; or worse, when a recruiter opens your portfolio.

Tests are your safety net. They catch regressions before they reach production, give you confidence to refactor, and document how your code is supposed to behave.

<aside> ๐Ÿ’ก

Worth knowing: At most companies, writing tests is not optional. PRs without test coverage will not be merged. Starting to think in tests now is one of the most career-relevant habits you can build.

</aside>


Two layers of testing

Before writing a single line, let's establish the mental model you'll use all week.

Frontend applications have two distinct things worth testing, and they need different approaches.

1. Pure functions; unit tests

A pure function takes inputs and returns outputs with no side effects. These are your utility functions, data transformers, validators, and formatters. For these, you test inputs and outputs thoroughly; every edge case, every unexpected input, every boundary.

// A pure function โ€” perfect candidate for unit tests
function filterProjectsByTech(projects: Project[], tech: string): Project[] {
  return projects.filter(p => p.techStack.includes(tech))
}

2. React components; behavioural tests

Components render UI and respond to user interaction. Here, you test what the user sees and does, not how the component is wired internally. You don't care whether it uses useState or useReducer. You care that when a user submits an empty form, they see an error message.

// What we test: the behaviour the user experiences
// NOT: which hook is used, or how state is structured internally
test('shows error when form is submitted empty', async () => {
  // We'll write this in Chapter 2
})

<aside> ๐Ÿ’ก

Rule of thumb: If it's a function with inputs and outputs, unit test it thoroughly. If it's a component, test the behaviour a user would notice.

</aside>


Setting up Vitest

Vitest is a testing framework built specifically for Vite projects. It's fast, shares config with your existing Vite setup, and has a Jest-compatible API; most things you'll read about Jest apply here too.

Installation

In your portfolio project, run:

npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Package What it does
vitest The test runner
@testing-library/react Renders React components in tests
@testing-library/jest-dom Custom matchers like toBeInTheDocument()
@testing-library/user-event Simulates real user interactions
jsdom Simulates a browser DOM in Node.js

Configure Vitest

Open your vite.config.ts and add a test block:

/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
  },
})

The jsdom environment simulates a browser DOM in Node.js; this is what lets React Testing Library render your components during tests without a real browser.

Create a setup file

Create src/test/setup.ts:

import '@testing-library/jest-dom'

This registers the custom matchers from jest-dom globally, so you can use toBeInTheDocument(), toBeVisible(), toHaveValue(), and others in every test file without importing them manually.

Add test scripts

In your package.json:

"scripts": {
  "test": "vitest",
  "test:ui": "vitest --ui",
  "test:coverage": "vitest run --coverage"
}

<aside> ๐Ÿ’ก

vitest --ui opens a browser-based test dashboard. It's optional but genuinely useful once you have more than a handful of tests; you can see which passed, which failed, and re-run individual tests with a click.

</aside>

Verify the setup

Create src/test/setup.test.ts:

test('Vitest is working', () => {
  expect(1 + 1).toBe(2)
})

Run npm test. Green? You're ready.


Writing your first unit test

Let's write a real test for a real function. Your portfolio has a utility that filters projects by technology; let's test it properly.

The function

Create src/utils/filterProjects.ts:

export type Project = {
  id: number
  title: string
  techStack: string[]
}

export function filterProjectsByTech(projects: Project[], tech: string): Project[] {
  return projects.filter(p =>
    p.techStack.map(t => t.toLowerCase()).includes(tech.toLowerCase())
  )
}

The test file

Create src/utils/filterProjects.test.ts:

import { describe, it, expect } from 'vitest'
import { filterProjectsByTech, Project } from './filterProjects'

const projects: Project[] = [
  { id: 1, title: 'Portfolio', techStack: ['React', 'TypeScript'] },
  { id: 2, title: 'Weather App', techStack: ['React', 'TailwindCSS'] },
  { id: 3, title: 'Blog', techStack: ['Next.js', 'TypeScript'] },
]

describe('filterProjectsByTech', () => {
  it('returns projects that include the given tech', () => {
    const result = filterProjectsByTech(projects, 'React')
    expect(result).toHaveLength(2)
    expect(result[0].title).toBe('Portfolio')
  })

  it('is case-insensitive', () => {
    const result = filterProjectsByTech(projects, 'typescript')
    expect(result).toHaveLength(2)
  })

  it('returns an empty array when no projects match', () => {
    const result = filterProjectsByTech(projects, 'Vue')
    expect(result).toHaveLength(0)
  })

  it('returns an empty array when given an empty list', () => {
    const result = filterProjectsByTech([], 'React')
    expect(result).toHaveLength(0)
  })
})

What's happening here

<aside> ๐Ÿ’ก

Naming tests well matters. When a test fails, the first thing you read is the test name. "returns an empty array when no projects match" tells you exactly what broke. "test 3" tells you nothing.

</aside>

Notice we tested four distinct cases:

This is what thorough means for unit tests. You're not trying to hit every possible string; you're thinking about the categories of input that could behave differently.


Additional Resources

Videos

https://www.youtube.com/watch?v=XdDZKeM5_pQ&list=PL4cUxeGkcC9iyuClsf48SSgsJPBStHo7F

Reading


The HackYourFuture curriculum is licensed underย CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with โค๏ธ by the HackYourFuture community ยท Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.