Object-Oriented Programming (Java/Python)

Chapter 11
Testing, Debugging, and Quality Analytics

Bachelors Program in Full Stack Development
Object-Oriented Programming (Java/Python)   •   Bachelor

Testing, Debugging, and Quality Analytics

Introduction

Reliable software is not produced merely by writing code that appears to work. It is produced through systematic evidence that the code satisfies its requirements, remains maintainable, and behaves acceptably when conditions change. In object-oriented systems, this evidence must address individual methods and classes, collaborations among objects, external dependencies, error handling, performance, and the quality of the development process.

Testing is the controlled execution of software to find failures or provide evidence about specified behaviour. Debugging is the investigation and correction of the causes of failures. Quality analytics uses measurements—such as coverage, defect trends, mutation scores, and build outcomes—to support engineering decisions. These activities are related but not interchangeable: a test can expose a failure, debugging can locate its cause, and analytics can reveal whether the overall quality strategy is improving.

This chapter applies these ideas to Java with JUnit and Python with pytest. The examples use an object-oriented order-processing domain, but the principles generalize to web applications, APIs, services, and full-stack systems.

Learning objectives

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

  • distinguish unit, integration, system, acceptance, and regression testing;
  • design tests using equivalence classes, boundary values, state transitions, and properties;
  • write unit and integration tests with JUnit and pytest;
  • use fixtures, test doubles, mocks, and dependency injection appropriately;
  • apply logging and systematic debugging techniques;
  • interpret coverage, mutation testing, static-analysis, and quality metrics;
  • profile code without confusing performance measurement with functional testing;
  • design a quality pipeline suitable for continuous integration (CI);
  • evaluate the limitations, ethical implications, and governance requirements of automated quality controls.

1. A model of software quality

A useful quality model considers several dimensions:

DimensionCentral questionTypical evidence
Functional correctnessDoes the system produce the required result?Unit, integration, and acceptance tests
ReliabilityDoes it continue to behave correctly over time and under expected faults?Failure-rate data, retries, resilience tests
MaintainabilityCan developers understand and change it safely?Static analysis, complexity, review findings
PerformanceDoes it meet response-time and resource constraints?Profiling and load tests
SecurityDoes it protect data and resist misuse?Security tests, scans, threat modelling
Usability and accessibilityCan intended users operate it effectively?User testing and accessibility evaluation

No single metric represents quality. For example, high line coverage does not prove that assertions are meaningful, and a clean static-analysis report does not prove that business rules are correct. Quality assurance therefore combines human reasoning with automated evidence.

The testing process is often described as a test pyramid. Many fast, isolated unit tests form the base; fewer integration tests verify collaboration with databases, queues, or APIs; and a small number of slower end-to-end tests verify complete user journeys. The pyramid is a risk-management model, not an absolute law. A system with substantial data-access logic may reasonably require a larger integration layer, but excessive end-to-end testing usually makes feedback slow and failures difficult to diagnose.

2. Test levels and test design

2.1 Unit testing

A unit test examines a small, logically coherent unit—usually a method or class—in isolation from replaceable dependencies. It should be fast, deterministic, and able to identify the failed behaviour precisely. A unit test normally follows Arrange–Act–Assert:

  1. Arrange: construct objects and inputs;
  2. Act: invoke the behaviour under test;
  3. Assert: compare the observable result with the expected result.

A good unit test tests a public behaviour rather than private implementation details. Tests that inspect every private variable can prevent legitimate refactoring.

2.2 Integration and system testing

An integration test verifies that components work together. Examples include a service communicating with a real repository, an application writing to a test database, or an API serializing and deserializing a domain object. A system or end-to-end test exercises a complete deployed path, often through a user interface or public API.

Integration tests provide realism but usually require more setup, are slower, and can fail because of environmental conditions. They should use isolated test data, controlled configuration, and reliable cleanup. External services should generally be replaced by contract-tested stubs or dedicated test environments.

2.3 Regression and acceptance testing

A regression test prevents a previously corrected defect from returning. An acceptance test checks whether a feature satisfies business or user requirements. Acceptance criteria should be observable and testable—for example, “an expired coupon is rejected with an explanatory error,” rather than “coupon handling is robust.”

2.4 Test design techniques

Equivalence partitioning divides inputs into groups expected to behave similarly. For an age field requiring values from 18 through 120, useful partitions include below 18, 18–120, and above 120.

Boundary-value analysis tests values at and immediately around limits: 17, 18, 19, 119, 120, and 121. Defects frequently occur at comparison operators such as < versus ≤.

Decision-table testing represents combinations of conditions and expected outcomes. It is valuable for discount rules, authorization policies, and tax calculations.

State-transition testing checks legal and illegal changes between states. An order might transition from NEW to PAID, then to SHIPPED; it should not move directly from NEW to SHIPPED if payment is required.

Negative testing deliberately supplies invalid, missing, malformed, or unauthorized inputs. It is essential for robust APIs and security-sensitive code.

3. Unit testing with JUnit and pytest

3.1 JUnit 5

JUnit 5 uses annotations such as @Test, @BeforeEach, and @ParameterizedTest. A compact Java example is:

class PriceCalculatorTest {
    @Test
    void appliesTenPercentDiscount() {
        PriceCalculator calculator = new PriceCalculator();

        Money result = calculator.finalPrice(new Money("100.00"), 0.10);

        assertEquals(new Money("90.00"), result);
    }

    @ParameterizedTest
    @CsvSource({"0, 100.00", "0.10, 90.00"})
    void calculatesExpectedPrice(double discount, String expected) {
        PriceCalculator calculator = new PriceCalculator();
        assertEquals(new Money(expected),
            calculator.finalPrice(new Money("100.00"), discount));
    }
}

The production code should use a monetary value type such as BigDecimal rather than binary floating-point values when exact decimal currency is required. The test also assumes that a discount is represented as a fraction between 0 and 1 and that invalid discounts are rejected by a separate test.

JUnit assertions should explain the intended contract. assertThrows verifies exceptional behaviour:

@Test
void rejectsDiscountAboveOne() {
    PriceCalculator calculator = new PriceCalculator();

    assertThrows(IllegalArgumentException.class,
        () -> calculator.finalPrice(new Money("100.00"), 1.2));
}

Parameterized tests reduce duplication while retaining multiple examples. They should not become a substitute for explaining why each partition matters.

3.2 pytest

pytest discovers functions whose names begin with test_ and uses plain Python assertions:

import pytest
from pricing import final_price

@pytest.mark.parametrize(
    "discount, expected", [(0, 100.00), (0.10, 90.00)]
)
def test_final_price(discount, expected):
    assert final_price(100.00, discount) == pytest.approx(expected)

def test_rejects_discount_above_one():
    with pytest.raises(ValueError):
        final_price(100.00, 1.2)

pytest.approx is appropriate when the implementation uses floating-point arithmetic and a tolerance is acceptable. For currency, decimal arithmetic is preferable and exact comparison may be appropriate.

A fixture supplies reusable setup and controlled resources:

import pytest
from repository import InMemoryOrderRepository

@pytest.fixture
def repository():
    repo = InMemoryOrderRepository()
    yield repo
    repo.clear()

def test_saves_and_loads_order(repository):
    repository.save({"id": "A1", "status": "NEW"})
    assert repository.find("A1")["status"] == "NEW"

Fixtures can have scopes such as function, class, module, or session. Broader scopes improve speed but increase the risk of shared mutable state and order-dependent tests. Function-scoped fixtures are a safe default.

4. Test doubles, mocks, and dependency injection

A test double is a replacement for a real collaborator. A stub returns controlled values, a fake provides a lightweight working implementation, a spy records calls, and a mock verifies expected interactions. The terms are sometimes used loosely, but the distinction clarifies intent.

Suppose an OrderService depends on a payment gateway. Injecting the dependency makes the service testable:

class OrderService:
    def __init__(self, gateway, repository):
        self.gateway = gateway
        self.repository = repository

    def pay(self, order_id, amount):
        receipt = self.gateway.charge(amount)
        self.repository.mark_paid(order_id, receipt)
        return receipt

A pytest mock can isolate the gateway:

def test_pay_marks_order_paid(mocker):
    gateway = mocker.Mock()
    gateway.charge.return_value = "R-7"
    repository = mocker.Mock()
    service = OrderService(gateway, repository)

    result = service.pay("A1", 25)

    assert result == "R-7"
    gateway.charge.assert_called_once_with(25)
    repository.mark_paid.assert_called_once_with("A1", "R-7")

JUnit commonly uses Mockito in the same way:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock PaymentGateway gateway;
    @Mock OrderRepository repository;

    @Test
    void paymentMarksOrderPaid() {
        when(gateway.charge(25)).thenReturn("R-7");
        OrderService service = new OrderService(gateway, repository);

        assertEquals("R-7", service.pay("A1", 25));
        verify(repository).markPaid("A1", "R-7");
    }
}

Mocking every collaborator is harmful when it merely verifies implementation details. Prefer real value objects and simple fakes where they are inexpensive. Mock external boundaries—payment providers, clocks, email services—when their behaviour is not the subject of the unit test. Integration tests should verify the real adapter against a controlled environment.

5. Property-based testing

Example-based tests specify selected input-output pairs. Property-based testing generates many inputs and checks a general property that should always hold. For a reversible transformation decode(encode(x)), the property is:

decode(encode(x)) = x

This property assumes that x is within the supported domain and that encoding is lossless for that domain. In Python, Hypothesis can generate structured values:

from hypothesis import given, strategies as st
from identifiers import encode, decode

@given(st.text(min_size=0, max_size=100))
def test_encoding_is_reversible(value):
    assert decode(encode(value)) == value

Useful properties include invariants, idempotence, monotonicity, conservation, and round-trip relationships. For an idempotent operation, applying it twice has the same result as applying it once: f(f(x)) = f(x). Property-based tests do not eliminate the need for examples: examples document important business cases, while generated tests explore a broad input space.

Generated failures should be reproducible through the framework’s seed or saved example. Strategies must reflect valid domain assumptions; otherwise, failures may concern unsupported inputs rather than defects.

6. Debugging and observability

A failing test is evidence, not an explanation. A disciplined debugging workflow is:

  1. reproduce the failure reliably;
  2. read the complete failure message and stack trace;
  3. reduce the input to a minimal failing case;
  4. identify the first incorrect value, not merely the final exception;
  5. form a hypothesis about the cause;
  6. use a debugger, targeted logs, or instrumentation to test the hypothesis;
  7. correct the cause and add a regression test;
  8. run nearby tests and the full suite.

A debugger provides breakpoints, stepping, call-stack inspection, and expression evaluation. Conditional breakpoints are useful for failures occurring only for particular identifiers or states. Debugging should not be reduced to adding random print statements; uncontrolled output can hide sequence and concurrency problems.

Logging is part of observability. A useful log event includes timestamp, severity, component, correlation identifier, and relevant non-sensitive context. Levels commonly include DEBUG, INFO, WARNING, and ERROR. Logs should describe events and decisions, not expose passwords, payment credentials, access tokens, or unnecessary personal data. Structured logs, such as JSON, support searching and aggregation, but they do not replace traces and metrics in distributed systems.

When a defect is found in production, the durable response includes a regression test, a root-cause analysis, and an assessment of whether monitoring or process changes are needed. Blaming individuals is less effective than improving the system that allowed the defect to escape.

7. Static analysis, coverage, and mutation testing

Static analysis examines source code without executing it. Linters identify style and suspicious constructs; type checkers detect inconsistent types; security analyzers identify vulnerable patterns; and complexity tools identify code that may be difficult to maintain. Java projects may use Checkstyle, SpotBugs, or PMD; Python projects commonly use Ruff, Pylint, mypy, or Bandit. Findings should be triaged: an ignored warning should have a documented reason, not a silent global suppression.

Coverage measures which program elements were executed by tests. Common measures include line, statement, branch, and condition coverage. A basic line-coverage percentage is:

Cline = (Lexecuted ÷ Ltotal) × 100%

Here, Lexecuted is the number of executable lines executed by the test suite and Ltotal is the number of executable lines measured. The measure depends on the tool’s definition of executable code and says nothing directly about assertion quality. Branch coverage is often more informative for decisions because it asks whether outcomes such as both true and false have executed.

For example, if 84 of 100 measured lines execute, Cline = 84%. This does not imply that 84% of requirements are correct. Coverage thresholds should be risk-informed, and uncovered code should be reviewed rather than automatically deleted or excluded.

Mutation testing evaluates test sensitivity by making small artificial changes, called mutants, such as replacing > with ≥ or deleting a statement. The mutation score is commonly calculated as:

Smutation = (Mkilled ÷ Mvalid) × 100%

Mkilled is the number of valid mutants detected by tests, and Mvalid is the number of valid mutants considered. Equivalent mutants—changes that do not alter observable behaviour—should be excluded or recognized because tests cannot kill them meaningfully. Mutation testing is computationally expensive, so it is often applied to critical modules or scheduled in CI rather than run on every local change.

8. Profiling and quality metrics

Profiling measures runtime behaviour to locate performance hotspots. CPU profilers identify time-consuming functions; memory profilers identify allocation and retention; database profilers expose expensive queries; and statistical profilers sample call stacks with relatively low overhead. Profiling should use representative workloads because an artificial microbenchmark may optimize the wrong problem.

Functional tests ask whether the result is correct; profiling asks where time or memory is spent. A faster incorrect program is not an improvement. Performance tests should define workload, environment, warm-up assumptions, sample count, and acceptable thresholds. Results can vary because of hardware, operating-system scheduling, garbage collection, caching, and network conditions.

Useful quality analytics include:

  • escaped defects per release or per feature;
  • test failure rate and flaky-test rate;
  • mean time to repair a failed build;
  • lead time from change to deployment;
  • code-review rework and static-analysis trends;
  • coverage and mutation trends for critical modules;
  • deployment rollback or change-failure rate.

A defect-density measure may be written as:

D = Ndefects ÷ S

where Ndefects is the number of defined defects in a specified period and S is a size measure such as thousands of lines of code or function points. The unit is defects per chosen size unit. Comparisons are meaningful only when defect definitions, observation periods, and size measures are consistent. Metrics can create harmful incentives: a team rewarded for coverage may write superficial tests, while a team rewarded only for low defect counts may under-report defects.

9. Continuous integration and the quality gate

A CI pipeline should provide rapid, trustworthy feedback whenever code changes. A practical sequence is:

  1. install pinned dependencies and create a clean environment;
  2. compile or package the application;
  3. run formatting and static-analysis checks;
  4. run fast unit tests in parallel where safe;
  5. publish test and coverage reports;
  6. run integration tests using isolated services or containers;
  7. perform security and dependency checks;
  8. run selected performance or mutation tests on an appropriate schedule;
  9. build an artifact only if required quality gates pass.

A quality gate should be explicit and version-controlled. Examples include “no high-severity security findings,” “all tests pass,” and “coverage for payment code does not fall below the agreed threshold.” Flaky tests should be quarantined temporarily with an owner and deadline; silently rerunning failures can conceal real nondeterminism.

Tests must be independent, deterministic, and repeatable. Time, randomness, network access, and environment variables should be controlled or injected. Parallel execution requires isolation of files, ports, databases, and global state. CI secrets must be stored in a secret-management system and must not appear in logs.

10. Illustrative case: testing an online order service

Background

An illustrative company operates an online bookstore. Its OrderService validates an order, charges a payment gateway, stores the order, and sends a confirmation message. A release caused two incidents: a coupon at its expiry boundary was incorrectly accepted, and a payment timeout caused a customer to be charged without a recorded order status.

Problem analysis

The original suite contained happy-path unit tests but no boundary test for coupon expiry, no integration test for transaction behaviour, and no contract test for the payment adapter. A line-coverage report showed 91%, but mutation testing revealed that changing the expiry comparison did not fail any test. The metric had encouraged execution without adequate assertions.

Approach

The team first wrote a decision table for coupon validity: valid code, order minimum, current time before expiry, and current time at or after expiry. Boundary tests covered the exact expiry instant and one instant on either side. The coupon clock was injected so tests did not depend on wall-clock time.

Second, the service was separated from infrastructure through interfaces for payment, persistence, and messaging. Unit tests used a payment stub and verified that a rejected coupon caused no charge. Integration tests used a disposable database and a fake payment server. A failure test verified that a gateway timeout produced a safe error and that the persistence transaction did not leave an order falsely marked as paid.

Third, the pipeline added static analysis, branch-coverage reporting, and mutation testing for pricing and payment modules. Logs included a correlation identifier but excluded card data. A dashboard tracked flaky tests, escaped defects, and build-repair time rather than coverage alone.

Outcome

The boundary defect was detected before release, and the transaction test exposed an incorrect error-handling path. The team did not claim that the system was defect-free; instead, it gained stronger evidence for the highest-risk behaviours and a repeatable response when failures occurred.

Learning points

The case demonstrates that test design begins with domain rules, not tools. Dependency injection makes failure scenarios controllable. Coverage is diagnostic rather than a quality certificate. Mutation testing can reveal weak assertions, and observability must be designed with privacy and operational needs in mind.

11. Limitations, ethics, and contemporary practice

Automated testing cannot prove that requirements are complete, ethical, or socially appropriate. A test suite may faithfully enforce discriminatory rules. Data used in tests may contain personal information, creating privacy and regulatory risk. Production-like data should be anonymized or synthetically generated, access should be limited, and retention should be defined.

AI-assisted coding and test generation can suggest cases, fixtures, or mutations, but generated tests may reproduce assumptions in the implementation and may contain insecure or copyrighted material. Developers remain responsible for reviewing generated code, validating licences and provenance, and checking that tests assert requirements rather than implementation details. AI systems should not receive confidential source code or personal data unless organizational policy and legal controls permit it.

Modern quality engineering increasingly combines shift-left practices—early tests, static checks, and threat modelling—with shift-right practices such as canary releases, synthetic monitoring, feature flags, and controlled fault injection. These techniques complement rather than replace unit and integration tests. Contract testing is particularly valuable in microservices because it verifies that a provider and consumer agree on an API without requiring every consumer test to deploy the entire system.

Best-practice checklist

  • Test observable requirements, including invalid and boundary cases.
  • Keep unit tests isolated, fast, deterministic, and readable.
  • Use integration tests at real architectural boundaries.
  • Inject clocks, randomness, and external services when control is required.
  • Prefer focused test doubles over indiscriminate mocking.
  • Use property-based testing for broad invariants and generated edge cases.
  • Treat coverage as evidence about test reach, not proof of correctness.
  • Use mutation testing selectively to assess assertion strength.
  • Make logs useful, structured, correlated, and free of sensitive data.
  • Profile representative workloads and measure before optimizing.
  • Make CI gates explicit, reproducible, and proportionate to risk.
  • Track trends and outcomes, not vanity metrics.

Chapter Summary

Testing supplies evidence about software behaviour at unit, integration, system, regression, and acceptance levels. Effective test design uses equivalence classes, boundaries, decision tables, state transitions, negative cases, and properties. JUnit and pytest support assertions, parameterization, fixtures, and exception testing, while dependency injection enables controlled use of stubs, fakes, spies, and mocks. Debugging moves from reproducible failure to root cause and regression prevention. Static analysis, coverage, mutation testing, profiling, logging, and quality metrics provide complementary evidence, each with important limitations. A mature CI pipeline combines fast automated checks, isolated integration environments, security controls, transparent quality gates, and responsible interpretation of metrics. Ethical practice requires privacy protection, secure handling of secrets, careful use of AI assistance, and recognition that automated tests cannot determine whether requirements themselves are just or complete.

Key Terms

Unit test: A test of a small isolated software unit.

Integration test: A test of collaboration between components or systems.

Regression test: A test that prevents a corrected defect from returning.

Acceptance test: A test of behaviour against user or business criteria.

Fixture: Controlled setup and teardown data or resources for tests.

Test double: A replacement for a real collaborator in a test.

Stub: A double that supplies controlled answers.

Fake: A lightweight working implementation used in testing.

Mock: A double used primarily to verify expected interactions.

Property-based testing: Testing general properties over generated inputs.

Invariant: A condition that remains true throughout a defined operation or state.

Static analysis: Examination of code without executing it.

Coverage: A measure of code elements reached by tests.

Mutation testing: Assessment of tests by introducing artificial code changes.

Profiler: A tool that measures runtime resource use and hotspots.

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

Continuous integration: Frequent automated integration and verification of changes.

Quality gate: A predefined condition that determines whether a change may proceed.

Observability: The ability to infer system state from logs, metrics, and traces.

Dependency injection: Supplying collaborators from outside rather than constructing them internally.

Review Questions

Short Answer Questions

  1. Distinguish a unit test from an integration test.
  2. Why are boundary values important in test design?
  3. What is the purpose of a pytest fixture?
  4. Why does high line coverage not prove correctness?
  5. State one privacy risk associated with application logging.

Long Answer Questions

  1. Explain the test pyramid and discuss circumstances in which a project may need more integration tests than usual.
  2. Compare stubs, fakes, spies, and mocks, giving an appropriate use for each.
  3. Describe a systematic debugging workflow from failure reproduction to regression prevention.
  4. Explain how mutation testing complements coverage analysis, including the problem of equivalent mutants.
  5. Design a CI quality pipeline for a Java or Python web service and justify its stages.

Analytical / Application Questions

  1. An order service accepts coupons until an expiry timestamp. Design equivalence classes, boundary tests, and a property-based test strategy for this rule.
  2. A project has 95% line coverage but frequent production defects in payment processing. Analyze at least four likely weaknesses in its quality strategy and propose corrective evidence or tests.
  3. A CI build fails intermittently only when tests run in parallel. Develop a diagnostic plan that considers shared state, time, randomness, files, ports, databases, and external services.

Multiple Choice Questions with Answers

Question 1

Which test level primarily verifies a small class or method in isolation?

A. Unit testing

B. System testing

C. Acceptance testing

D. Load testing

Correct Answer: A. Unit testing

Question 2

What is the main purpose of a test fixture in pytest?

A. To generate production traffic

B. To provide controlled setup and resource management

C. To replace every assertion

D. To measure CPU instructions

Correct Answer: B. To provide controlled setup and resource management

Question 3

Which technique is most appropriate for finding defects around a valid range of 18 through 120?

A. Random deletion testing

B. Boundary-value analysis

C. Log compression

D. Deployment testing only

Correct Answer: B. Boundary-value analysis

Question 4

If 84 of 100 measured executable lines are executed, what is line coverage?

A. 16%

B. 84%

C. 100%

D. 184%

Correct Answer: B. 84%

Question 5

What does mutation testing evaluate?

A. Whether tests detect small artificial changes to code

B. Whether a database has enough storage

C. Whether logs contain timestamps

D. Whether a program uses inheritance

Correct Answer: A. Whether tests detect small artificial changes to code

Question 6

Which test double primarily verifies that an expected interaction occurred?

A. Mock

B. Compiler

C. Profiler

D. Formatter

Correct Answer: A. Mock

Question 7

Which statement best describes property-based testing?

A. It tests only one manually selected example

B. It generates inputs and checks a general property

C. It removes the need for requirements

D. It measures only network latency

Correct Answer: B. It generates inputs and checks a general property

Question 8

Which practice is most appropriate for sensitive data in logs?

A. Logging access tokens for diagnosis

B. Logging complete payment credentials

C. Excluding secrets and unnecessary personal data

D. Disabling all error reporting

Correct Answer: C. Excluding secrets and unnecessary personal data

Question 9

What is the primary purpose of profiling?

A. To locate runtime performance hotspots

B. To prove business requirements are complete

C. To replace integration tests

D. To create mock objects

Correct Answer: A. To locate runtime performance hotspots

Question 10

Why should a CI pipeline use isolated test resources?

A. To encourage order-dependent tests

B. To prevent shared state from causing nondeterministic results

C. To avoid running static analysis

D. To ensure every test accesses production data

Correct Answer: B. To prevent shared state from causing nondeterministic results

Object-Oriented Programming (Java/Python)
Chapter 11 of 15