You are building our blogging app with Next.js, TypeScript, and React. You’ve got a function to format dates, a component to display posts, and an API to fetch them. You write a test, run it, and it passes—great! But then someone asks, “Is that a unit test, an integration test, or an end-to-end test?” Suddenly, you’re not so sure. Understanding the differences is key to testing effectively, saving time, and catching bugs where they matter most. Today, we’ll break it down using our blogging app, so you’ll know exactly what you’re testing and why it matters—especially in a CI/CD workflow.
You might think, “A test is a test, right? If it works, who cares what kind it is?” Not quite! Each type has a unique purpose, and mixing them up can lead to slow, confusing, or incomplete tests. Let’s explore this step-by-step with our app as the example.
Unit testing focuses on testing one thing at a time in complete isolation—like a single function or a React component. It’s like checking if a single Lego brick is the right shape before building a castle.
For our blogging app, let’s test the formatDate
function:
// utils/date.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
}
// tests/date.test.ts
import { describe, it, expect } from "vitest";
import { formatDate } from "../utils/date";
describe("formatDate", () => {
it("formats a valid date correctly", () => {
const date = new Date("2025-04-07");
expect(formatDate(date)).toBe("April 7, 2025");
});
it("handles edge cases like invalid dates", () => {
const invalidDate = new Date("invalid");
expect(formatDate(invalidDate)).toBe("Invalid Date");
});
});
formatDate
, nothing else. No API calls, no components—just the function.Now, a React component example with React Testing Library:
// components/PostPreview.tsx
import { formatDate } from "../utils/date";
interface PostPreviewProps { title: string; date: Date; }
export function PostPreview({ title, date }: PostPreviewProps) {
return (
<div>
<h2>{title}</h2>
<p>{formatDate(date)}</p>
</div>
);
}
// tests/PostPreview.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { PostPreview } from "../components/PostPreview";
describe("PostPreview", () => {
it("renders title and formatted date", () => {
render(<PostPreview title="My First Post" date={new Date("2025-04-07")} />);
expect(screen.getByText("My First Post")).toBeDefined();
expect(screen.getByText("April 7, 2025")).toBeDefined();
});
});
PostPreview
component alone, passing it props directly. No fetching data, no parent components—just the component itself.Ask yourself:
Integration testing steps up a level, checking how multiple units work together. It’s like testing if a few Lego bricks snap together properly before building the whole castle.
Let’s test if PostPreview
works with a mocked API fetch in a PostList
component:
// components/PostList.tsx
import { useEffect, useState } from "react";
import { PostPreview } from "./PostPreview";
export function PostList() {
const [posts, setPosts] = useState<{ title: string; date: Date }[]>([]);
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then((data) => setPosts(data.map((p: any) => ({ ...p, date: new Date(p.date) }))));
}, []);
return (
<div>
{posts.map((post) => (
<PostPreview key={post.title} title={post.title} date={post.date} />
))}
</div>
);
}
// tests/PostList.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { PostList } from "../components/PostList";
describe("PostList", () => {
it("fetches and renders posts with PostPreview", async () => {
vi.spyOn(global, "fetch").mockResolvedValue({
json: () => Promise.resolve([{ title: "Test Post", date: "2025-04-07" }]),
} as any);
render(<PostList />);
expect(await screen.findByText("Test Post")).toBeDefined();
expect(await screen.findByText("April 7, 2025")).toBeDefined();
});
});
PostList
(which fetches data) and PostPreview
(which displays it) together. The API is mocked, but the test checks their interaction.Ask yourself:
E2E testing is the big picture—it’s like testing the entire Lego castle to see if it stands up. For our blogging app, we’ll use Playwright to simulate a user browsing posts and an admin adding one.
// tests/e2e/blog.spec.ts
import { test, expect } from "@playwright/test";
test("user views posts and admin adds a post", async ({ page }) => {
await page.goto("http://localhost:3000");
await expect(page.getByText("My First Post")).toBeVisible();
await expect(page.getByText("April 7, 2025")).toBeVisible();
await page.goto("http://localhost:3000/admin");
await page.fill("#title", "New Post");
await page.fill("#date", "2025-04-08");
await page.click("button[type='submit']");
await page.goto("http://localhost:3000");
await expect(page.getByText("New Post")).toBeVisible();
});
Ask yourself:
Let’s compare them using our blogging app example:
formatDate
alone or PostPreview
with fake props. Fast, isolated, no network.PostList
fetching mock data and passing it to PostPreview
. Checks interaction, still mocks external systems.fetch
or a database, it’s integration. Unit tests don’t touch external systems—they mock them.PostPreview
by fetching real data instead of passing props.In our CI/CD pipeline:
formatDate
fails).This layering builds confidence step-by-step without slowing down development.
Unit, integration, and E2E tests each have a job in our blogging app:
formatDate
or PostPreview
in isolation—fast and focused.PostList
with a mock API—checks connections.How to Know What You Wrote:
Key Takeaways:
With this clarity, you’ll write the right test for the right job, making our blogging app rock-solid!
Get ready to master testing! We’ll explore unit, integration, and E2E tests using NextJS, TypeScript, and React in our blogging app. Learn what to test, when, and how it fits CI/CD. With Vitest, Playwright, and React Testing Library, we’ll ensure our app shines—bug-free and user-ready!
You may ask yourself: why do I need automated tests? My code runs great!
Imagine you revisit your blog application and add a new feature, forgetting about what was done previously. A bug crashes it all! Testing prevents this, catching issues early and ensuring a smooth experience.
In this lecture, we’ll explore three testing types to protect our app: unit for small pieces, integration for connections, and E2E for the full user journey.
These tests automate quality checks in our CI/CD pipeline, so every change is safe and users making them accountable!
Let's explore our first type of test: The Unit Test.
Unit tests zoom in on a single function or component, like a magnifying glass. They’re fast and catch bugs in the smallest bits of our blogging app.
Let's take a look at an example. We test a formatDate function to ensure posts show dates like “April 7, 2025” correctly. You can run these while writing code to fix logic errors instantly.
This is the formatDate function in the date ts file that we will be tesing.
We are using the Vitest framework, which shares a very similar API with other unit testing frameworks such as Jest, Jasmine or Mocha.
We import the function that we wish to test. This function becomes the unit we are testing.
"Describe" function describes the unit tests. Most often, you specify what is the unit that you are testing.
The "it" function describes and specifies individual unit tests. Each "it" function should test a specific functionality or test case. Unit tests typically include multiple "it" functions. While we only observe one test, we can easily expand it to evaluate different international date variants.
Finally, within the "it" function, we present the body of the test, which utilises an assertion library to evaluate the unit. In this instance, we employ the "expect" library, which offers assertions in natural language, such as toBe or toBeHigherThan.
Let's talk about integration tests, where multiple teams have to work together.
Integration tests check how components collaborate, like fetching posts and showing them in our app.
In this integration test, we’ll mock an API and test if posts render properly in a list. Mocking replaces the original functionality with a custom implementation. You run integration tests after connecting parts together to ensure they play nice. They are still very fast, similar to unit tests.
This is the component that we are testing.
It uses fetch function to get data from our backend ...
... and uses the formatDate function we tested earlier.
Integration tests share the same structure as unit tests with "describe" and "it" functions.
We use Vitest's mocking mechanism to fake the response of the fetch function, providing our data. For mocking approaches, please check Vitest documentation.
Next, we utilise the render function from the React Testing Library to render the component, which will invoke this mocked fetch function and display the result.
Lastly, we utilise the screen from the React Testing Library, which enables us to find items based on various selectors, such as searching for a specific text. At this stage, we are not concerned with the rendered elements; we are only focused on ensuring that an expected text is displayed to the user.
Last, let's talk about end-to-end testing, also known as system testing.
End-to-end testing, also known as E2E, tests the whole app. E2E tests act like users, clicking through our app to verify everything works from start to finish.
In our E2E test, we'll test if users see posts and admins can add new ones.
You can see that E2E test perform user actions in the browser, such as navigating to a specific url ...
... filling a value in a form element ...
... or clicking on a button.
Also, similar to integration tests it allows you to search for elements on the displayed page and use assertion libraries to test for their properties. We will cover E2E testing in-dept later in this chapter.
So, when should you write and execute these thests?
Unit tests are the base—tons of them, super quick, testing small bits like formatDate. In CI/CD, run unit tests on every code push—fast feedback keeps bugs out early. Use unit tests for quick checks on functions and components—its's your first line of defense.
You will need fewer integration tests to check connections, like API to UI, with moderate speed. In CI/CD, execute unit tests on pull requests, ensuring features work together before merging. Integration tests ensure data moves smoothly between parts—this is vital for app cohesion.
E2E tests are the tip—slow but thorough, ensuring the whole app works. In CI/CD E2E tests run last, green-lighting deployment when the app’s fully functional. You tie them into CI/CD for a bulletproof dev process!
Let's explore a case study on how tests can help us fix issues. In this example a user reports that posts aren’t loading—let’s use our tests to fix it!
All unit tests pass—things like date formatting aren’t the issue.
All the integration tests pass! It seems that all of our components are working well, so where could the issue be?
In our app, we were using an external API that changed the format of the return data, breaking our application. The fix now requires adjusting multiple unit and integration tests to work with the new return data structure.