Object-Oriented Programming (Java)

Chapter 12
Testing, Quality, and Professional Engineering Workflow

Bachelors Program in IT
Object-Oriented Programming (Java)   •   Bachelor

Testing, Quality, and Professional Engineering Workflow

Introduction

Object-oriented programming is not complete when a Java program compiles. A professional system must also behave correctly, remain understandable as it evolves, integrate safely with other components, and be deliverable through a repeatable process. Testing and engineering workflow convert programming from an individual coding activity into disciplined software development.

This chapter examines unit, integration, parameterized, and mock-based testing; test-driven development (TDD); static analysis; code review; Git; build automation; dependency management; documentation; continuous integration and continuous delivery (CI/CD); and coding standards. The emphasis is practical: a Java developer should be able to design effective tests, interpret failures, collaborate through version control, and establish a quality workflow for a small or medium-sized application.

Learning Objectives

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

  • distinguish unit, integration, system, and acceptance tests;
  • write effective JUnit tests, including parameterized tests;
  • isolate collaborators with mocks and recognize inappropriate mocking;
  • apply the red–green–refactor cycle of TDD;
  • use static analysis, code review, and coding standards to prevent defects;
  • organize a Java project with Git, Maven or Gradle, and explicit dependencies;
  • explain the role of documentation and build reproducibility;
  • design a basic CI/CD pipeline; and
  • evaluate testing limitations, quality risks, and ethical responsibilities.

1. Software Quality as an Engineering Concern

Quality is multidimensional. Correctness means that observable behavior satisfies requirements. Reliability concerns consistent operation over time. Maintainability concerns how easily developers can understand and change the system. Security, performance, accessibility, portability, and usability may also be quality attributes.

Testing provides evidence about behavior; it does not prove that a non-trivial program is free of defects. A test passes only for the inputs and conditions it exercises. Quality therefore depends on complementary practices: clear requirements, modular design, automated verification, human review, secure dependency management, and operational monitoring.

A useful distinction is between verification and validation:

  • Verification: “Are we building the product correctly?” Examples include compilation, static analysis, and checking implementation against design rules.
  • Validation: “Are we building the correct product?” Examples include acceptance tests, usability evaluation, and stakeholder review.

A defect is an error in a work product, such as incorrect code. A failure is an observed deviation from expected behavior. A fault is the mechanism in software that can cause a failure. A test oracle is the means by which an expected result is determined, such as an assertion, specification, or invariant.

2. Levels and Types of Testing

Testing levels differ mainly in scope and isolation.

LevelPrimary targetTypical speedExample
UnitOne class or small unitVery fastTest Invoice.total()
IntegrationCollaborating componentsModerateService with a database
SystemComplete applicationSlowerREST application in a test environment
AcceptanceBusiness requirementVariableCustomer can complete checkout

A unit test should normally be deterministic, focused, and independent of external services. It tests behavior rather than private implementation details. A unit test for a DiscountCalculator may supply an amount and customer category and assert the calculated discount.

An integration test checks that components work together. It may use a real database, file system, message broker, or HTTP boundary. Integration tests discover configuration, serialization, schema, transaction, and protocol defects that isolated unit tests cannot reveal.

A system test exercises the assembled application. An acceptance test evaluates behavior against a user or business requirement. The commonly used “test pyramid” recommends many fast unit tests, fewer integration tests, and a smaller number of broad end-to-end tests. The pyramid is a guideline, not a law: a distributed system may require substantial contract and integration testing.

Other useful classifications include:

  • Regression testing: repeating tests after change to detect reintroduced defects.
  • Smoke testing: a small set of tests that establishes whether a build is sufficiently functional for deeper testing.
  • Contract testing: verifying that a service provider and consumer agree on an interface.
  • Property-based testing: checking general properties over generated inputs rather than only examples.
  • Performance testing: measuring response time, throughput, resource use, or scalability under specified conditions.
  • Security testing: examining authentication, authorization, input handling, secrets, and known vulnerabilities.

3. Unit Testing with JUnit

JUnit is the standard testing framework in the Java ecosystem. A test class commonly uses @Test and assertions from org.junit.jupiter.api.Assertions.

class TemperatureConverter {
    double toFahrenheit(double celsius) {
        return celsius * 9.0 / 5.0 + 32.0;
    }
}

class TemperatureConverterTest {
    private final TemperatureConverter converter = new TemperatureConverter();

    @Test
    void convertsZeroCelsiusToThirtyTwoFahrenheit() {
        assertEquals(32.0, converter.toFahrenheit(0.0), 0.000001);
    }
}

The third argument in assertEquals is a tolerance for floating-point comparison. Decimal fractions often cannot be represented exactly in binary floating-point; exact equality may therefore be inappropriate. The tolerance must be chosen according to the domain’s precision requirement.

A well-structured test often follows Arrange–Act–Assert:

  1. Arrange the objects and input.
  2. Act by invoking the behavior under test.
  3. Assert the expected result or observable effect.

Tests should have descriptive names, one principal reason to fail, and meaningful assertions. Avoid testing private methods directly; test the public behavior through the class interface. If a class is difficult to test, that may indicate excessive responsibility or tight coupling.

Exceptions are part of behavior and should be tested explicitly:

@Test
void rejectsNegativeQuantity() {
    IllegalArgumentException error = assertThrows(
        IllegalArgumentException.class,
        () -> new OrderLine("Book", -1, 20.00)
    );
    assertEquals("quantity must not be negative", error.getMessage());
}

A test should be deterministic: repeated execution under the same controlled conditions should give the same result. Tests that depend on current time, random values, machine-specific paths, network availability, or shared mutable state require explicit control of those dependencies.

4. Parameterized Testing

When the same rule applies to several inputs, parameterized tests reduce duplication and make the data boundary visible. In JUnit 5, @ParameterizedTest may be combined with @CsvSource, @ValueSource, or a method source.

@ParameterizedTest
@CsvSource({
    "0, 32",
    "100, 212",
    "-40, -40"
})
void convertsCelsiusExamples(double celsius, double expected) {
    assertEquals(expected, converter.toFahrenheit(celsius), 0.000001);
}

Parameterized tests are especially valuable for boundary partitions: empty versus non-empty input, minimum and maximum permitted values, zero, negative values, and malformed data. They do not automatically guarantee coverage. Poorly selected data can repeat the same case many times while missing an important boundary.

For complex data, a method source can return domain objects. The test data should remain understandable; if a row requires extensive explanation, a named test or fixture builder may be clearer.

5. Mock-Based Testing and Test Doubles

A unit often collaborates with repositories, clocks, payment gateways, or notification services. A test double replaces a real collaborator during testing. Common forms are:

  • Stub: returns prepared answers.
  • Fake: a lightweight working implementation, such as an in-memory repository.
  • Spy: records calls while often providing real behavior.
  • Mock: verifies expected interactions, such as whether a method was called.

Mockito is widely used with JUnit. Consider a service that sends an email after registering a user:

class RegistrationService {
    private final UserRepository repository;
    private final Mailer mailer;

    RegistrationService(UserRepository repository, Mailer mailer) {
        this.repository = repository;
        this.mailer = mailer;
    }

    void register(User user) {
        repository.save(user);
        mailer.sendWelcome(user.email());
    }
}

A mock-based test could verify the interaction:

@ExtendWith(MockitoExtension.class)
class RegistrationServiceTest {
    @Mock UserRepository repository;
    @Mock Mailer mailer;

    @Test
    void savesUserAndSendsWelcomeMail() {
        User user = new User("[email protected]");
        RegistrationService service = new RegistrationService(repository, mailer);

        service.register(user);

        verify(repository).save(user);
        verify(mailer).sendWelcome("[email protected]");
    }
}

Mocking is appropriate when a collaborator is slow, unavailable, nondeterministic, expensive, or has side effects. It is less appropriate for simple value objects or every internal method of the class under test. Excessive interaction verification couples tests to implementation and makes harmless refactoring difficult. Important integration paths must still be tested with real or realistic components.

6. Test-Driven Development

TDD is a development discipline in which a failing automated test is written before the production behavior, followed by the smallest implementation that makes it pass. The cycle is:

  1. Red: write a test that expresses an unmet requirement and observe failure.
  2. Green: implement the simplest correct solution.
  3. Refactor: improve design without changing externally visible behavior.

For example, a developer might first specify that an empty shopping cart has a total of zero. After that test passes, additional tests can specify adding an item, applying a discount, and rejecting invalid quantities.

TDD encourages small interfaces, rapid feedback, and executable requirements. It also exposes design problems early: a test requiring many complicated setup steps may indicate excessive coupling. However, TDD is not a replacement for exploratory, usability, performance, security, or acceptance testing. A test-first process can still encode the wrong requirement, and high line coverage can coexist with weak assertions.

Coverage metrics must be interpreted carefully. Statement coverage measures executed statements; branch coverage measures exercised decision outcomes. Neither proves correctness. Mutation testing, in which small faults are deliberately introduced and tests are checked for detecting them, can provide stronger evidence about test effectiveness, although it increases build cost.

7. Static Analysis and Coding Standards

Static analysis examines source code or compiled artifacts without executing the application. Compilers catch type errors; tools such as Checkstyle, PMD, SpotBugs, Error Prone, and IDE inspections identify style violations, suspicious constructs, possible null errors, unused code, security weaknesses, and complexity problems.

Coding standards should specify naming, formatting, package organization, exception handling, logging, documentation, dependency rules, and test conventions. Automated formatting is preferable to subjective arguments during code review. A standard should improve readability and safety rather than enforce arbitrary uniformity.

Important practices include:

  • use meaningful names and small cohesive classes;
  • prefer immutable objects where practical;
  • limit visibility and expose the smallest useful API;
  • validate data at trust boundaries;
  • do not log passwords, tokens, or sensitive personal data;
  • preserve the cause when rethrowing exceptions;
  • avoid broad exception catches that hide failures;
  • make thread-safety assumptions explicit; and
  • treat warnings as defects unless consciously justified.

Static analysis produces false positives and false negatives. Teams should configure severity, review suppressions, and periodically update rules. A suppression should explain why a warning is safe, not merely silence an inconvenient tool.

8. Code Review

Code review is a structured examination of a change by one or more people other than its author. It can find defects, clarify design, spread knowledge, and improve maintainability. Reviewers should examine requirements, error handling, security, tests, complexity, observability, documentation, and compatibility—not only formatting.

Effective reviews are small, timely, and respectful. A pull request should state its purpose, relevant issue, testing performed, and known limitations. Review comments should distinguish blocking defects from suggestions. Authors should respond to technical points without treating criticism as a personal judgment.

A review is not a substitute for automated tests, and approval does not guarantee correctness. Teams should avoid both superficial approval and excessively large reviews that encourage fatigue. CODEOWNERS or ownership rules can ensure that sensitive areas receive appropriate expertise.

9. Git and Collaborative Development

Git records a directed history of snapshots. A typical workflow is:

  1. update the local branch;
  2. create a focused feature branch;
  3. make small, coherent commits;
  4. run tests and quality checks;
  5. push the branch and open a pull request;
  6. address review feedback; and
  7. merge using the team’s agreed strategy.

A commit should represent one logical change and have a clear message. Do not commit generated build directories, IDE-specific metadata, credentials, or local configuration containing secrets. A .gitignore file helps, but it cannot protect a secret already committed; exposed credentials must be revoked and replaced.

Branches, merge commits, rebasing, and squashing are workflow choices. The essential principles are traceability, reviewability, and a shared policy. Tags can identify releases. Reverting a problematic commit is generally safer than rewriting public history. Access controls and protected branches should require review and passing checks before merging.

10. Build Tools and Dependency Management

Maven and Gradle automate compilation, testing, packaging, reporting, and publication. A build should be executable from a clean environment without relying on a developer’s workstation. It should specify the Java version, repositories, plugins, test framework, and dependency scopes.

Dependencies should be explicit and limited. A direct dependency is declared by the project; a transitive dependency is brought in by another dependency. Dependency management must address:

  • version compatibility;
  • reproducible resolution, often using lockfiles or dependency constraints;
  • vulnerability scanning;
  • license compatibility;
  • update policy; and
  • removal of unused libraries.

Never download arbitrary libraries into a project without examining their provenance, maintenance, license, and known vulnerabilities. A software bill of materials (SBOM) records components used in a build and supports vulnerability response. Dependency pinning improves reproducibility, while automated update tools can propose controlled upgrades.

A build commonly separates production and test dependencies. For example, JUnit and Mockito should not be packaged into the deployed application. Build profiles should not silently disable tests or security checks in release builds.

11. Documentation and Maintainability

Documentation exists at several levels:

  • API documentation: public classes, methods, parameters, return values, exceptions, and examples.
  • Architecture documentation: major components, responsibilities, data flows, and important decisions.
  • Operational documentation: configuration, deployment, monitoring, backup, and recovery.
  • Developer documentation: setup, build commands, test execution, contribution rules, and troubleshooting.

Good documentation explains intent and constraints, not merely what obvious code does. Comments should be maintained with the code. An outdated comment is a defect because it creates false knowledge. Architecture decision records can preserve the reasons behind significant choices, including rejected alternatives.

12. Continuous Integration and Delivery

Continuous integration (CI) automatically builds and tests changes when code is pushed or a pull request is opened. A typical pipeline is:

Commit → Compile → Unit tests → Static analysis → Package
                         ↓
                 Integration tests
                         ↓
              Security and quality gates
                         ↓
          Publish artifact → Deploy to staging
                         ↓
                  Acceptance checks

Continuous delivery means that every change that passes the required checks is kept release-ready, usually with a controlled production approval. Continuous deployment goes further by automatically releasing qualifying changes to production.

A reliable pipeline should use clean, reproducible environments; fail clearly; preserve test reports and artifacts; keep secrets in a secure secret store; and avoid depending on a developer’s machine. Parallel jobs can reduce duration, but tests that share state must be isolated. Flaky tests—tests that pass and fail without a relevant code change—damage trust and should be quarantined temporarily only with ownership and a removal deadline.

Quality gates should be meaningful. A pipeline might require compilation, all unit tests, selected integration tests, no high-severity dependency vulnerabilities, and review approval. Gates should not encourage teams to game a single metric such as line coverage.

13. Applied Case: A Library Lending Service

The following is an illustrative case. A university library is developing a Java lending service. A member may borrow at most five books, a book may not be borrowed when unavailable, and a due date is calculated from the lending date. The service uses a repository and a notification gateway.

Background and problem

An initial implementation placed validation, database access, date calculation, and email delivery in one large method. Tests were slow because every test required a database and an SMTP server. A change to the due-date rule caused intermittent failures in the delivery environment.

Analysis

The class had several responsibilities and depended directly on current system time. Tests also shared database records, so their order affected results. The team identified a need for a clock abstraction, a repository interface, clear domain rules, and separate integration tests.

Approach and solution

The team first wrote unit tests for the borrowing rules using fixed dates and an in-memory fake repository. Parameterized tests covered member loan counts of 0, 4, and 5, plus attempts at 6. A mock notification gateway verified that a successful loan triggered one notification and a rejected loan triggered none. A small set of integration tests used a real test database to verify mapping, constraints, and transactions.

The build was configured so every pull request compiled the project, ran unit and integration tests, performed static analysis, and scanned dependencies. A reviewer required that the maximum-loan rule be represented in the domain service rather than hidden in a controller. Documentation recorded the due-date policy and the reason for injecting a clock.

Outcome

Unit tests became fast and deterministic. Database integration tests still detected schema errors, while mock-based tests checked service orchestration. The fixed clock removed time-dependent failures. The pipeline prevented a change from being merged when the loan-limit test failed.

Learning points

The case demonstrates that quality comes from the combination of design, test selection, automation, and review. Mocks alone would not detect a database mapping defect; integration tests alone would be too slow for every design iteration. Separating the test levels provides both rapid feedback and realistic confidence.

14. Limitations, Ethics, and Contemporary Practice

Automated testing can miss unspecified requirements, accessibility barriers, harmful outcomes, and defects in the test itself. Developers must test beyond the happy path and consider privacy, security, fairness, and misuse. Logs and test fixtures must not expose personal or confidential information. Production data should not be copied into development environments without lawful authorization, minimization, and protection.

AI-assisted coding tools can suggest tests, refactorings, and documentation, but generated code may contain insecure patterns, licensing concerns, hallucinated APIs, or tests that merely confirm implementation. Developers remain responsible for reviewing, testing, and understanding generated output. Sensitive source code and credentials should not be submitted to an AI service contrary to organizational policy.

Contemporary engineering increasingly includes supply-chain security, SBOMs, signed artifacts, secret scanning, container and infrastructure testing, and observability-driven quality. These practices extend the same principle: make risks visible early, automate repeatable checks, and preserve human accountability for decisions.

Best-Practice Checklist

Before merging a Java change, ask:

  • Is the requirement clear and is the public behavior tested?
  • Are unit tests fast, isolated, deterministic, and meaningful?
  • Are integration boundaries tested realistically?
  • Are boundary and invalid inputs represented, perhaps through parameterized tests?
  • Are mocks used only for genuine collaborators and side effects?
  • Does static analysis pass, and are suppressions justified?
  • Is the change small enough for effective review?
  • Are dependencies explicit, current, licensed appropriately, and scanned?
  • Does the build work in a clean environment?
  • Are documentation, logging, security, and operational effects addressed?
  • Will CI produce traceable evidence and a reproducible artifact?

Chapter Summary

Professional Java development requires more than compiling code. Unit tests provide fast feedback about focused behavior; integration, system, and acceptance tests examine progressively broader boundaries. Parameterized tests efficiently express input partitions and boundary cases, while mocks and other test doubles isolate collaborators when used selectively. TDD applies a red–green–refactor cycle that supports incremental design but does not replace exploratory, security, performance, or acceptance testing.

Static analysis, coding standards, code review, and documentation reduce defects and improve maintainability. Git supplies traceability and collaboration; Maven or Gradle provide repeatable builds; dependency management addresses version, security, license, and supply-chain risks. CI/CD automates evidence and delivery, while quality gates, artifact management, and secure secrets handling make releases safer. Finally, ethical engineering requires protecting data, assessing AI-generated code critically, and recognizing that no single metric or test suite proves software quality.

Key Terms

Acceptance testing: Testing whether software satisfies user or business requirements.

Code coverage: A measure of which parts of code are executed by tests.

Continuous integration: Automatically building and verifying changes frequently.

Continuous delivery: Keeping validated software ready for controlled release.

Defect: An imperfection in a software work product.

Fake: A lightweight working implementation used in tests.

Integration test: A test of collaborating components or an external boundary.

Mock: A test double used primarily to verify interactions.

Parameterized test: One test structure executed with multiple data sets.

Regression test: A repeated test intended to detect reintroduced defects.

Static analysis: Automated examination of code without executing it.

Stub: A test double that returns prepared responses.

System test: A test of an assembled application.

Test oracle: A source of expected behavior used to judge a result.

Test-driven development: Development through failing tests, implementation, and refactoring.

Test double: A replacement for a real collaborator during testing.

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

Version control: A system for recording and coordinating changes to artifacts.

Review Questions

Short Answer Questions

  1. Distinguish verification from validation and give one example of each.
  2. What makes a unit test deterministic?
  3. Explain the difference between a stub, fake, spy, and mock.
  4. State the three stages of the TDD cycle.
  5. Why should dependencies and build versions be explicitly managed?

Long Answer Questions

  1. Compare unit, integration, system, and acceptance testing. Explain why a professional project needs more than one level.
  2. Explain how parameterized testing and boundary-value selection improve test quality.
  3. Discuss the benefits and limitations of mock-based testing.
  4. Describe how Git, code review, static analysis, and CI work together in a pull-request workflow.
  5. Explain the security, maintenance, and reproducibility issues involved in Java dependency management.

Analytical / Application Questions

  1. A test suite has 95% statement coverage but frequently misses defects in database mappings. Diagnose the problem and propose a revised testing strategy.
  2. Design a test plan for a Java payment service that validates inputs, calls a payment gateway, writes a transaction, and sends a receipt. Identify unit, mock-based, integration, and acceptance tests.
  3. A CI pipeline contains flaky tests, unreviewed dependency upgrades, and credentials in configuration files. Prioritize the risks and propose a staged remediation plan.

Multiple Choice Questions with Answers

Question 1

Which testing level primarily examines one class or small unit in isolation?

A. Unit testing

B. System testing

C. Acceptance testing

D. Production monitoring

Correct Answer: A. Unit testing

Question 2

What is the principal purpose of a parameterized test?

A. To replace all integration tests

B. To execute one test structure with multiple data sets

C. To test private methods directly

D. To measure deployment speed

Correct Answer: B. To execute one test structure with multiple data sets

Question 3

Which test double is primarily used to verify that an interaction occurred?

A. Fake

B. Stub

C. Mock

D. Fixture

Correct Answer: C. Mock

Question 4

What is the correct order of the TDD cycle?

A. Green, refactor, red

B. Red, green, refactor

C. Refactor, red, green

D. Red, refactor, deploy

Correct Answer: B. Red, green, refactor

Question 5

Which activity is an example of static analysis?

A. Running a user acceptance session

B. Sending a request to a production server

C. Inspecting source code for suspicious null handling without executing it

D. Measuring response time under load

Correct Answer: C. Inspecting source code for suspicious null handling without executing it

Question 6

Why should a test avoid depending on the current system clock?

A. The compiler cannot process dates

B. It can make the test nondeterministic

C. It prevents all mocking

D. It makes integration tests mandatory

Correct Answer: B. It can make the test nondeterministic

Question 7

Which practice best supports effective code review?

A. Submitting very large unrelated changes

B. Disabling automated checks before review

C. Making a focused change with its purpose and tests described

D. Requiring reviewers to inspect only formatting

Correct Answer: C. Making a focused change with its purpose and tests described

Question 8

What is a transitive dependency?

A. A dependency explicitly imported by a Java source file only

B. A dependency brought in by another dependency

C. A dependency that exists only in production logs

D. A dependency that cannot be versioned

Correct Answer: B. A dependency brought in by another dependency

Question 9

Which statement best describes continuous integration?

A. Manual release of software once per year

B. Automatic building and verification of frequently integrated changes

C. Replacing all tests with code review

D. Deploying every commit directly to production

Correct Answer: B. Automatic building and verification of frequently integrated changes

Question 10

Why are high code-coverage percentages insufficient evidence of correctness?

A. Coverage tools cannot execute Java code

B. Coverage measures execution, not whether assertions express the right behavior

C. Coverage applies only to documentation

D. Coverage guarantees that requirements are wrong

Correct Answer: B. Coverage measures execution, not whether assertions express the right behavior

Object-Oriented Programming (Java)
Chapter 12 of 15