Programming Fundamentals (Python/C++)

Chapter 13
Testing and Software Quality

Bachelors Program in Full Stack Development
Programming Fundamentals (Python/C++)   •   Bachelor

Testing and Software Quality

Introduction and Context

Software testing is the systematic evaluation of a program to discover defects, establish evidence of correctness, and reduce the risks associated with using the software. Software quality is broader than the absence of bugs. It includes suitability for purpose, reliability, maintainability, usability, security, performance, portability, and the ability to evolve safely.

For a full-stack developer, testing is not an activity performed only at the end of development. It is an engineering practice integrated with requirements analysis, design, implementation, code review, deployment, and maintenance. A well-tested program is easier to change because its tests provide executable evidence of intended behavior.

Testing cannot prove that a non-trivial program contains no defects. It can, however, increase confidence by examining representative inputs, important interactions, exceptional conditions, and previously failing situations. Quality therefore depends on both technical practices and professional judgment.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish verification, validation, testing, debugging, and quality assurance;
  • design unit, integration, boundary, regression, and property-oriented tests;
  • apply test-driven development and test doubles appropriately;
  • conduct effective code reviews using coding standards and maintainability criteria;
  • refactor code while preserving externally observable behavior;
  • use documentation, coverage measures, and other maintainability indicators critically;
  • create a basic continuous integration workflow; and
  • evaluate testing choices in an applied software-development scenario.

Foundations of Software Quality

Verification, validation, testing, and debugging

Verification asks whether the software is being built according to its specification and design: “Are we building the product right?” Examples include code review, static analysis, and checking an implementation against an interface contract.

Validation asks whether the resulting software solves the users’ problem: “Are we building the right product?” User acceptance testing, usability evaluation, and checking realistic workflows support validation.

Testing executes a system or component with selected inputs and compares observed behavior with expected behavior. A test may pass without proving universal correctness, because it examines only selected cases.

Debugging is the process of locating, understanding, and correcting the cause of a failure. Testing reveals evidence of a problem; debugging investigates and repairs it. A corrected defect should normally be captured by a regression test.

Quality assurance is a broader organizational approach that establishes processes intended to prevent defects and improve development activities. Quality control focuses more directly on evaluating deliverables. Testing contributes to both, but is not identical to either.

The cost and visibility of defects

Defects discovered early are generally less expensive to correct because fewer components, assumptions, and stakeholders are affected. A defect in a small pure function may be fixed immediately; the same defect discovered after deployment may require data repair, communication with users, emergency release procedures, and reputational recovery. This is a general engineering tendency rather than a universal numerical law.

Testing should therefore be risk-based. A payment calculation, authentication boundary, or data-deletion operation deserves more scrutiny than an easily replaceable display label. Risk reflects both the likelihood of failure and its consequence.

A Practical Testing Model

The test pyramid

The test pyramid is a heuristic for balancing test types:

1. Unit tests form the broad base. They are fast, isolated, and numerous.

2. Integration tests verify interactions among modules, databases, services, or frameworks.

3. End-to-end tests exercise complete user workflows through multiple layers.

The pyramid is not a rigid law. A system may require contract tests, component tests, security tests, performance tests, or exploratory testing. The central principle is to obtain fast and precise feedback at lower levels while reserving slower, more fragile tests for behavior that cannot be checked lower in the system.

Test cases and oracles

A test case includes a setup, an action, and an assertion about the expected result. The expected-result mechanism is called a test oracle. An oracle may be an exact expected value, an invariant, a reference implementation, a schema, or a comparison with a trusted external service.

A useful test is:

  • Focused: it examines one behavior or a small coherent behavior;
  • Repeatable: it gives the same result under the same conditions;
  • Independent: it does not rely unnecessarily on another test’s execution order;
  • Readable: its purpose is clear;
  • Fast enough: it provides feedback at the appropriate development level;
  • Diagnostic: when it fails, the likely cause is reasonably clear.

A test that passes but does not meaningfully distinguish correct from incorrect behavior has little value. For example, asserting only that a function does not crash may miss an incorrect result.

Unit Testing in Python and C++

A unit is a small testable part of a program, commonly a function, class method, or module. Unit tests should isolate the unit from unrelated resources where practical. Pure functions are especially easy to test because their result depends only on their inputs.

A Python example using `unittest` is:

```python

from shipping import shipping_cost

import unittest

class ShippingCostTests(unittest.TestCase):

def test_free_shipping_at_threshold(self):

self.assertEqual(shipping_cost(50.00), 0.00)

def test_standard_charge_below_threshold(self):

self.assertEqual(shipping_cost(49.99), 5.00)

def test_negative_total_is_rejected(self):

with self.assertRaises(ValueError):

shipping_cost(-1.00)

```

The function’s contract might be: a non-negative order total below 50 costs 5 monetary units; an order total of at least 50 costs zero; a negative total is invalid. The tests cover ordinary behavior, a threshold, and invalid input.

In C++, a framework such as GoogleTest provides equivalent facilities:

```cpp

TEST(ShippingCost, FreeAtThreshold) {

EXPECT_DOUBLE_EQ(shippingCost(50.00), 0.00);

}

```

Floating-point comparisons require care. Exact equality is appropriate only when the computation and representation make it reliable. For calculated decimal quantities, a tolerance-based assertion is often more appropriate, provided the tolerance is justified by the domain.

Arrange, Act, Assert

The Arrange–Act–Assert (AAA) pattern improves readability:

  • Arrange inputs, objects, and dependencies.
  • Act by invoking the behavior under test.
  • Assert the expected result or observable effect.

A test should generally verify observable behavior rather than private implementation details. Tests coupled to internal names or incidental call sequences can prevent beneficial refactoring.

Test doubles

A test double substitutes for a real dependency:

  • A stub supplies predetermined answers.
  • A mock verifies expected interactions.
  • A fake is a lightweight working implementation, such as an in-memory repository.
  • A spy records calls for later inspection.

Use doubles to isolate a unit from slow, unreliable, expensive, or destructive dependencies. Do not mock every object automatically. Excessive mocking can produce tests that verify an imagined implementation rather than meaningful behavior.

Integration and End-to-End Testing

Integration tests examine whether components work together. Examples include testing a repository with a real test database, checking serialization between an API and a client, or verifying that an authentication middleware correctly communicates with a session store.

Integration tests expose defects that unit tests may miss:

  • incompatible data types or schemas;
  • incorrect configuration;
  • transaction and rollback errors;
  • authentication and authorization mismatches;
  • differences between a fake dependency and the production service.

A reliable integration environment should be isolated, repeatable, and populated with controlled data. Database tests should clean up after themselves or use disposable databases. External services may be replaced with contract-tested simulators, but the real integration boundary must also be tested periodically.

End-to-end tests exercise complete workflows, such as registering a user, adding an item to a cart, and completing checkout. They provide strong evidence of system-level behavior but are slower and more vulnerable to environmental failures. A small number of high-value end-to-end tests should complement, not replace, lower-level tests.

Boundary, Negative, and Error-Path Testing

Many defects occur at boundaries, where behavior changes or constraints meet. Boundary-value analysis tests values just below, at, and just above a limit. If a password must contain 8–64 characters, useful cases include lengths 7, 8, 9, 63, 64, and 65. The exact set depends on the specification, but testing only a typical value is inadequate.

Important boundary categories include:

  • minimum and maximum numeric values;
  • empty, one-item, and large collections;
  • zero, negative, and unusually large amounts;
  • date and time transitions, including time zones;
  • missing, malformed, and extra fields;
  • permissions at authorization boundaries;
  • network timeouts, partial responses, and retry limits.

Negative testing deliberately supplies invalid or hostile inputs. It checks whether the system rejects them safely and communicates an appropriate error. An error path should not expose passwords, tokens, database details, or stack traces to an untrusted user.

Partitioning inputs into equivalence classes reduces unnecessary tests. For example, an age field might have classes “negative,” “valid adult,” “valid minor,” “non-numeric,” and “missing.” At least one representative from each meaningful class should be selected, with additional boundary cases where risk warrants them.

Regression Testing and Defect Prevention

A regression is a failure introduced when a change damages previously working behavior. A regression suite contains tests that protect established behavior. When a defect is found, the team should normally:

1. reproduce the defect;

2. write a test that fails for the defect;

3. correct the implementation;

4. run the new and existing tests;

5. retain the test in the regression suite.

This practice prevents the same defect from silently returning. Regression suites should be maintained: obsolete tests should be removed, slow tests optimized or reclassified, and important uncovered risks added.

Property-Oriented Testing

Example-based testing specifies selected input-output pairs. Property-oriented testing specifies general relationships that should hold for many generated inputs. A property is not merely a random assertion; it is a domain rule or invariant.

For a sorting function, useful properties include:

  • the output has the same elements as the input, including duplicates;
  • the output is ordered according to the chosen comparison rule;
  • sorting an already sorted list does not change it;
  • sorting twice gives the same result as sorting once.

For a reversible encoding, a property may be `decode(encode(x)) == x` for every valid `x`. Property-based tools such as Hypothesis in Python can generate inputs and shrink a failure to a simpler counterexample. In C++, frameworks such as RapidCheck support similar techniques.

Properties must respect preconditions. A sorting property may apply to all finite sequences, while a division property applies only when the divisor is nonzero. Generated tests should include domain-relevant distributions; purely uniform random values may overlook important boundaries.

Test-Driven Development and Test-Driven Thinking

In test-driven development (TDD), the developer works through a short cycle:

1. Red: write a test for a behavior that is not yet implemented.

2. Green: implement the simplest code that makes the test pass.

3. Refactor: improve design while keeping the tests passing.

TDD encourages explicit requirements, small increments, and designs that are observable and modular. It does not mean that every test must be written before every design decision, nor does it eliminate exploratory testing, system testing, or usability evaluation. Test-driven thinking can be applied even when a team uses a broader development process: state the expected behavior first, identify failure conditions, and make the requirement executable where practical.

Code Reviews, Standards, and Documentation

Automated tests cannot detect every problem. Code review provides human examination of correctness, clarity, security, and design. A useful review checks:

  • Does the code satisfy the stated requirement?
  • Are invalid inputs and failure modes handled?
  • Are tests meaningful and maintainable?
  • Are names, functions, and modules appropriately scoped?
  • Are security, privacy, and resource implications considered?
  • Is duplicated or unnecessarily complex logic present?
  • Does the change include appropriate documentation and migration information?

Coding standards should define naming, formatting, error handling, documentation expectations, and language-specific practices. Automated formatters and linters enforce mechanical rules consistently, allowing reviewers to focus on design and behavior.

Documentation should explain purpose, contracts, assumptions, side effects, configuration, and examples. Comments should clarify why a non-obvious decision exists, not merely repeat what the code says. Public APIs should document valid inputs, outputs, exceptions, units, and compatibility expectations.

Refactoring and Maintainability

Refactoring is a behavior-preserving transformation of code structure. Common refactorings include extracting a function, renaming a misleading variable, replacing duplicated logic with a well-defined abstraction, and simplifying conditional expressions.

The safe sequence is usually:

1. establish a reliable test baseline;

2. make one small structural change;

3. run relevant tests and static checks;

4. inspect the resulting design;

5. commit a focused change.

Tests do not guarantee that refactoring preserves every behavior, especially behavior not covered by tests. Public contracts, security properties, performance constraints, and operational effects must also be considered.

Maintainability can be discussed using indicators such as cyclomatic complexity, duplication, dependency coupling, change failure rate, defect density, and test execution time. These are signals, not absolute quality scores. For example, cyclomatic complexity counts independent decision paths in a control-flow graph under a particular counting convention; a high value suggests that a function may be difficult to understand and test, but a low value does not guarantee good design. Metrics should support investigation rather than become targets that encourage gaming.

Coverage measures the proportion of executed code or branches under a test run. Statement coverage can be expressed as:

\[

\text{Statement coverage} = \frac{\text{executed statements}}{\text{total statements}} \times 100\%.

\]

This equation is a descriptive ratio for a defined test suite and measurement tool. It does not measure the correctness of assertions, the quality of test data, or unexecuted requirements. High coverage with weak assertions can provide false confidence; lower coverage may be acceptable when the uncovered code is unreachable, generated, or low risk. Branch and condition coverage can reveal gaps that statement coverage hides.

Continuous Integration

Continuous integration (CI) is the practice of frequently merging changes into a shared repository and automatically building and testing them. A basic CI pipeline may perform the following steps:

1. obtain a clean checkout;

2. install pinned dependencies;

3. format-check and lint the code;

4. build the application;

5. run unit tests;

6. run selected integration tests;

7. publish test and coverage reports;

8. make the result visible to the team.

A pipeline should fail for reproducible quality violations, while transient infrastructure failures should be distinguished and investigated. Secrets must be supplied securely rather than committed to source control. CI environments should approximate production where that affects behavior, but should remain deterministic and suitably isolated.

CI is not continuous delivery by itself. Continuous delivery adds the capability to release safely; continuous deployment automatically releases qualifying changes. These practices require operational monitoring, rollback plans, access control, and appropriate approval policies.

Applied Case: An Illustrative Online Course Enrollment Service

Background and problem

Consider a fictional university course-enrollment service. Students can enroll if a course is open, prerequisites are satisfied, the student is not already enrolled, and capacity remains. The service exposes an API and stores records in a relational database.

A production incident occurs during registration: two students are both accepted for the final seat. A second defect allows a student to enroll twice when two browser requests arrive quickly.

Analysis

Unit tests covered the eligibility function with ordinary inputs, but did not test the capacity boundary or duplicate concurrent requests. An end-to-end test checked a single enrollment flow, but not database transaction behavior. The code review focused on API formatting and did not ask whether the capacity check and insertion were atomic.

Approach and solution

The team first adds unit tests for zero capacity, one remaining seat, a full course, an unmet prerequisite, and an existing enrollment. It adds a regression test for duplicate enrollment. Property-oriented tests verify that a successful enrollment decreases available capacity by one and that an unsuccessful enrollment does not change it, subject to the database transaction model.

Integration tests use a disposable database and execute concurrent enrollment attempts against a course with one seat. The implementation then places the capacity check and enrollment insertion within a transaction, applies an appropriate database constraint against duplicate student-course pairs, and handles a constraint failure as a normal conflict response rather than an unhandled server error. The precise locking or isolation strategy must be selected according to the database system and workload; the important requirement is that the business invariant is enforced atomically.

The CI pipeline runs fast unit tests on every change and the database integration suite on protected branches. A review checklist is updated to include invariants, concurrency, authorization, and migration impact.

Outcome and learning points

The new tests detect both duplicate requests and over-enrollment before release. The database constraint provides a second line of defense if application logic changes. The case demonstrates that quality is a system property: unit tests, integration tests, database design, review, and CI work together. It also shows why tests should be derived from business invariants and failure risks, not only from the visible “happy path.”

Challenges, Ethics, and Governance

Testing can introduce its own risks. Test data must not expose real personal information unnecessarily; anonymization and access controls are essential. Security tests should be authorized and conducted within defined scope. Accessibility, fairness, and inclusive behavior should be tested where software affects people with different abilities, languages, or circumstances.

Teams should avoid using production data casually, treating coverage as the sole performance goal, or suppressing failing tests merely to keep a pipeline green. Flaky tests consume engineering time and undermine trust; they should be repaired, isolated temporarily with an owner and deadline, or removed when their premise is invalid.

AI-assisted code generation can accelerate implementation, but generated code and tests require human review. An AI-generated test may reproduce the same misunderstanding as the implementation, omit security risks, or encode an incorrect assumption. Teams remain responsible for licensing, privacy, security, correctness, and maintainability.

Best Practices

  • Test behavior and contracts rather than private implementation details.
  • Start with risks, invariants, boundaries, and failure modes.
  • Keep unit tests fast and deterministic; isolate external dependencies.
  • Use integration tests for real interfaces and persistence behavior.
  • Add a regression test for every confirmed defect when practical.
  • Use property-based testing for broad input spaces and general invariants.
  • Review tests as seriously as production code.
  • Refactor in small, verified steps.
  • Interpret coverage and complexity metrics in context.
  • Keep CI reproducible, visible, secure, and fast enough to support frequent feedback.
  • Treat privacy, accessibility, security, and operational behavior as quality concerns.

Chapter Summary

Software quality includes functional correctness, reliability, security, usability, performance, and maintainability. Verification checks conformance to specifications, validation checks fitness for user needs, testing supplies evidence through selected executions, and debugging repairs causes of failure. Unit, integration, and end-to-end tests provide different levels of isolation and confidence. Boundary-value, negative, regression, and property-oriented testing expose risks that ordinary happy-path examples miss.

Test-driven thinking makes expected behavior explicit, while code review, standards, documentation, refactoring, and maintainability metrics support long-term quality. Coverage and complexity are useful indicators but cannot replace judgment. Continuous integration automates repeatable feedback through builds, checks, and tests. Finally, quality is socio-technical: secure data handling, accessibility, ethical use of tools, sound database constraints, and responsible governance are part of professional software development.

Key Terms

Verification: Checking whether software conforms to specified requirements or design.

Validation: Checking whether software meets real user and business needs.

Test oracle: A mechanism for determining the expected result of a test.

Unit test: A test of a small isolated program component.

Integration test: A test of interactions among components or external systems.

End-to-end test: A test of a complete workflow across the application stack.

Boundary-value analysis: Testing values at, just below, and just above important limits.

Equivalence class: A group of inputs expected to produce similar behavior.

Regression test: A test that protects behavior from being broken by later changes.

Property-based testing: Testing general rules over many generated inputs.

Test double: A substitute for a dependency used during testing.

Mock: A test double that verifies expected interactions.

Test-driven development: A red-green-refactor development cycle guided by tests.

Refactoring: Restructuring code without intentionally changing its behavior.

Code review: Human examination of code and related tests before integration.

Cyclomatic complexity: A control-flow-based indicator of independent decision paths.

Coverage: A measurement of which code elements were exercised by tests.

Continuous integration: Frequent integration supported by automated build and test checks.

Flaky test: A test whose result changes without a relevant code or input change.

Invariant: A condition that should remain true throughout or after an operation.

Review Questions

Short Answer Questions

1. Distinguish verification from validation.

2. What is the purpose of a regression test?

3. State the three stages of the Arrange–Act–Assert pattern.

4. Why can high statement coverage give false confidence?

5. Give two examples of boundary values for a field constrained to 1–100.

Long Answer Questions

1. Explain the test pyramid and discuss the strengths and limitations of unit, integration, and end-to-end tests.

2. Describe test-driven development and analyze situations in which TDD should be supplemented by exploratory or usability testing.

3. Compare stubs, mocks, fakes, and spies, including an appropriate use case for each.

4. Explain how code review, coding standards, documentation, and refactoring contribute to maintainability.

5. Design a basic CI pipeline for a Python or C++ web application and justify the order of its stages.

Analytical / Application Questions

1. An API accepts a quantity from 0 through 500 inclusive. Design equivalence classes and boundary-value tests, including invalid inputs and expected outcomes.

2. A shopping-cart service passes all unit tests but occasionally charges a customer twice. Analyze likely testing gaps and propose unit, integration, regression, and property-oriented tests.

3. A team has 98% statement coverage but frequent production defects and many flaky tests. Evaluate what the metric fails to show and propose a prioritized quality-improvement plan.

Multiple Choice Questions with Answers

Question 1

Which statement best distinguishes testing from debugging?

A. Testing prevents all defects, while debugging documents requirements

B. Testing reveals evidence of failures, while debugging locates and fixes their causes

C. Testing is performed only by users, while debugging is performed only by managers

D. Testing checks style, while debugging checks licensing

Correct Answer: B. Testing reveals evidence of failures, while debugging locates and fixes their causes

Question 2

Which test type primarily verifies interactions between an application and a database?

A. Unit test

B. Integration test

C. Mutation test

D. Lint test

Correct Answer: B. Integration test

Question 3

For a rule requiring a password length from 8 through 64 characters inclusive, which set best represents boundary testing?

A. 1, 2, 3, and 4

B. 8, 16, 32, and 64 only

C. 7, 8, 9, 63, 64, and 65

D. 10, 20, 30, and 40 only

Correct Answer: C. 7, 8, 9, 63, 64, and 65

Question 4

What is the main purpose of adding a regression test after fixing a defect?

A. To guarantee that no future defects are possible

B. To ensure the same failure is detected if it reappears

C. To replace all integration tests

D. To increase code size for maintainability

Correct Answer: B. To ensure the same failure is detected if it reappears

Question 5

Which is a property of a sorting function?

A. It always uses quicksort internally

B. It always allocates exactly one array

C. Its output is ordered and contains the same elements as the input

D. It prints the input before sorting

Correct Answer: C. Its output is ordered and contains the same elements as the input

Question 6

In test-driven development, what normally occurs in the red stage?

A. A new test is written and fails because the behavior is not implemented

B. All production code is deleted

C. The CI server is permanently disabled

D. The database is migrated without testing

Correct Answer: A. A new test is written and fails because the behavior is not implemented

Question 7

Which test double is generally a lightweight working implementation, such as an in-memory repository?

A. Fake

B. Spy

C. Mock

D. Stub

Correct Answer: A. Fake

Question 8

What does statement coverage measure?

A. The percentage of requirements accepted by users

B. The percentage of statements executed by a specified test run

C. The number of defects corrected per developer

D. The percentage of branches proven correct mathematically

Correct Answer: B. The percentage of statements executed by a specified test run

Question 9

Which practice is most directly associated with continuous integration?

A. Merging changes infrequently after a long release cycle

B. Running automated builds and tests when changes are integrated

C. Testing only the user interface manually

D. Storing credentials in source control for repeatability

Correct Answer: B. Running automated builds and tests when changes are integrated

Question 10

Why should a capacity check and enrollment insertion often be handled atomically?

A. To improve variable naming

B. To prevent concurrent requests from violating the capacity invariant

C. To eliminate the need for all unit tests

D. To ensure every request succeeds

Correct Answer: B. To prevent concurrent requests from violating the capacity invariant

Programming Fundamentals (Python/C++)
Chapter 13 of 15