Module 4 Testing and Maintenance
Module 4 Testing and Maintenance
MODULE 4
TESTING & MAINTENANCE
Ch 10: Testing
Testing
The main objective of software testing is to detect and remove as many defects as possible
from a program. Although exhaustive testing is not feasible because the input domain of most
programs is extremely large, testing remains a practical approach to improve software
reliability. For example, testing a function that takes floating-point numbers as input would
require infinite combinations, making complete testing impossible. Despite this limitation,
systematic testing significantly reduces defects by identifying a large percentage of errors,
thereby improving software quality and ensuring that the software performs as expected under
various conditions.
Key Terminologies
• Mistake: A human action that introduces a defect, such as failing to initialize a variable
or ignoring exceptional conditions like division by zero.
Example: A designer might overlook a requirement during the design phase, leading to its
absence in the code and causing a program error. This illustrates how mistakes in early stages
propagate into errors during implementation.
• Validation: Confirms that the fully developed software meets customer requirements.
It involves executing the software and observing whether it performs as intended.
System testing is a key validation activity.
Verification emphasizes phase containment of errors, meaning defects should be identified
and corrected as early as possible. Detecting a design error during the design phase is
significantly cheaper than fixing it during testing, which would otherwise require redesign,
recoding, and retesting. Therefore, both verification and validation are essential bug filters, and
together they ensure the development of a reliable software product.
Testing Activities
The testing process consists of four major activities:
1. Test Suite Design: Creating a set of test cases based on various test case design
techniques.
2. Executing Test Cases: Running each test case and comparing actual results with
expected results. Any mismatches indicate failures.
4. Error Correction: Debugging the identified errors and modifying the code to eliminate
defects.
Among these activities, debugging is often the most time-consuming because it involves
analyzing program behavior and locating the root cause of defects (as shown in Figure 10.2).
For example, consider a faulty code snippet for finding the maximum of two integers:
if (x > y) max = x;
else max = x;
A test suite with inputs (3,2) and (2,3) can detect the bug, but adding more random test cases
such as (4,3) or (5,1) will not reveal any additional errors. Thus, a carefully designed minimal
test suite is more effective than a large, randomly generated one.
To achieve this, systematic test case design techniques are used, primarily categorized into:
Both approaches complement each other and are used together to ensure comprehensive
testing.
• Integration Testing: Combines tested modules incrementally and tests them as a group
to verify their interaction.
• System Testing: Validates the entire integrated system against its requirements and is
referred to as testing in the large.
Testing modules individually before integration simplifies debugging because identifying the
source of a defect is easier in isolated modules than in an integrated system. Once all modules
are tested independently, integration and system testing ensure the overall system functions
correctly.
Unit Testing
Unit testing is the process of testing individual modules of a software system in isolation after
they have been coded and reviewed. This is typically performed by the developer who
implemented the module, usually during the coding phase. The primary objective of unit testing
is to verify that each module performs its intended functionality correctly before it is integrated
with other modules.
• A procedure to call the functions of the module under test with appropriate parameters.
However, during the early stages of development, the modules that interact with the one being
tested may not yet be implemented or tested. To address this issue, driver and stub modules
are introduced to simulate the required environment and facilitate testing.
Stub Modules
A stub is a dummy procedure that simulates the behavior of a lower-level module that is called
by the module under test. A stub has the same input/output parameters as the real procedure it
represents but contains only a simplified implementation. For example, a stub may simply
return predefined values from a table rather than executing the full functionality of the actual
module. This allows the module under test to be executed without requiring the actual
dependent modules.
Driver Modules
A driver is a piece of code that simulates a higher-level module that calls the module under
test. It is responsible for:
• Initializing and managing any non-local data structures used by the module under test.
• Invoking the functions of the module under test with appropriate test inputs.
In essence, drivers provide the mechanism to test a module in isolation, while stubs provide
the behavior of dependent modules that are not yet available. Together, drivers and stubs form
the testing environment necessary for effective unit testing, as illustrated in Figure 10.3.
Black-Box Testing
Black-box testing is a testing approach in which test cases are designed by examining only the
input and output of a program, without any knowledge of its internal design or code. This
method focuses purely on verifying whether the software meets its functional specifications.
Black-box testing is widely used because it evaluates the software from the end-user’s
perspective and helps ensure that the program behaves as expected under various input
conditions.
Two important techniques are used for designing black-box test cases:
be processed similarly by the program. The main principle behind this technique is that testing
any single input from an equivalence class is as effective as testing any other input from the
same class.
Equivalence classes can be defined by analyzing the input and output data of a program. The
general rules for creating equivalence classes include:
• If the input data can be expressed as a range of values, define one valid equivalence
class and two invalid equivalence classes.
Example: For inputs in the range 1 to 10, the valid class is [1, 10], while the invalid
classes are (−∞, 0) and (11, +∞).
• If the input data consists of discrete values, define one valid equivalence class for
acceptable values and one invalid equivalence class for all other values.
Example: If the valid inputs are {A, B, C}, then the invalid equivalence class is all
values except {A, B, C}.
Example:
For a software that calculates the square root of an integer in the range 0 to 5000, the
equivalence classes are:
• Negative integers (invalid)
BVA involves creating test cases using values at the boundaries of each equivalence class.
• If an equivalence class is a range of values, test cases should include its boundary
values.
Example:
For a function that calculates the square root of integers from 0 to 5000, the equivalence classes
are:
• Negative integers
The boundary value test suite would include: {0, −1, 5000, 5001}.
This ensures that the program handles the transition between valid and invalid input values
correctly.
4. For equivalence classes defined as ranges, add test cases for their boundary values.
Black-box testing is an intuitive and effective approach for detecting functional errors. The
most critical step is the correct identification of equivalence classes. While this may be
challenging initially, with practice it becomes straightforward. Once the equivalence classes
are identified, designing test cases for equivalence classes and their boundaries becomes a
systematic and almost mechanical process.
White-Box Testing
White-box testing is a structural testing technique in which test cases are designed based on an
analysis of the source code. Unlike black-box testing, which focuses on input-output behavior,
white-box testing ensures that internal logic, code paths, and program structure are thoroughly
verified. It is commonly used during unit testing but can also be applied in other stages of
software testing to ensure that every code element functions correctly and no hidden defects
remain.
Basic Concepts
White-box testing strategies can be broadly categorized into coverage-based testing and fault-
based testing:
• Coverage-based testing focuses on ensuring that specific elements of the source code,
such as statements, branches, or paths, are executed during testing. Popular examples
include statement coverage, branch coverage, multiple condition coverage, and
path coverage. The testing criterion in this context defines the specific program
elements that must be executed to consider the test suite adequate. For example, if a
strategy aims to execute every statement at least once, its testing criterion is statement
coverage.
• Fault-based testing, on the other hand, is aimed at detecting specific classes of faults
that are likely to occur in the program. An example of this approach is mutation testing,
where intentional small changes (mutations) are made in the program to check whether
the test cases can detect them.
White-box testing strategies are also compared based on their strength. A strategy is said to be
stronger if it covers all the program elements of a weaker strategy and includes additional
elements. If two strategies each cover unique elements not covered by the other, they are
considered complementary, meaning both should be used together for better testing coverage.
Statement Coverage
Statement coverage is one of the simplest white-box testing techniques. Its goal is to design
test cases that execute every statement in the program at least once. The idea behind this method
is straightforward: if a statement is never executed, there is no way to confirm whether it
contains a defect. For example, a line of code may contain an incorrect arithmetic operation
that would never be discovered unless executed during testing.
However, statement coverage has limitations. Executing a statement only once does not
guarantee that it works correctly for all possible input values. Despite this drawback, statement
coverage is still an intuitive and useful starting point for white-box testing.
Branch Coverage
Branch coverage (also known as edge coverage) ensures that each branch or decision point
in the program’s control flow is executed for both true and false outcomes. This means every
possible decision outcome must be tested at least once.
Branch coverage improves upon statement coverage because it tests decision-making logic and
ensures that both sides of every conditional statement are validated. This helps in detecting
logical errors that might be missed with statement coverage alone.
For example, for the condition ((c1 AND c2) OR c3), test cases should ensure that c1, c2, and
c3 each assume true and false values independently. This approach provides a higher level of
confidence in the correctness of the code but comes at the cost of a rapidly increasing number
of test cases. For n conditions, 2ⁿ test cases may be required, making it practical only for small
n.
Path Coverage
Path coverage focuses on testing all linearly independent paths in a program. It is based on
the program’s control flow graph (CFG), which represents the flow of control between
different program statements.
• Control Flow Graph (CFG): Each statement in the program is represented as a node,
and edges represent control transfers between statements. CFGs allow visualization of
sequence, selection (branching), and iteration (loops).
• Path: A path is a sequence of nodes and edges from the program’s entry point to an exit
point.
Since loops can create infinite paths, testing all paths is impractical. Instead, testers focus on
linearly independent paths (also called basis paths)—paths that add at least one new edge
not present in previously identified paths.
The number of linearly independent paths can be determined using McCabe’s Cyclomatic
Complexity, which provides an upper bound for the required number of tests. Path coverage
is more rigorous than statement or branch coverage, ensuring that the program’s logic is tested
thoroughly.
• All definition-use paths criterion: All possible definition-use paths (excluding cycles
or including only simple cycles) should be tested.
Data flow testing is especially helpful in programs with nested conditional and loop structures
because it focuses on variable usage correctness and detects data-related errors that other
techniques may miss.
Mutation Testing
Mutation testing is a fault-based white-box testing technique that checks the effectiveness of
a test suite in detecting real programming errors. In this approach, small changes called
mutations are introduced into the program to create mutants. These mutations could include
changes like modifying arithmetic or logical operators, deleting statements, or changing
variable values.
The mutated program is then tested against the existing test suite:
• If no test case detects it, the mutant is alive, and additional tests are designed to kill it—
unless the mutant is equivalent to the original program (no observable behavioral
change).
Mutation testing is highly effective but computationally expensive because it generates many
mutants, each requiring testing. Automated tools are often used to perform mutation testing
efficiently.
Conclusion:
White-box testing is an essential part of software testing because it verifies internal code
behavior and improves software quality. Combining various strategies—such as statement
coverage, branch coverage, path coverage, data flow testing, and mutation testing—provides a
stronger guarantee of correctness and helps in identifying defects that would be missed by
black-box testing alone.
Integration Testing
Integration testing is performed after individual modules have successfully passed unit testing.
While unit testing verifies that each module works in isolation, integration testing ensures that
modules interact correctly with one another. The primary goal is to identify interface-related
errors, such as incorrect parameter passing, mismatched data types, or improper
communication between modules.
A degenerate case of phased integration is big-bang testing, where all modules are combined
in one step. Incremental integration is generally preferred because it simplifies debugging and
improves test effectiveness.
System Testing
Once all modules have been integrated successfully, system testing is conducted to validate
the complete software system against its Software Requirements Specification (SRS). Unlike
integration testing, which focuses on interfaces between modules, system testing ensures that
the overall system satisfies both functional and non-functional requirements.
System testing is independent of the development approach—whether the system is object-
oriented or procedural—because the test cases are derived from the SRS, not the code. There
are three main types of system testing based on who performs the testing:
1. Alpha Testing
Alpha testing is performed by the development team’s test engineers in-house. Its goal is to
identify and fix defects before the product is released to external users.
2. Beta Testing
Beta testing is carried out by a small group of external users (friendly customers) who use the
software in a real-world environment. Feedback from beta testing is used to fix remaining
issues before the official release.
3. Acceptance Testing
Acceptance testing is conducted by the customer to determine whether the software meets all
specified requirements and is ready for deployment.
Before system testing begins, smoke testing is performed to ensure that the software is stable
enough for rigorous testing.
Smoke Testing
Smoke testing is a preliminary test that checks whether the basic functionalities of the software
work correctly. If the system fails smoke testing, further testing is postponed until critical issues
are fixed. For example, in a library automation system, smoke testing might verify that books
can be added, deleted, borrowed, and returned without errors.
Performance Testing
Performance testing ensures that the software meets its non-functional requirements. Various
types of performance testing include:
• Stress Testing: Evaluates system behavior under extreme conditions or peak loads. For
example, testing a system designed for 60 users by simulating 70 concurrent users.
• Volume Testing: Checks if the system can handle large volumes of data (e.g., verifying
if a compiler’s symbol table overflows with very large programs).
• Regression Testing: Ensures that recent bug fixes or enhancements do not introduce
new defects into previously working functionality.
• Recovery Testing: Evaluates how the system responds to faults such as power failures
or resource loss, and whether it can recover gracefully.
• Usability Testing: Validates the user interface (UI) to ensure it is user-friendly and
meets the requirements outlined in the SRS.
• Security Testing: Ensures that the system is protected from unauthorized access and
vulnerabilities through techniques like penetration testing and password cracking.
Error Seeding
Error seeding is a technique used to estimate the number of residual errors in the software and
to assess the effectiveness of the testing process. In this method, a known number of artificial
errors are deliberately inserted into the program. During testing, the number of seeded errors
detected is compared with the number of actual errors found. This data is then used to:
• Estimate the number of remaining errors in the software.
Conclusion:
Integration testing validates module interactions, while system testing validates the overall
system behavior. These testing levels ensure software quality, reliability, and compliance with
requirements before deployment.
Regression Testing
Regression testing is a critical software testing activity that spans across unit testing,
integration testing, and system testing. Unlike these testing levels, which focus on specific
aspects of the software (individual modules, module interfaces, or the overall system),
regression testing is a separate dimension of testing aimed at ensuring that recent changes or
fixes do not negatively impact the existing functionality of the software.
1. Detect unintended side effects: Ensure that code modifications do not break existing
functionality.
2. Validate bug fixes: Confirm that previously reported defects have been resolved
without affecting other areas.
3. Ensure software stability: Provide confidence that the system remains reliable after
updates.
1. Retest All
o Involves executing the entire existing test suite regardless of the scope of the
change.
o More cost-effective but requires careful impact analysis to identify the affected
areas.
• Automated regression testing tools (e.g., Selenium, JUnit, TestNG) can reduce effort
and improve speed.
• Test case maintenance, as changes in the software often require updating regression
test cases.
• Tool selection and setup for automated regression testing can require significant initial
investment.
• Builds confidence for both developers and customers before every release.
Conclusion:
Regression testing acts as a safety net that ensures software quality is preserved during every
stage of evolution. By combining selective test execution, prioritization, and automation,
regression testing enables faster and more reliable software releases, minimizing the risks
associated with frequent changes.
Debugging
Debugging is the process of identifying, analyzing, and fixing errors (or bugs) in a software
program. After a failure has been detected during testing or execution, the next step is to locate
the faulty program statements that caused the failure and correct them.
Debugging is one of the most time-consuming activities in software development and requires
not only technical skills but also logical reasoning, patience, and experience.
Debugging Approaches
Several techniques can be used to locate the source of a software error. Each has its strengths
and weaknesses, and often, programmers use a combination of them:
• This is the most common but also the least efficient approach.
• How it works: Developers insert print statements (or use logging) throughout the
program to display intermediate variable values and execution points, hoping to identify
where things go wrong.
• Single Stepping: Developers execute one instruction at a time while checking if the
program state matches the expected result.
Advantages:
Disadvantages:
2. Backtracking
• Start from the point of failure and trace the program execution backward to the source
of the error.
• This approach works well for small, linear programs.
• However, in large programs with many branching paths, backtracking may become
unmanageable.
Advantages:
Disadvantages:
• Difficult to apply in programs with complex control flows and multiple modules.
• Related technique: Software Fault Tree Analysis (SFTA) – builds a logical tree to
identify root causes of the error.
Advantages:
• Reduces random trial-and-error debugging.
Disadvantages:
4. Program Slicing
• A more systematic and focused method than backtracking.
• A slice of a program is defined as the set of statements that may affect the value of a
particular variable at a specific point in the program.
Advantages:
Disadvantages:
• Requires tools or frameworks to perform automated slicing effectively.
Debugging Guidelines
Effective debugging is not just about finding errors but also preventing the introduction of new
ones. Some general guidelines include:
o Novice programmers often fix error symptoms (e.g., changing a variable value)
without addressing the underlying root cause.
o Large simultaneous changes make it harder to identify the source of new errors.
o Sometimes a fresh perspective from another developer can help identify errors
faster.
Real-World Example
Suppose a banking application shows negative balances for certain transactions.
• Cause Elimination: Test if it’s a concurrency issue, incorrect fee calculation, or data
type overflow.
• Program Slicing: Focus only on the code slice that updates the balance variable.
Conclusion:
Debugging is an iterative and structured process that should be combined with regression
testing and best practices. Using a mix of debugging approaches, tools, and guidelines
significantly improves software reliability and reduces error correction costs.
o Uninitialized variables
o Dead code
Example: Tools like SonarQube, Pylint, and ESLint perform static analysis for code quality.
• Coverage Analysis:
• Performance Analysis:
• Test Optimization:
Example: Tools like JaCoCo (Java), Valgrind (C/C++), and Clover provide dynamic
analysis capabilities.
Output Code quality reports, Kiviat charts Coverage reports, performance data
Conclusion
Program analysis tools improve software quality by combining static checks for code
correctness with dynamic analysis for runtime validation. Using both approaches ensures
early detection of errors, better test coverage, and overall maintainability of software.
Symbolic Execution
Symbolic Execution is a program analysis technique that involves executing a program using
symbolic inputs instead of actual data values. Instead of assigning concrete values to program
variables, symbolic execution represents inputs as symbolic variables and then determines how
these symbolic inputs propagate through the program. This allows the tool to generate
expressions for program paths and automatically derive test cases that cover different program
paths.
During symbolic execution, the program is explored path by path. For every branch condition,
the tool evaluates the condition using symbolic variables and generates a path condition—a
set of constraints that must be satisfied for that path to be executed. A constraint solver is then
used to determine whether a feasible set of input values exists for these constraints.
For example, if a branch has a condition if (x > 10), the symbolic executor will generate two
paths: one with the constraint x > 10 and another with x ≤ 10. These constraints are later solved
to produce concrete test inputs.
• Automated test generation: Helps in generating precise test cases to achieve high path
coverage.
• Bug detection: Detects runtime errors like assertion violations or division by zero early.
• Increased confidence in software correctness: Ensures that many execution paths are
validated systematically.
Limitations of Symbolic Execution
• Path explosion problem: The number of possible paths grows exponentially with
program complexity.
• Handling external dependencies: Difficulties arise when analyzing programs with
complex libraries or system calls.
Model Checking
Model Checking is a formal verification technique used to verify whether a system model
satisfies a given specification, typically expressed in temporal logic. Unlike symbolic
execution, which focuses on analyzing program paths, model checking systematically explores
all possible states of a system to ensure correctness.
In model checking, the system is first represented as a state-transition model (e.g., finite state
machines). The properties to be verified are then expressed using formal specifications, such
as Linear Temporal Logic (LTL) or Computational Tree Logic (CTL). The model checker
automatically traverses all states and verifies whether the properties hold. If a property is
violated, the tool provides a counterexample trace showing the sequence of events that lead
to the violation, which greatly helps in debugging.
For example, in a concurrent program, a model checker might verify that “it is never possible
for two threads to access the same resource simultaneously,” ensuring mutual exclusion.
• State explosion problem: The number of states grows exponentially with system
complexity.
• Abstraction needed: Requires careful abstraction to simplify the model without losing
accuracy.
• Tool dependence: Effective model checking depends on powerful tools such as SPIN,
NuSMV, or UPPAAL.
Conclusion
• Symbolic Execution is best suited for generating path-specific test cases and analyzing
smaller program units.
• Both techniques complement each other and are widely used in modern software
verification to improve reliability and correctness.
Sl. Q.
Question Marks BL
No. No.
1 Q1 Define mistake, error, defect, and failure with suitable examples. 8 BL1
3 Q3 Explain drivers and stubs used in unit testing with an example. 8 BL3
Write short notes on: (a) Alpha, Beta, and Acceptance testing (b)
10 Q10 8 BL3
Smoke testing