Top-Down and Bottom-Up Testing: Test Drivers, Test Stubs & Examples
Imagine a seat-booking app where the screen, booking service, and database code are written by different people. Which part should we connect and test first? This one decision explains top-down and bottom-up integration testing.
In this chapter
First, what is being tested?
Suppose each module works alone. The booking screen accepts a seat number. The booking service applies a rule. The seat repository reads and updates stored seats. Yet the complete booking can still fail if these modules pass the wrong data to one another. Integration testing checks those connections and interactions. If you need the broader testing level first, read software testing in Software Engineering and how unit testing checks individual modules.
Top-down and bottom-up are integration strategies. They describe the order in which we connect modules. They do not describe functional versus performance testing, and neither replaces a test of the complete real system.
A higher module calls a lower module. This direction of the call helps you remember where the temporary replacement goes.
What is top-down integration testing?
In the top-down approach, we begin with the top-level control or user-facing module. We then connect its child modules, followed by their children, until the lower levels are included. Think of drawing a tree from the trunk towards the branches.
On day one, our booking screen is ready but the real booking service is unfinished. We still want to check what the screen displays when booking succeeds. So we give it a stub: a small replacement service that returns a predictable result. Later, we replace that stub with the real service. If the seat repository is still missing, we can temporarily stub the repository while checking the screen and service together.
Run the booking screen or top controller.
Use stubs for lower modules not yet available.
Connect real child modules one by one and check interactions.
Useful when: you want to see the main user flow and high-level control early. Challenge: a stub can hide a real lower-level problem because its answer is prearranged. Lower-level logic may get tested later.
What is bottom-up integration testing?
In the bottom-up approach, we begin with low-level modules, combine them into working groups, and move towards the modules that use them. Here, we can begin by checking the seat repository, then connect the booking service, and finally connect the screen.
But how do we run the repository if the booking service that normally calls it is not ready? A driver acts as a temporary caller. It supplies a seat ID, invokes the lower module, and checks the result. Once the real higher-level module is ready, the temporary driver for that integration path can be removed.
Test a seat repository or another lower module.
Use a driver to call it while its parent is absent.
Connect real services and finally the screen.
Useful when: core services and data-handling modules are ready first. Challenge: the full screen-to-database user flow is visible later, and writing temporary drivers takes effort.
Top-down: use a stub below
returns a planned answer
Test the screen while its real dependency is missing.
Bottom-up: use a driver above
makes the call
Test the repository while its real caller is missing.
Dashed purple boxes show temporary test code; solid green boxes show the real module being exercised.
Test stub vs test driver: the exact difference
A stub replaces something the tested module calls. It usually provides a controlled response. A driver replaces something that would call the tested module. It starts the test by providing input and making the call.
Memory trick: Stub sits below and supplies an answer. Driver sits above and drives the call. The word “above” or “below” refers to the module relationship in our diagram, not to where the test file happens to be saved.
For example, the booking screen asks a service, “Book A1.” If that service does not exist, the screen can call a stub that answers “success.” On the other hand, a repository waits for someone to call reserve("A1"). If its service does not exist, a driver makes that call and checks the return value.
One important limit: a stub returning “success” proves that the screen reacts properly to that controlled response. It does not prove that the real booking service can reserve a seat. That requires a later test with the real modules connected.
A small JavaScript example you can actually follow
Below, showBookingMessage represents the upper module. It calls a booking service. We first use a stub to check the screen’s response. Then we test a real lower-level repository with a driver. The two examples are independent so you can focus on the direction of each call.
function showBookingMessage(bookingService, seatId) {
const result = bookingService.book(seatId);
return result.success ? "Seat booked" : "Seat unavailable";
}
// The real booking service is not ready yet.
const bookingServiceStub = {
book: (seatId) => ({ success: true })
};
console.assert(
showBookingMessage(bookingServiceStub, "A1") === "Seat booked"
);
The stub is called by the screen function. The result is predictable, so we can check that the screen handles a successful response. In a later test, also try a stub that returns { success: false } to check the unavailable message.
function createSeatRepository() {
const availableSeats = new Set(["A1"]);
return {
reserve(seatId) {
if (!availableSeats.has(seatId)) return false;
availableSeats.delete(seatId);
return true;
}
};
}
// This test function acts as the temporary driver.
function testRepositoryDriver() {
const repository = createSeatRepository();
console.assert(repository.reserve("A1") === true);
console.assert(repository.reserve("A1") === false);
}
testRepositoryDriver();
Here the driver calls the real lower-level module. The first reservation succeeds; the second request for the same seat fails. Next, connect a real booking service to this repository, test them together, and finally connect the screen. console.assert normally stays silent when the condition is true; it reports a failed assertion when the result is wrong.
Testing-level nuance: the tiny repository example by itself checks a low-level component. It demonstrates what a driver does. The integration test begins when we connect and check interactions between real modules, such as booking service + repository. Similarly, a screen + stub alone cannot prove the real service interaction.
Top-down vs bottom-up integration testing
These are common patterns, not a rule that every top-down test can only contain stubs or every bottom-up test can only contain drivers. The actual test setup depends on which components are ready and what interaction you need to examine.
How do you choose an integration order?
Choose according to the architecture, available modules, and the risk you want to expose early. If the main workflow and interface matter most now, top-down can help. If reliable lower-level services are the main concern and they are already available, bottom-up can help. A team may also combine both directions and meet in the middle; this is often called a sandwich or hybrid approach.
Whatever the order, document the interfaces, use meaningful test inputs, replace temporary parts with real parts, and rerun relevant tests. For the larger context of how modules are arranged, see software design and architectural design. To place integration work within the development process, review the SDLC phases.
Exam-ready answer and quick revision
Top-down and bottom-up testing are approaches to incremental integration testing. Top-down testing begins with high-level modules and connects lower-level modules gradually. A test stub simulates a lower module that has not yet been developed. Bottom-up testing begins with lower-level modules and moves upward. A test driver simulates a higher-level caller when that module is not yet available. Top-down reveals high-level flow early, whereas bottom-up allows low-level functionality to be examined early. After temporary code is replaced by real modules, their interactions must be tested again.
- Top → down → stub: missing module is below the tested caller.
- Bottom → up → driver: missing caller is above the tested module.
- Stub returns; driver calls.
- Real integration: check the interaction again after real modules replace temporary code.
In a viva, if someone asks, “Can a stub prove the database works?”, answer: No. It helps test the caller’s reaction to a chosen response. We still need an interaction test with the actual lower module. This distinction is also useful when studying verification and validation in software quality.
Frequently asked questions
Which approach uses test stubs?
Top-down integration testing commonly uses stubs for lower-level modules that are not yet available.
Which approach uses test drivers?
Bottom-up integration testing commonly uses drivers to call low-level modules before their higher-level callers are ready.
Are a stub and a driver the same?
No. A stub is called by the module under test and supplies a controlled response. A driver calls the module under test and supplies test input.
Does top-down mean testing is always better?
No. The suitable order depends on module readiness, architecture, and what failures the team needs to find early.
Can we use both approaches in one project?
Yes. Teams can develop and test from the top and bottom, then connect the tested groups. This is often called hybrid or sandwich integration testing.
Terminology reference: ISTQB syllabus (component integration, top-down/bottom-up, stubs and drivers). The booking examples and diagrams in this chapter are original teaching examples.
