Unit Testing in Software Engineering: Process, Techniques, Types & Examples
What is Unit Testing?
Unit Testing tests individual software units separately so that defects can be found close to the part of the code where they were introduced.
Suppose an online shopping application contains:
- Login function
- Tax calculation function
- Discount calculation function
- Payment module
- Order module
Instead of waiting for the complete application to be ready, developers can test the discount function separately.
If a premium customer should receive a 10% discount, the expected output may be:
If the function returns 900, that particular test passes. If it returns 1000 or another incorrect value, the test fails.
What is a Unit in Unit Testing?
A unit does not always mean exactly one line of code.
The meaning depends on the programming language, architecture, and testing approach.
Function
A small independent function such as calculateTax().
Method
A method belonging to an object or class.
Class
A class whose public behaviour is tested as a small component.
Module
A small cohesive software module containing related behaviour.
Objectives of Unit Testing
Verify Correct Logic
Check whether the unit produces the expected output for a given input.
Find Defects Early
Detect logical errors before components are combined into larger systems.
Check Boundary Behaviour
Test minimum, maximum, zero, empty, and edge values.
Check Error Handling
Verify whether invalid or exceptional conditions are handled correctly.
Support Refactoring
A good unit test suite can provide quick feedback when internal code is changed.
Prevent Regression
Previously tested behaviour can be checked again after new changes.
Improve Design Feedback
Code that is very difficult to unit test may reveal excessive coupling or hidden dependencies.
Document Behaviour
Well-written unit tests can show how a function or class is expected to behave.
Provide Fast Feedback
Small isolated tests can usually execute quickly during development.
Why is Unit Testing Important?
Without Unit Testing, developers may discover a defect only after many components have been integrated.
At that stage, finding the exact source of the problem may become more difficult.
Who Performs Unit Testing?
Unit Testing is commonly performed by developers because they understand the internal implementation and normally have access to the source code.
Automated unit tests are often written along with the production code and executed repeatedly during development.
Unit Testing Process
A systematic Unit Testing process is better than randomly trying input values.
Arrange-Act-Assert Pattern
One of the easiest ways to structure a unit test is the Arrange-Act-Assert pattern.
Prepare input data, objects, dependencies, and expected values.
Execute the function or method being tested.
Compare the actual result with the expected result.
Simple AAA Example
Types of Unit Testing
Unit Testing can be classified in different ways depending on the purpose and testing approach.
1. Manual Unit Testing
A developer manually executes or checks a small unit without maintaining a repeatable automated test script.
This may be useful for quick investigation, but it becomes inefficient when tests must be repeated frequently.
2. Automated Unit Testing
Test code is written using a testing framework and executed automatically.
This is especially useful for:
- Regression testing
- Repeated builds
- Continuous Integration
- Large collections of unit tests
- Fast developer feedback
3. Positive Unit Testing
Checks expected behaviour using valid input.
Example:
4. Negative Unit Testing
Checks behaviour when invalid or unexpected input is provided.
Example:
5. Exception Testing
Checks whether expected exceptions or error conditions are produced correctly.
Example:
6. State-Based Unit Testing
Checks the state of an object before and after an operation.
Example:
7. Interaction-Based Unit Testing
Checks whether a unit interacts with its dependency correctly.
For example, a service method may be expected to call a repository exactly once with specific data.
8. Parameterized Unit Testing
The same test logic is executed using multiple sets of input and expected output.
| Input | Expected Square |
|---|---|
| 2 | 4 |
| 3 | 9 |
| 5 | 25 |
| 10 | 100 |
Unit Testing Techniques
Good unit tests are selected using testing techniques rather than random values.
Statement Testing
Design tests to execute statements in the unit.
Branch Testing
Design tests to exercise decision outcomes such as true and false branches.
Condition Testing
Check individual Boolean conditions in logical expressions.
Boundary Value Analysis
Test values near important input boundaries.
Equivalence Partitioning
Divide possible input into groups expected to behave similarly.
Error Guessing
Use experience to predict likely problem cases.
White-Box Unit Testing Techniques
Unit Testing is often performed with knowledge of the source code, so white-box techniques are especially useful.
Statement Coverage
Checks whether executable statements have been executed by the tests.
Testing only:
does not exercise the second return path in all logical situations.
Branch Testing
Every decision outcome should be tested.
For the previous example, useful tests include:
| Input | Branch | Expected Output |
|---|---|---|
| 5 | True | Positive |
| -2 | False | Zero or Negative |
Condition Testing
Consider:
The two important Boolean conditions are:
- age >= 18
- hasID
Tests should consider meaningful true and false combinations.
Loop Testing
When a unit contains loops, useful test conditions may include:
- Zero iterations
- One iteration
- Typical number of iterations
- Boundary number of iterations
- Large permitted iteration count
Path Testing
Different control-flow paths through the unit are identified and selected for testing.
For complex units, Cyclomatic Complexity can help identify the number of linearly independent basis paths.
Black-Box Techniques at Unit Level
Even when source code is available, input-output based techniques can still be useful at unit level.
Equivalence Partitioning Example
Suppose a function accepts marks from 0 to 100.
Invalid Partition
marks < 0Valid Partition
0–100Invalid Partition
marks > 100Boundary Value Analysis Example
For a valid range of 0–100, useful values include:
Decision Table Testing
This can be useful when a unit contains several business-rule combinations.
| Condition | Rule 1 | Rule 2 | Rule 3 | Rule 4 |
|---|---|---|---|---|
| Premium Customer? | Yes | Yes | No | No |
| Order > ₹1000? | Yes | No | Yes | No |
| Discount | 20% | 10% | 5% | 0% |
What is a Test Fixture?
A test fixture is the known environment or state required for a test.
It may contain:
- Objects
- Initial values
- Test accounts
- Temporary data
- Mock dependencies
- Configuration
Setup and Teardown
Many test frameworks provide setup and teardown operations.
What are Test Doubles?
A unit may depend on another object, database, API, file system, clock, payment service, email service, or network connection.
Using the real dependency may make a unit test slow, unpredictable, or no longer truly isolated.
A test double replaces a real dependency during testing.
Dummy Object
An object passed only because a parameter is required but whose behaviour is not actually used by the test.
Stub
Returns predefined data when the unit calls a dependency.
Example: always return a fixed customer record.
Fake
A simplified working implementation used instead of the real production dependency.
Example: in-memory repository instead of a real database.
Spy
Records information about calls so the test can later check how the dependency was used.
Mock
A controlled replacement with predefined interaction expectations that can be verified by the test.
Stub vs Mock
| Basis | Stub | Mock |
|---|---|---|
| Main Purpose | Provide controlled responses | Verify expected interactions |
| Main Question | What should the dependency return? | Was the dependency called correctly? |
| Example | Return user ID 10 | Verify save() was called once |
Stubs and Drivers in Unit Testing
Traditional Software Engineering books often explain unit testing using stubs and drivers.
Stub
A temporary replacement for a lower-level module or dependency called by the unit under test.
It returns controlled results.
Driver
A temporary program that calls the unit being tested when the real calling module is not yet available.
Dependency Isolation
Isolation is one of the most important ideas in Unit Testing.
Suppose an OrderService directly uses:
- Real database
- Real payment gateway
- Real email server
If all these real services are required for a test, the test has moved closer to integration testing.
For a focused unit test, these external dependencies can often be replaced with controlled test doubles.
Dependency Injection
Dependency Injection makes testing easier by allowing dependencies to be supplied from outside rather than being created secretly inside the unit.
During production, a real payment service can be supplied.
During unit testing, a fake or mock payment service can be supplied.
How to Design a Unit Test Case
A good unit test should clearly state what behaviour is being checked.
UT-DISCOUNT-01
calculateDiscount()
Amount = 1000, Premium = true
900
Premium customer receives 10% discount
Pass if actual output is 900
Complete Unit Testing Example
Production Function
Test 1: Premium Customer
Test 2: Normal Customer
Test 3: Zero Amount
Test 4: Negative Amount
What Did These Tests Cover?
- Normal premium behaviour
- Normal non-premium behaviour
- Zero boundary condition
- Invalid negative input
- Exception behaviour
Unit Testing of Functions with Multiple Conditions
Useful test cases include:
| Age | Citizen? | Expected Result |
|---|---|---|
| 20 | Yes | True |
| 17 | Yes | False |
| 20 | No | False |
| 18 | Yes | True |
Code Coverage in Unit Testing
Code coverage measures how much of the program structure was exercised while tests were executed.
Coverage is useful, but it must be interpreted carefully.
Statement Coverage Formula
Branch Coverage Formula
Example
Suppose a function contains 20 executable statements and tests execute 18 of them.
Cyclomatic Complexity and Unit Testing
Cyclomatic Complexity can help identify the number of independent paths in a unit's control-flow graph.
For a single connected function, another simple representation is often:
If a function has many decision points, it may require more tests to exercise different branches and paths.
Test-Driven Development (TDD)
Test-Driven Development is a development approach in which a test is written before the production code required to make that test pass.
Write a failing test
Write minimum code to pass
Improve the code
Step 1: Red
Write a test for behaviour that does not yet exist.
The test should fail.
Step 2: Green
Write enough production code to make the test pass.
Step 3: Refactor
Improve the code structure while keeping the tests passing.
Unit Testing vs TDD
Unit Testing and TDD are related but they are not the same.
| Basis | Unit Testing | TDD |
|---|---|---|
| Meaning | Testing individual software units | Development approach driven by tests |
| When Test is Written | Before or after production code | Normally before corresponding production code |
| Main Focus | Verify unit behaviour | Guide design and implementation through tests |
Unit Testing and the Test Pyramid
The Test Pyramid is a common testing heuristic that encourages a large base of fast small tests, fewer integration tests, and fewer broad end-to-end tests.
The exact balance depends on the project. The pyramid should be treated as a useful design idea rather than a fixed universal percentage.
Automated Unit Testing
Unit Testing is especially suitable for automation because individual units can often be tested quickly and repeatedly.
A typical automated flow is:
Unit Tests in CI/CD
Automated unit tests are often executed during Continuous Integration.
If an important test fails, the build or pipeline can be stopped before the defective change moves further through delivery stages.
Advanced Unit Testing Concepts
1. Parameterized Testing
One test definition is executed using multiple input-output combinations.
This reduces repetitive test code while improving data coverage.
2. Data-Driven Testing
Test data is separated from test logic and may come from a table, file, object, or generated dataset.
3. Property-Based Testing
Instead of checking only a few fixed examples, a general property is defined and many input values are generated automatically.
Example property:
4. Mutation Testing
Mutation Testing evaluates the strength of tests by intentionally making small changes to production code.
For example:
If existing unit tests detect the changed behaviour, the mutant is killed.
If all tests still pass, the test suite may be missing an important case such as age exactly 18.
5. Characterization Testing
When working with legacy code whose behaviour is poorly documented, characterization tests can record the current observable behaviour before major refactoring begins.
6. Testing Asynchronous Code
Modern software often contains asynchronous operations such as promises, tasks, callbacks, and asynchronous functions.
Unit tests should wait correctly for the asynchronous operation and verify both successful and failed outcomes.
7. Testing Time-Dependent Code
Code depending directly on the current date or time can create unstable tests.
A controllable clock or injected time provider can make the test deterministic.
8. Testing Random Behaviour
Random values can make results difficult to reproduce.
Tests can control randomness by injecting a random-number provider or using a fixed seed where appropriate.
9. Floating-Point Testing
Floating-point calculations may contain tiny precision differences.
Instead of always checking exact equality, tests may compare values using a small acceptable tolerance.
Testing Object-Oriented Classes
For object-oriented programs, useful Unit Testing areas include:
- Constructor behaviour
- Public methods
- Object state changes
- Boundary conditions
- Exceptions
- Interactions with dependencies
- Inheritance or polymorphic behaviour where relevant
Testing Procedural Programs
In procedural programming, a unit may be a function or procedure.
Tests should check:
- Input values
- Return values
- Output parameters
- State changes
- Side effects
- Error handling
- Boundary conditions
Characteristics of Good Unit Tests
A useful memory rule is FIRST.
F – Fast
Unit tests should generally execute quickly so developers can run them often.
I – Independent
One test should not depend on another test running first.
R – Repeatable
The same conditions should produce the same result repeatedly.
S – Self-Validating
The test should clearly determine pass or fail using assertions.
T – Timely
Tests should be written close to the time when the related behaviour is created.
Other Good Qualities
- Readable
- Focused
- Deterministic
- Maintainable
- Meaningful test names
- Clear failure messages
- Small amount of setup
- Checks useful behaviour
How Should Unit Tests Be Named?
Test names should explain the situation and expected behaviour.
A descriptive test name makes failures easier to understand.
One Behaviour Per Test
A unit test should normally focus on one clearly defined behaviour or reason for failure.
This does not mean every test must contain only one assertion.
Several related assertions may be acceptable when they verify one logical behaviour.
Common Unit Testing Mistakes
Testing Too Much at Once
If the test uses the real database, network, email service, and payment gateway, it may no longer be a focused unit test.
Testing Implementation Instead of Behaviour
Tests that depend heavily on private internal details can break during harmless refactoring.
No Negative Tests
Testing only valid values leaves invalid and exceptional behaviour unchecked.
Shared Mutable State
One test changing data required by another test can create unpredictable results.
Flaky Tests
A test that sometimes passes and sometimes fails without a relevant code change reduces trust in the suite.
Over-Mocking
Mocking every internal detail can create tests that are difficult to maintain and too closely tied to implementation.
Chasing Coverage Numbers Only
High coverage without meaningful assertions may give false confidence.
Ignoring Test Code Quality
Unit tests are also software and should remain readable and maintainable.
What is a Flaky Unit Test?
A flaky test produces inconsistent results without a meaningful change in the code being tested.
Common causes include:
- Current time dependency
- Random data
- Shared global state
- Thread timing
- Network dependency
- Real external service
- Uncontrolled database data
- Test execution order dependency
What Should Not Normally Be a Unit Test?
If the purpose of a test is specifically to verify the real interaction between several components or external systems, it belongs more naturally to integration or system testing.
Examples include:
- Real database connectivity
- Actual payment gateway integration
- Complete browser workflow
- Real API-to-API communication
- Full application deployment
Unit Testing vs Integration Testing vs System Testing
| Basis | Unit Testing | Integration Testing | System Testing |
|---|---|---|---|
| Scope | Individual unit/component | Interaction between components | Complete system |
| Main Purpose | Verify local behaviour | Verify interfaces and interactions | Verify complete system behaviour |
| Typical Performer | Developers | Developers/testers depending on project | Testing team or relevant project team |
| External Dependencies | Often isolated | Usually some real integrations | Large part of complete system |
| Execution Speed | Usually fast | Usually slower than unit tests | Often slower and more complex |
| Example | calculateTax() | Order Service + Database | Complete shopping application |
Unit Testing vs Regression Testing
These terms describe different ideas.
Unit Testing describes the level or scope of the test.
Regression Testing describes the purpose of repeating tests after change.
Therefore, automated unit tests can become part of a regression test suite.
Unit Testing vs Debugging
Unit Testing
Executes tests to reveal incorrect behaviour in a small unit.
Debugging
Investigates the cause of a failure and modifies the code to correct the defect.
Advantages of Unit Testing
Early Defect Detection
Problems can be discovered close to the code where they were introduced.
Fast Feedback
Small automated tests can usually run frequently during development.
Supports Refactoring
Tests can detect unintended behaviour changes during internal restructuring.
Improves Regression Safety
Existing tests can be rerun after changes.
Improves Modularity Feedback
Difficult-to-test code can reveal excessive coupling or hidden dependencies.
Useful Documentation
Tests can show examples of expected behaviour.
Limitations of Unit Testing
Cannot Find Every Integration Problem
Units may work individually but fail when connected.
Cannot Replace System Testing
Unit tests do not verify the complete user workflow.
Maintenance is Required
Tests need updates when valid behaviour or interfaces change.
Poor Tests Give False Confidence
Many weak tests are not automatically better than fewer meaningful tests.
External Quality Needs Other Tests
Performance, usability, deployment, and real integration behaviour require other testing approaches.
Coverage is Not Correctness
Executing code does not automatically prove the result is correct.
Best Practices for Unit Testing
- Keep each unit test focused.
- Use meaningful test names.
- Test both positive and negative cases.
- Test important boundary values.
- Keep tests independent.
- Avoid uncontrolled external dependencies.
- Use clear assertions.
- Keep tests deterministic.
- Run unit tests frequently.
- Fix failing tests rather than ignoring them.
- Refactor duplicated test code carefully.
- Do not chase coverage percentage without useful assertions.
- Use mocks and stubs only where they provide value.
- Keep production behaviour and test expectations understandable.
Common Unit Testing Metrics
Test Pass Rate
Statement Coverage
Branch Coverage
Example
Suppose:
- 100 unit tests executed
- 96 passed
- 4 failed
Unit Testing in Agile Development
In Agile development, developers frequently create and change small pieces of functionality during short iterations.
Unit tests can provide quick feedback after these changes.
Unit Testing in DevOps
Unit tests are commonly placed early in automated build pipelines because they can provide fast feedback before slower test stages are executed.
Common Student Confusions
Memory Tricks
Independent
Repeatable
Self-validating
Timely
Important Exam Questions
Important 5-Mark Question
- Understand the unit.
- Identify test conditions.
- Prepare test data and environment.
- Create unit test cases.
- Execute tests.
- Compare actual and expected results.
- Fix defects.
- Run tests again.
Important 10-Mark Question
- Definition
- Objectives
- Unit Testing process
- White-box techniques
- Boundary and equivalence testing
- Stub and driver
- Test doubles
- Code coverage
- Advantages
- Limitations
- Suitable example
Quick Revision Notes
Testing individual software units
Component Testing
Developers
Verify local unit behaviour
Arrange → Act → Assert
Provides controlled dependency response
Calls the unit under test
Checks expected interaction
Known test state/environment
Valid expected input
Invalid or unexpected input
Check values around limits
Executed statements percentage
Executed decision outcomes percentage
Red → Green → Refactor
Fast, Independent, Repeatable, Self-validating, Timely
Frequently Asked Questions
What is Unit Testing in Software Engineering?
Unit Testing tests individual functions, methods, classes, or components, normally in isolation, to verify that their behaviour is correct.
Is Unit Testing the same as Component Testing?
Yes. Modern testing terminology commonly uses Component Testing for the testing level traditionally called Unit Testing.
Who performs Unit Testing?
Unit Testing is commonly performed by developers because they usually understand the internal source code and implementation.
What is the difference between a stub and a mock?
A stub mainly provides predefined responses to the unit, while a mock is commonly used to verify whether expected interactions occurred.
What is the difference between Unit Testing and Integration Testing?
Unit Testing checks individual components in isolation. Integration Testing checks the interfaces and interactions between components.
Does 100% unit test coverage mean the software has no bugs?
No. Coverage shows which code structure was executed. It does not prove that all requirements, edge cases, or correct results were properly verified.
What is TDD in Unit Testing?
TDD is a development approach that follows the Red-Green-Refactor cycle: first write a failing test, then write enough code to pass it, and finally improve the code while keeping the tests passing.
Why are mocks and stubs used in Unit Testing?
They help isolate the unit from dependencies such as databases, APIs, network services, or other components so that the local behaviour can be tested in a controlled environment.
Conclusion
Unit Testing is the foundation of code-level software testing. Instead of waiting until the entire application is completed, developers test small pieces of software individually and verify their behaviour under normal, boundary, invalid, and exceptional conditions.
A complete Unit Testing approach involves much more than calling a function once. Developers identify test conditions, create controlled fixtures, isolate dependencies, use assertions, apply white-box and black-box test techniques, measure useful coverage, and repeatedly execute the test suite whenever the code changes.
Concepts such as test doubles, stubs, drivers, mocks, dependency injection, Arrange-Act-Assert, code coverage, Test-Driven Development, parameterized testing, and mutation testing make Unit Testing much more powerful in real software development.
Good Unit Tests = Fast + Independent + Repeatable + Clear + Meaningful
For exams, first remember that Unit Testing tests individual software components in isolation. Then connect it with the complete flow:
Once this basic flow is clear, topics such as stubs, drivers, mocks, coverage, TDD, and automation become much easier to understand.
