Component (Unit) Testing
Component testing, which ISTQB also calls unit testing, is the test level that checks the smallest separately testable parts of the software on their own: a function, a class, a module or a single UI component. The tests are code that calls the component directly with chosen inputs and compares what it returns with an expected result, usually written with a unit test framework such as JUnit, pytest, Jest or Vitest.
For example, a function that applies a 10% coupon to a 200.00 SAR basket must return 180.00. A component test calls the function with exactly those values and checks the answer, without opening a browser, starting the whole application or contacting the real coupon service. Because such tests need nothing else, each one usually runs in milliseconds and the team can run all of them on every change.
Why it matters
Many defects, such as a wrong formula, a missed condition or an unhandled empty value, live inside a single component. When a component test finds one, its location is known at once: the failing test names the function and the input. When the same defect is found later through the user interface, it needs a bug report, reproduction and investigation across several layers before anyone knows where it is.
For a tester, component tests are the first safety net in the team's test strategy. Knowing what they cover lets you aim system and acceptance tests at what only those levels can find, instead of repeating checks that already run on every commit. Knowing what they cannot cover, the behaviour hidden behind stubs and mocks, tells you where integration risk remains.
What a component test is, and who writes it
The ISTQB Foundation syllabus v4.0 describes component testing as focusing on testing components in isolation. It often needs specific support, such as a test harness or a unit test framework, and it is normally performed by developers in their own development environment. In practice its test basis is the material closest to the code: the component's detailed design or specification, the code itself and the data model.
What counts as a component depends on the code. In a back end it is usually a function or a class; in a front end it can be a single UI component such as a date picker. Teams also use the words differently: some say "component test" for something larger, such as one whole service with everything around it replaced, so ask what your team means before you compare numbers.
Most unit tests share a three-part shape, often called Arrange, Act, Assert: prepare the inputs and dependencies, call the component once, and compare the result with the expected value. A good unit test is fast, checks one behaviour, gives the same result on every machine and every run, and has a name that states the expected behaviour.
Isolation and test doubles: stubs, mocks and the rest
Real components depend on other things: a database, a payment provider, the current date, another team's service. To test one component in isolation, the developer replaces those dependencies with test doubles, a general term from Gerard Meszaros named after the stunt doubles of film. Isolation makes the test fast and repeatable, and it makes situations testable that are hard to create with the real dependency, such as a timeout or an expired coupon.
The ISTQB glossary uses two older terms: a stub replaces a component that the component under test calls, and a driver calls the component under test in place of its real caller. Drivers and stubs together make up the test harness. The finer distinctions below come from Meszaros, as described by Martin Fowler:
- Dummy: passed only to fill a parameter list; it is never actually used.
- Stub: gives canned answers to the calls made during the test, for example "SAVE10 is a 10% coupon". The test then checks the component's result (state verification).
- Spy: a stub that also records how it was called, so the test can inspect the calls afterwards.
- Mock: pre-programmed with the calls it expects, and the test fails if they do not happen, for example "send exactly one confirmation e-mail" (behaviour verification).
- Fake: a working but simplified implementation, such as an in-memory database.
Try it: Mock API Response Generator Try it: Mock Error Response Generator
Worked example: a coupon component with a stubbed service
Requirement: "A valid coupon takes its percentage off the basket subtotal, up to the coupon's maximum discount. An unknown or expired coupon leaves the subtotal unchanged and is reported as invalid." The component applyCoupon looks up the code through couponApi, which in production calls a remote coupon service. You do not need to know JavaScript to read the code below: await waits for an answer, test(...) defines one named test, and expect(actual).toEqual(expected) compares the actual result with the expected one.
In the tests, couponApi is a stub that knows one coupon, SAVE10: 10% off, at most 50.00 SAR. The results no longer depend on the real service being up, on which coupons exist in a shared database, or on today's date. The developer writes one test per rule, each with an expected value worked out from the requirement:
- 200.00 with SAVE10 → total 180.00 (10% is 20.00, below the cap).
- 1000.00 with SAVE10 → total 950.00 (10% would be 100.00, so the 50.00 cap applies).
- A stub that returns the coupon marked as expired → total 200.00, error
COUPON_INVALID. - A stub that finds nothing for the code FAKE99 → total 200.00, error
COUPON_INVALID. - A stub that throws "service unavailable" → the error escapes from
applyCoupon. The requirement never says what checkout should do then, so the tester takes that question to the product owner. A double made this path testable in seconds; with the real service it is hard to produce on purpose.
// Component under test
async function applyCoupon(code, subtotal, couponApi) {
const coupon = await couponApi.lookup(code);
if (!coupon || coupon.expired) return { total: subtotal, error: 'COUPON_INVALID' };
const discount = Math.min((subtotal * coupon.percent) / 100, coupon.maxDiscount);
return { total: subtotal - discount, error: null };
}
// Arrange: a stub gives a canned answer instead of the remote coupon service
const couponApi = {
lookup: async (code) =>
code === 'SAVE10' ? { percent: 10, maxDiscount: 50, expired: false } : null,
};
test('SAVE10 takes 10% off a 200.00 basket', async () => {
const result = await applyCoupon('SAVE10', 200, couponApi); // Act
expect(result).toEqual({ total: 180, error: null }); // Assert
});Stub returns a known coupon
- Unit test
applyCouponComponent under test- Stub for
couponApi
- 1 Unit test internally: Arrange: a stub that knows SAVE10
- 2 From Unit test To
applyCoupon: Act: call the component onceapplyCoupon('SAVE10', 200, couponApi) - 3 From
applyCouponTo Stub forcouponApi: Looks up the codelookup('SAVE10') - 4 From Stub for
couponApiToapplyCoupon: Canned answer, no network call reply{ percent: 10, maxDiscount: 50 } - 5 From
applyCouponTo Unit test: Computes and returns the total reply 10% of 200.00 is 20.00, below the 50.00 cap{ total: 180, error: null } - 6 Unit test internally: Assert: equals the expected 180.00 Pass
Outcome: Checked in milliseconds; the real coupon service is never called. Pass
Stub simulates an outage
- Unit test
applyCouponComponent under test- Stub for
couponApi
- 1 Unit test internally: Arrange: a stub that throws
- 2 From Unit test To
applyCoupon: Act: call the component onceapplyCoupon('SAVE10', 200, couponApi) - 3 From
applyCouponTo Stub forcouponApi: Looks up the codelookup('SAVE10') - 4 From Stub for
couponApiToapplyCoupon: Throws an error replyError('service unavailable') - 5 From
applyCouponTo Unit test: The error escapes: no total, no error code reply - 6 Unit test internally: Assert: the requirement gives no expected result Caution
Outcome: Open question for the product owner: what should checkout do when the coupon service is down? Caution
Test-driven development: the test comes first
The ISTQB syllabus lists test-driven development (TDD), together with acceptance test-driven development and behaviour-driven development, among the approaches in which tests drive development. In TDD the developer works in short cycles: write a component test for the next small piece of behaviour and see it fail, write just enough code to make it pass, then improve the structure of the code while all tests stay green. Practitioners call the cycle red, green, refactor.
In the coupon example, the developer would first write the 180.00 test against a function that does not exist yet, then the cap test, then the expired-coupon test, adding code only to make each one pass. TDD leaves behind a suite of fast regression tests and pushes the design towards small components that are easy to isolate. It is a way of developing that applies early testing; it does not replace the other test levels, because the developer's tests can only check the developer's own understanding of the requirement.
What a tester should know and review
Testers rarely own the unit tests, but reading them is one of the cheapest reviews a team can do. You do not have to write the code yourself to ask good questions; the test names and inputs usually tell you what was checked. Look at:
- Rules: map each acceptance criterion to a test. A rule without a test, such as a cap or a default value, is a gap at this level.
- Inputs: empty values, zero, the exact limit named in the requirement and invalid formats, not only one typical value.
- Assertions: each test compares the result with a value worked out from the requirement, not copied from the code's output and not just "no error was thrown".
- Doubles: stubs return what the real dependency really returns, including its errors and empty results. Later, integration tests and contract tests (tests that check that the real service still answers the way the doubles assume) confirm that they match.
- Independence: no dependence on run order, the real clock, random values or shared data. A test that passes and fails without any code change is flaky.
- Results: know where each build's unit test report is; a build whose unit tests failed or were skipped is not ready for system testing.
Try it: Requirements → Test Cases Try it: Boundary Value Generator
Common mistakes
- Treating unit tests as the developers' private business and never reading them, so the team tests the same rules twice and misses the rules nobody tested.
- Assuming that green unit tests mean the feature works. Doubles replace the real dependencies, so wrong assumptions about a real service, a database or another team's API only show up at integration level.
- Calling every test a developer writes a unit test. A test that talks to a real database or network is usually classed as an integration test: slower, and it can fail for reasons outside the component.
- Stubs that return only the happy path, or fields the real service never sends; the tests pass and production fails.
- Over-mocking: checking every internal call, so the tests break on every refactoring while catching few real defects.
- Copying expected values from what the code returned the first time instead of working them out from the requirement; the test then protects the defect.
The QA perspective
Bring component testing into the conversation early. In refinement, when a story has calculation or input rules, suggest which of them belong in unit tests and give the developer your test conditions: the limits, the empty and invalid inputs, the error responses of dependencies. In code review or pairing, read the test names against the acceptance criteria and ask what each stub returns when its dependency fails.
Record what the component level already covers, for example in the story's test notes or a traceability matrix (a table that links each requirement to its tests), so system tests focus on flows, integration and user-visible behaviour instead of repeating every rule through the UI. Note the behaviour that is only ever tested against doubles: it is the first candidate for contract tests and for integration tests; the lesson on integration testing comes next.
Practice exercise
Review the unit tests of a shipping-fee component
A developer has written the component below and three unit tests for it. All three pass. Review them as a tester: decide what kind of test double they use, check which parts of requirement R-12 are tested, and find what is missing.
Write the missing tests as an input, the stub's behaviour and the expected result. You do not need to write code.
// Requirement R-12
// Orders of 300.00 SAR or more ship free. Other orders pay the fee that the
// rate service returns for the delivery city. If the rate service fails,
// charge the default fee of 25.00 SAR.
async function shippingFee(total, city, rateService) {
if (total > 300) return 0;
try {
return await rateService.feeFor(city);
} catch {
return 25;
}
}
// Unit tests: all three pass
const rates = { feeFor: async (city) => 18 };
test('free shipping for large orders', async () => {
expect(await shippingFee(500, 'Riyadh', rates)).toBe(0);
});
test('small order pays the city fee', async () => {
expect(await shippingFee(120, 'Riyadh', rates)).toBe(18);
});
test('fee is a number', async () => {
const fee = await shippingFee(120, 'Jeddah', rates);
expect(typeof fee).toBe('number');
});- Name the test double the tests use and say what kind it is.
- Map each sentence of R-12 to the test that checks it.
- For every test, ask: would it fail if the code were wrong?
- Write the missing tests and one question for the developer or the product owner.
Show the model answer
rates is a stub: it gives the same canned answer, 18, to every call and records nothing. The tests check the returned value, which is state verification.
Rule 1 (300.00 or more ships free) is tested only with 500, far from the limit. Rule 2 (the city fee) is checked by the second test. Rule 3 (the default fee when the service fails) has no test at all.
Missing tests: a total of exactly 300.00 → fee 0. This test fails against the code, because total > 300 charges a fee at 300.00: a defect found before anyone opens the app. Also 299.99 → 18, and a stub whose feeFor throws an error → fee 25.
The third test only asserts that the fee is a number, so it would pass with any wrong fee; replace it with an exact expected value. Because the stub returns 18 for every city, passing the wrong city would go unnoticed: use a stub with a different fee per city (Riyadh 18, Jeddah 22) or a spy that records the city it received.
Questions: what counts as the rate service failing: an error, a timeout, an unknown city? Is the fee in SAR and a plain number, as the stub assumes? Which integration or contract test checks that?
Frequently asked questions
Is component testing the same as unit testing?
In ISTQB terminology, yes: component testing is also known as unit testing. Some teams use "component test" for something larger, such as a UI component rendered with its child components, or a whole service with everything around it replaced by test doubles. Ask what your team means.
Do testers write unit tests?
Usually developers write and maintain them; ISTQB describes component testing as normally performed by developers. Testers contribute test conditions, review the tests and their results, and in some teams pair with developers or write tests themselves. Their main contribution is an independent view: a developer tests what they believe the code should do.
What is the difference between a mock and a stub?
A stub supplies canned answers so the component can run, and the test checks the component's result. A mock carries expectations about the calls it should receive, and the test fails if those calls are wrong or missing. Many mocking libraries call every double a mock, so look at what the double actually does.
If the developers use TDD, does the team still need testers?
Yes. TDD tests check small pieces of code against the developer's understanding. They do not check how components work together, whether the whole system meets its requirements, or whether a requirement itself is right. Those questions belong to integration, system and acceptance testing.
References
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
- Test Double (Martin Fowler)Gerard Meszaros's vocabulary for test doubles — dummy, fake, stub, spy and mock — as summarised by Martin Fowler.
- Mocks Aren't Stubs (Martin Fowler)The difference between state verification and behaviour verification, and between the classical and mockist styles of test-driven development.
Practise with Mutqan tools
Generate complete mock API responses (status, headers, JSON body) for success, list, detail, error and empty cases.
Generate structured error responses (validation, auth, not found, rate limit, server error) in popular formats.
Generate mock JSON from a template with placeholders ({{name}}, {{uuid}}, {{int:1-10}}, {{date}}, {{pick:a|b}}).
Convert a list of numbered requirements into traceable test cases with a requirement-to-test matrix.
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Go deeper
- End-to-End Product Quality AssuranceTest levels: unit, integration, system and acceptance
- Manual vs Automated TestingDeciding what to automate: the pyramid and a simple rule
- API Testing Approaches ComparedMocking: testing the consumer and the error paths
- CI/CD TestingTest stages in a pipeline and why the order matters
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.