Unit, Integration, and End-to-End Testing: Explained Through Pizza
I had trouble understanding automated testing until I thought about pizza
When I started my first software engineering role at Candlelit Therapy back in 2020, the only automated testing I was familiar with was unit testing. First I learned Chai and Mocha and then Jest.
After working at Teachers Pay Teachers which had a much larger and older code base and larger engineering teams, I was introduced to two other forms of automated testing: Integration and End-to-End Testing. When I first learned of these testing types, I thought, “So unit testing is like testing the toppings and integration or end-to-end testing is like taste-testing the pizza!”
If I’m honest with you, I was conflating integration and end-to-end testing. It wasn’t until I had an interview with MailChimp at Intuit that I realized they were different and I needed to take time to understand the three types of automated testing. I am really attached to the pizza metaphor so I’m going to try to explain with an expanded pizza metaphor.
Unit Testing is like testing the toppings
Imagine you’re a pizza chef and you want to design the best pizza. First, you start with testing your toppings. Are the tomatoes fresh? Is the cheese of good quality? When does the dough expire?
Now, come back to reality, you’re a software engineer again and you want to test your code for a new feature or even a Leetcode problem. You might first turn to Unit testing.
Unit Testing is like testing the toppings. You can think of toppings as the individual components of functions.
Take the classic two-sum problem: I want to build a function that has two parameters: an array of integers nums and an integer target, and then return indices of the two numbers such that they add up to the target.
If I use the hash table method using JavaScript, I might write my solution like this:
//index.js
function twoSum(nums, target) {
let vals = {};
for (let i = 0; i < nums.length; i++) {
if (target - nums[i] in vals) {
return [vals[target - nums[i]], i];
} else {
vals[nums[i]] = i;
}
}
return [];
}
To write my unit test in Jest to see if it works for an array of [3, 4, 5, 6]
and target of 7
, I might write it this way:
//index.test.is
const twoSum = require("./index");
test("Input: nums = [3,4,5,6], target = 7 Output: [0,1]", () => {
const nums = [3,4,5,6];
const target = 7;
const result = [0, 1];
expect(twoSum(nums, target)).toEqual(result);
})
You can also use unit testing to test components.
For example, if I create a like button for a social media app in React, I might write my component like this:
// LikeButton.jsx
import React, { useState } from "react";
const LikeButton = () => {
const [liked, setLiked] = useState(false);
const handleClick = () => {
setLiked((prevLiked) => !prevLiked);
};
return (
<button onClick={handleClick}>
{liked ? "Liked!" : "Like"}
</button>
);
};
export default LikeButton;
And if I write my tests in Jest, I might write this:
// LikeButton.test.jsx
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import LikeButton from "./LikeButton";
describe("LikeButton Component", () => {
test("renders the button with 'Like' text initially", () => {
render(<LikeButton />);
const button = screen.getByRole("button", { name: "Like" });
expect(button).toBeInTheDocument();
});
test("toggles the button text between 'Like' and 'Liked!' when clicked", () => {
render(<LikeButton />);
const button = screen.getByRole("button", { name: "Like" });
// Simulate the first click (Like -> Liked!)
fireEvent.click(button);
expect(button).toHaveTextContent("Liked!");
// Simulate the second click (Liked! -> Like)
fireEvent.click(button);
expect(button).toHaveTextContent("Like");
});
});
I want my like button to have the name “Like” when it has not been clicked. I want it to say “Liked” when it has. Unit tests look for that. So essentially, if we think about unit testing in terms of testing components, we can think about it as if we are testing the freshness of our tomatoes (i.e. looking for bruises or mold) or the quality of our cheese (i.e. What is the expiration date? Was it stored at the proper temperature? Is it made from a water buffalo’s milk or cow’s milk). Or even after you’ve made your tomato sauce, how does it taste?
For unit tests, we want to answer the question, does this particular function or component work the way we expect it to?
I like using Jest with JavaScript, Typescript, or React but here’s a list of other unit testing frameworks:
JUnit
PyUnit (Unittest)
Chai
Mocha
xUnit
Jasmine
There is a wide range of other testing libraries, so if you have any suggestions for other unit tests you like or any opinions about the ones I listed, please comment below!
An Integration Test is like testing the whole pizza
So, imagine you’re a chef again. You picked out the perfect ingredients. Congrats! Now you want to see how your ingredients come together after you’ve baked your pizza (Wow. I probably need to reward myself with a pizza after I’m done writing this.) So you taste test it or your pizza
You might ask:
Does the cheese melt properly on the sauce?
Does the dough stay crispy with these particular toppings?
Does the basil complement the tomatoes?
You're testing how components interact, but not necessarily the entire pizza-making experience and not the individual components separately.
As a software engineer, you can think of integration tests like a taste test. It’s like you’re testing how specific combinations of ingredients work together.
For example:
Testing a login process that involves:
A frontend UI sending credentials to the backend.
The backend querying a database for user authentication.
Returning a token to the frontend.
Verifying the integration of a payment processing system with an e-commerce application.
Ensuring API endpoints used by a mobile app function correctly with the backend.
These are possible frameworks for integration testing:
Cypress
Jest (yeah you can apparently write Integration tests in Jest too even thought it’s commonly associate with unit testing)
Jasmine (another repeat offender)
Cucumber
Fitnesse
Selenium
Postman for API testing
There are many more so again please comment with your favorites or opinions on the ones listed.
End-to-end tests like testing the complete pizza experience from order to delivery to digestion
Imagine you’re not just the pizza chef but you’re the owner of the pizza restaurant. You want test out the full dining experience or order-to-delivery experience of a customer. I actually don’t know if restaurant owners do this. But if they did, this would be sort of like what end-to-end testing is.
While integration tests focus on how components work together (ingredient combinations) and unit tests look at individual components and functions, end-to-end tests verify the entire system flow from start to finish (full pizza ordering and delivery and consumption experience).
I have honestly yet to build an automated end-to-end test, but I’ve seen one run and it’s amazing!
One of my startups used Cypress and you could see the actual buttons get pressed for real and pages load but automated and not manual. It's like testing an entire user journey through a web application!
End-to-end testing simulates real world scenarios like interactions with hardware, networks, or databases.
End-to-end tests tend to be more expensive so a team might use this for much bigger, much more important features. However, I have interviewed with some companies that say they run end-to-end tests for everything (and some that don’t use them, or any automated testing at all).
Popular End-to-end testing frameworks:
Selenium
Cypress
TestComplete
RobotFramework
Protractor
Once again, if you have experience with these or other libraries, please comment with your opinions. I would love to learn more! Especially on end-to-end testing as I have the least amount of experience there.
Thank you for reading and again, please comment!