AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Integration Testing: Testing Multiple Components

📚 Testing⏱️ 23 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 23 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

When Two Correct Things Combine Into a Wrong Thing

Imagine three students are building a result-processing program for their school's CBSE report card system. Riya writes a function that adds up a student's marks across five subjects. Karan writes a function that converts that total into a percentage. Aditi writes a function that looks at the percentage and decides which grade band it falls into — A1, A2, B1, and so on. Each of them tests their own function carefully before showing it to the others. Riya feeds her function a list of marks and checks the sum by hand. Karan feeds his function a total and a maximum, and checks the percentage on a calculator. Aditi feeds her function a few sample percentages and checks that the right grade comes back. All three report, truthfully, "my function works — I tested it."

Then the three functions are wired together into one pipeline: total marks flow into the percentage calculator, and the percentage flows into the grade assigner. The program runs on a real student's marks. It prints the wrong grade. Nobody's function has a bug in it, in the sense that each one does exactly what its own tests say it should do. The problem only exists in the seam between two functions — one is silently assuming something the other doesn't provide. This is the exact situation integration testing exists to catch, and by the end of this chapter you will be able to reproduce this bug, trace it, write the test that catches it, and know how to systematically test any multi-component system before it reaches a seam-shaped surprise like this one.

What Exactly Is Integration Testing?

Before defining integration testing, it helps to be precise about the word "component" (also called a module or unit in this context). A component is any self-contained piece of code that does one job — a function, a class, or a small file of related functions — and that can, at least in principle, be tested on its own by handing it inputs and checking its outputs.

Unit testing is the practice of testing exactly one component in isolation, with every other component it depends on either absent or faked. A unit test for Karan's percentage function only cares whether that one function, given a total and a maximum, returns the correct percentage. It never runs Riya's or Aditi's code at all.

Integration testing is the practice of testing two or more components together, exactly as they will actually call each other in the running program, to check that the connections between them — not just the insides of each one — are correct. An integration test for the report card pipeline would call Riya's function, feed its real output into Karan's function, feed that real output into Aditi's function, and check that the final grade is correct. Nothing is fake here; every component is the genuine article, wired the way the finished program wires them.

The distinction matters because unit tests and integration tests are hunting for two structurally different kinds of bug. A unit test can only catch a bug inside a single function's logic. It is mathematically incapable of catching a bug that exists purely in the disagreement between two functions' assumptions, because a unit test for one function never even runs the other function. That disagreement only becomes visible the moment the two are connected — which is precisely what an integration test does and a unit test does not.

calculate_total() returns sum, e.g. 439 calculate_percentage() returns 0-100 scale e.g. 87.8 assign_grade() expects 0-1 scale check unit test: PASS check unit test: PASS check unit test: PASS scale mismatch here assign_grade(calculate_percentage(calculate_total(marks), 500)) integration test: FAIL got 'A1' — expected 'A2' each function is correct alone; wrong once connected

Tracing the Bug: A Worked Example

Let's build the exact program from the opening scenario and trace it line by line, the way you would step through code with a debugger. Here are the three functions, written the way three different people, each reasoning only about their own piece, might plausibly write them.

def calculate_total(marks_list):
    return sum(marks_list)

def calculate_percentage(total, max_marks):
    return (total / max_marks) * 100

def assign_grade(percentage):
    # written and unit-tested assuming percentage is a
    # fraction between 0 and 1, e.g. 0.88 for "88%"
    if percentage >= 0.90:
        return 'A1'
    elif percentage >= 0.80:
        return 'A2'
    elif percentage >= 0.70:
        return 'B1'
    elif percentage >= 0.60:
        return 'B2'
    else:
        return 'C1'

Now look at each person's unit tests, run in isolation exactly as unit tests are meant to be run.

# Riya's unit test for calculate_total
assert calculate_total([10, 20, 30]) == 60          # PASS

# Karan's unit test for calculate_percentage
assert calculate_percentage(450, 500) == 90.0        # PASS

# Aditi's unit test for assign_grade
assert assign_grade(0.95) == 'A1'                    # PASS
assert assign_grade(0.72) == 'B1'                    # PASS

Every single assertion above passes. Trace Aditi's second test by hand to confirm it: assign_grade(0.72) checks 0.72 >= 0.90 — false — then 0.72 >= 0.80 — false — then 0.72 >= 0.70 — true — so it returns 'B1', matching the assertion. Aditi's function is doing exactly what she designed and tested it to do. So are Riya's and Karan's. If you only ran unit tests and watched them all print PASS, you would have every reason to believe the report card program is correct.

Now write the integration test — the one that actually chains the three real functions together the way the finished program does, using one concrete student's marks: 88, 92, 79, 95, and 85 out of 100 each.

def test_marks_to_grade_pipeline():
    total = calculate_total([88, 92, 79, 95, 85])
    percentage = calculate_percentage(total, 500)
    grade = assign_grade(percentage)
    assert grade == 'A2', f"expected A2, got {grade}"

test_marks_to_grade_pipeline()

Trace this exactly as the interpreter would. calculate_total([88, 92, 79, 95, 85]) adds the five marks: 88 + 92 = 180, + 79 = 259, + 95 = 354, + 85 = 439. So total = 439. Next, calculate_percentage(439, 500) computes (439 / 500) * 100 = 0.878 * 100 = 87.8. So percentage = 87.8. A student with 87.8% should land in the A2 band (80% and above, below 90%). Now trace assign_grade(87.8): the very first check is 87.8 >= 0.90. Since 87.8 is a number on the 0-to-100 scale and 0.90 is a number on the 0-to-1 scale, 87.8 is enormously larger than 0.90, so this condition is true, and the function immediately returns 'A1' without ever looking at the other branches. The assertion grade == 'A2' fails: the program reports a false 'A1' for a student who actually scored in the A2 band.

Notice something stronger: this isn't a one-off edge case. Because calculate_percentage always returns a value between 0 and 100, and any such value except a genuine failing score near zero will satisfy percentage >= 0.90, assign_grade will return 'A1' for almost every student in the school, regardless of their real performance. A single mismatched assumption about scale — "is this a fraction or a percentage?" — silently breaks the entire grading pipeline, and it was invisible to all three unit tests because no unit test ever ran calculate_percentage's actual output through assign_grade.

Why This Kind of Bug Specifically Needs Integration Testing

The root cause here has a name: an interface mismatch. An interface is the agreement between two components about what data passes between them — its meaning, its units, its scale, its format. Riya's and Karan's functions agree on units perfectly well (both work in marks and then percent). The break is between Karan and Aditi: Karan's function hands over a percentage on a 0–100 scale, and Aditi's function was built and unit-tested against the unstated assumption of a 0–1 fraction. Neither person did anything wrong by the standard of their own tests. The bug exists only in the gap between two components, which is a place no unit test can look, because a unit test by definition studies one component with everything else stripped away.

Interface mismatches are common causes of integration bugs, but not the only kind. Others include: two components disagreeing on the order of operations (one expects to be initialised before the other runs), two components competing for the same shared resource (both trying to write to the same file or database row), and timing or sequencing bugs that only appear when components run together rather than one at a time. What all of these share is the same defining trait: each individual component is behaving correctly according to its own tests, and the fault lives strictly in how they interact.

Common Misconception: "All My Unit Tests Passed, So My Program Works"

This is one of the most common and most dangerous beliefs among students writing their first multi-file programs — for a CBSE Class 9 group project, for instance, where one teammate writes the input-handling code, another writes the calculation logic, and a third writes the output formatting. It feels reasonable: if every piece is individually verified, surely the whole must be correct too. The worked example above is a direct, traceable counter-example to that belief. Unit tests verify that a component honours its own contract when fed inputs that match what its author assumed. They cannot verify that the component actually receives inputs matching that assumption once it's wired into a larger system, because verifying that requires running the components together — which is, by definition, integration testing, not unit testing. A correct mental model is: unit testing checks that each brick is solid; integration testing checks that the bricks were laid so the wall actually stands.

Strategies for Integration Testing: Which Modules Do You Combine First?

In a real program with many components, you rarely wire everything together and test it all in one shot — and, as you'll see, doing so is usually the worst option. Suppose you are part of a team building a simplified train-ticket booking flow, the kind of multi-step process students use every exam season to book tickets home: a Login module, then Search Train, then Book Seat, then Payment, then Confirmation. Each module calls the next one in sequence. There are four standard strategies for deciding the order in which you integrate and test these modules as they get built.

  • Big Bang integration. Wait until every module is finished, wire all five together at once, and run one large test. This is simple to set up but risky: if the combined test fails, the failure could be caused by a mismatch between any pair of the five modules, and you get no earlier checkpoint narrowing down which seam is broken. It is workable only for very small systems with very few components.
  • Top-down integration. Start testing from the top of the call chain — Login — and add real modules downward one at a time, in call order: Login, then Login+Search, then Login+Search+Book, and so on. Whenever the next real module in the chain isn't finished yet, you replace it temporarily with a stub — a fake version that returns a fixed, realistic-looking answer without doing real work — so you can keep testing the modules that are ready.
  • Bottom-up integration. Start from the opposite end — the modules that don't call anything else, like Confirmation — and work upward. Since a low-level module like Payment is normally only invoked by a higher module like Book Seat, and that higher module might not exist yet, you write a driver — a small piece of test code that plays the role of the missing caller, invoking the module under test with realistic arguments.
  • Sandwich (or hybrid) integration. Run top-down and bottom-up at the same time, meeting somewhere in the middle. This lets two sub-teams work in parallel — one testing from Login downward with stubs, another testing from Confirmation upward with drivers — which is faster for large systems, at the cost of needing to maintain both stubs and drivers simultaneously until the two halves meet.
Login Search Train Book Seat Payment Confirmation top-down: start at Login, use STUBS for modules not built yet bottom-up: start at Confirmation, use DRIVERS to simulate callers

Stubs and Drivers: Testing Before Every Piece Is Ready

Stubs and drivers solve the same practical problem from opposite directions: how do you test a module when the module it talks to doesn't exist yet? Both are throwaway pieces of code written purely to support testing — neither ships in the final product.

Suppose the Payment module in the booking flow isn't built yet, but Login, Search Train, and Book Seat are ready and you're integrating top-down. You write a stub that stands in for Payment: it accepts the same arguments the real module would, and returns a fixed, plausible-looking result instead of actually processing a payment.

def reserve_seat(train_id, passenger):
    # already built and already integrated
    return "S1-23"

def payment_stub(amount):
    # STUB: stands in for the real Payment module,
    # which isn't finished yet
    print(f"[STUB] pretending to charge Rs {amount}")
    return {"status": "SUCCESS", "transaction_id": "STUB123"}

def book_seat(train_id, passenger, amount):
    seat = reserve_seat(train_id, passenger)
    result = payment_stub(amount)
    if result["status"] == "SUCCESS":
        return f"Seat {seat} confirmed for {passenger}"
    return "Booking failed"

print(book_seat("12951", "Ananya", 1800))

Trace it: reserve_seat returns the fixed string "S1-23", payment_stub(1800) prints the stub notice and returns a dictionary with "status": "SUCCESS", so the if condition is true, and book_seat returns "Seat S1-23 confirmed for Ananya". This lets you confirm that Login → Search Train → Book Seat are wired correctly today, without waiting for whoever is building Payment — and later, when the real Payment module is ready, you simply delete the stub and re-run the same test with the genuine module in its place.

Now the opposite situation: suppose the low-level fare calculation logic is finished, but the larger booking flow that would normally call it is not. In bottom-up integration you write a driver — a minimal stand-in for the missing caller — that invokes the real module directly with realistic values.

def fare_calculator(distance_km, travel_class):
    # illustrative rates in rupees per km, not real IRCTC fares
    rate = {"SL": 0.5, "AC3": 1.5, "AC2": 2.2}[travel_class]
    return round(distance_km * rate)

def test_driver_for_fare_calculator():
    # DRIVER: plays the role of the not-yet-built booking
    # flow, which would normally call fare_calculator
    fare = fare_calculator(1200, "AC3")
    assert fare == 1800, f"expected 1800, got {fare}"
    print("driver test passed:", fare)

test_driver_for_fare_calculator()

Trace it: fare_calculator(1200, "AC3") looks up rate = 1.5 for the key "AC3", computes round(1200 * 1.5) = round(1800.0) = 1800. The assertion fare == 1800 holds, and the driver prints confirmation. This validates the fare calculator's real behaviour before the module that will eventually call it — the booking flow — even exists.

The distinction to keep straight: a stub replaces something the module under test calls (a callee, usually lower in the chain); a driver replaces something that calls the module under test (a caller, usually higher in the chain). Get this backwards and it's worth re-reading the two code examples side by side until the direction clicks — in book_seat, the stub is called by the code being tested; in the driver example, the driver is what calls the code being tested.

Where Integration Testing Sits: Not Unit Testing, Not System Testing

It's worth placing integration testing precisely between its two neighbours. Unit testing checks one component alone. Integration testing checks that a handful of connected components hand data to each other correctly. System testing, one level up again, checks the entire finished application against its overall requirements — for example, does the whole train-booking app, end to end, let a real user log in, search, book, pay, and receive a confirmation, matching what the app was supposed to do in the first place? Integration testing is concerned with the seams between components; system testing is concerned with whether the finished whole satisfies the user's actual need. You'll meet this three-level picture again, formalised further, in later work on the software development life cycle — but the core distinction, unit versus integration versus system, is exactly what you've just learned by tracing the marks-to-grade bug.

Check Your Understanding

1. In the marks-to-grade example, calculate_total and calculate_percentage both passed every unit test written for them. Why didn't unit testing catch the interface bug between calculate_percentage and assign_grade?

Answer: A unit test for assign_grade only checks assign_grade's own behaviour against the inputs its author chose — fractions like 0.95 and 0.72. It never runs calculate_percentage at all, so it has no way of knowing that the value it will actually receive in the real pipeline is on a 0–100 scale, not 0–1. Only a test that runs both functions together, in the real order, can expose that mismatch.

2. You're building a three-module chain, A calls B calls C, and C isn't finished yet. Which integration strategy lets you start testing A and B today, and what would you need to write to make that possible?

Answer: Top-down integration, starting at A. Since C is missing, you'd write a stub in its place — a fake version of C that returns a fixed, realistic result — so that B's calls to C don't crash, letting the real A-to-B interface be tested immediately.

3. "If every function in my program passes its own unit test, the whole program is guaranteed to work." True or false, and why — referencing the worked example?

Answer: False. calculate_percentage and assign_grade each passed their own unit tests while relying on two incompatible assumptions about scale, and the combined pipeline still produced the wrong grade. Unit tests verify a component against its own assumed inputs; they cannot verify that a neighbouring component's actual outputs match those assumptions — only integration testing does that.

4. Why is Big Bang integration riskier for an eight-module system than for a two-module one, even if every module individually has full unit test coverage?

Answer: With eight modules wired together all at once, a failing combined test could stem from a mismatch at any of the several seams between them, and there's no earlier checkpoint narrowing the search. Incremental strategies like top-down or bottom-up add one new real module at a time, so if a test that passed before suddenly fails after adding one module, the newly added module (or its connection to what's already integrated) is almost certainly where the fault is.

Summary

  • Integration testing runs two or more real components together, exactly as the finished program wires them, to catch bugs that live in the connections between components rather than inside any single one — bugs a unit test structurally cannot see, since a unit test never runs the other component at all.
  • The worked example traced a real interface mismatch: calculate_percentage returns a 0–100 value, assign_grade was unit-tested assuming a 0–1 fraction, and an 87.8% student was wrongly graded A1 instead of A2 — with every individual unit test passing.
  • Interface mismatches (wrong scale, wrong units, wrong format) are one common cause of integration bugs; shared-resource conflicts and ordering/timing issues are others. What unites them is that each component is individually correct by its own tests.
  • Four integration strategies decide the order components get combined: Big Bang (all at once — simple but hard to debug), top-down (start at the top, use stubs for unfinished lower modules), bottom-up (start at the bottom, use drivers to simulate unfinished higher modules), and sandwich (both directions at once, meeting in the middle).
  • A stub fakes something the module under test calls; a driver fakes something that calls the module under test. Both are temporary scaffolding, deleted once the real module they replace is ready.
  • Integration testing sits between unit testing (one component) and system testing (the whole finished application against its requirements) — three distinct checks, each catching a different class of bug.
← Unit Testing with JestEnd-to-End Testing with Cypress →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn