Java Programming

Chapter 13
Testing, Build Tools, and Software Delivery

Certificate in Java-Python-Programming Languages
Java Programming   •   Certificate

Testing, Build Tools, and Software Delivery

Introduction

A Java program is not complete when it compiles. Professional software must also behave correctly, remain maintainable as it changes, be reproducible on different computers, and be deliverable to users or production systems. Testing, build automation, version control, documentation, code review, continuous integration (CI), and deployment form a connected software-delivery discipline.

This chapter presents practical foundations for developing reliable Java applications. It uses JUnit for automated tests and Maven as the principal build tool; the same concepts apply to Gradle. The emphasis is on skills expected of a certificate-level Java developer: writing useful unit tests, managing dependencies, collaborating with Git, interpreting build results, and creating a basic delivery workflow.

Learning Objectives

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

  • distinguish unit, integration, system, and acceptance testing;
  • design effective test cases using equivalence classes, boundary values, and negative cases;
  • create JUnit 5 tests with assertions, lifecycle methods, and parameterized inputs;
  • explain Maven project structure, dependency management, and lifecycle phases;
  • compare Maven and Gradle at a practical level;
  • use Git branches, commits, pull requests, and code review responsibly;
  • document Java software for users and maintainers;
  • describe a CI pipeline and a basic deployment workflow;
  • recognize limitations, security risks, and ethical responsibilities in software delivery.

1. Software Quality and the Testing Mindset

Software quality is multidimensional. Correctness is important, but so are reliability, security, usability, performance, maintainability, and portability. Testing provides evidence about software behavior; it does not prove that a program contains no defects.

A useful distinction is between verification and validation. Verification asks whether the software was built according to its specification—for example, whether a method rejects invalid input. Validation asks whether the software solves the user’s actual problem—for example, whether a registration workflow is understandable and useful.

1.1 Levels of testing

LevelMain subjectTypical purpose
UnitA class or small method in isolationFind local logic errors quickly
IntegrationSeveral components togetherCheck interfaces, persistence, or external services
SystemThe complete applicationEvaluate end-to-end behavior
AcceptanceBusiness or user requirementsDecide whether the product is fit for use

A unit test should normally be fast, deterministic, and focused on one behavior. It may use a test double—such as a stub or mock—to isolate the unit from a database, network, clock, or file system. Integration tests are valuable, but they are usually slower and more sensitive to configuration.

1.2 The test pyramid

The test pyramid is a planning model rather than a rigid law. A healthy suite generally contains many fast unit tests, fewer integration tests, and a small number of broad end-to-end tests. The model reduces feedback time while preserving confidence in important interactions. It does not mean that every project must use a fixed percentage of each test type.

A test case should specify:

  1. Preconditions: the starting state;
  2. Stimulus: the input or action;
  3. Expected result: the observable behavior;
  4. Postconditions: the resulting state or side effect.

The Arrange–Act–Assert pattern expresses this structure in code: prepare data, invoke the behavior, and verify the outcome.

2. Designing Effective Tests

Testing every possible input is usually impossible. Good test design selects cases that provide high information value.

Equivalence partitioning

Inputs are divided into groups expected to behave similarly. For a method accepting an age from 18 through 65, useful partitions include below 18, 18–65, and above 65. Testing one or more representatives from each partition is more efficient than testing many arbitrary values.

Boundary-value analysis

Defects often occur at transitions. For the same age rule, important values include 17, 18, 65, and 66. Boundary testing should include both sides of a boundary where practical.

Positive, negative, and exceptional tests

A positive test checks valid behavior. A negative test checks rejection of invalid input. An exceptional test verifies that the correct exception type and message or state change occur. Tests should not merely reproduce the implementation; they should express a requirement that could reveal an incorrect implementation.

Determinism and isolation

A deterministic test gives the same result when run repeatedly under the same conditions. Avoid dependence on the current time, random values, machine-specific paths, network availability, or test execution order. Inject a Clock, random-number generator, or repository interface when such dependencies must be controlled.

A test that passes only when another test runs first is defective. Each test should create or reset its own state, and shared mutable fixtures should be used cautiously.

3. Unit Testing with JUnit 5

JUnit 5 consists of a platform, a Jupiter programming model, and test engines. Most introductory Java projects use Jupiter annotations and assertions.

A simple production class might be:

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

A corresponding test is:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

class TemperatureConverterTest {
    @Test
    void convertsFreezingPoint() {
        TemperatureConverter converter = new TemperatureConverter();

        double result = converter.toFahrenheit(0.0);

        assertEquals(32.0, result, 0.000001);
    }
}

The third argument in assertEquals is a tolerance for floating-point comparison. It is appropriate because binary floating-point values may not represent decimal fractions exactly. For exact monetary calculations, BigDecimal with an explicit rounding policy is generally preferable to double.

Common JUnit features

  • @Test identifies a test method.
  • @BeforeEach runs before each test and is useful for independent setup.
  • @AfterEach performs per-test cleanup.
  • @BeforeAll and @AfterAll run once for a test class; use them carefully because shared state can reduce isolation.
  • assertEquals, assertTrue, assertFalse, assertNull, and assertNotNull check outcomes.
  • assertThrows checks exceptional behavior.
  • assertAll groups related assertions while reporting all failures.
  • @DisplayName can make reports more readable.

For example:

import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;

class AccountTest {
    @Test
    void rejectsNegativeWithdrawal() {
        Account account = new Account(100.00);

        assertThrows(IllegalArgumentException.class,
            () -> account.withdraw(-1.00));
    }
}

Tests should generally have one reason to fail. Multiple assertions are appropriate when they describe one coherent result, such as checking both a returned status and an updated balance.

Parameterized tests

Parameterized tests apply one test structure to several inputs:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class TemperatureConverterTest {
    @ParameterizedTest
    @CsvSource({"0, 32", "100, 212", "-40, -40"})
    void convertsKnownValues(double celsius, double fahrenheit) {
        assertEquals(fahrenheit,
            new TemperatureConverter().toFahrenheit(celsius), 0.000001);
    }
}

Readable test names and realistic data make failures easier to diagnose. Avoid testing private methods directly; test the public behavior through which users depend on the class.

4. Maven and Gradle Build Automation

A build tool automates compilation, testing, packaging, dependency retrieval, and other repeatable tasks. Automation reduces “works on my machine” problems by making the build definition part of the source code.

4.1 Maven project structure

A conventional Maven project uses:

project/
├── pom.xml
└── src/
    ├── main/java/       production source
    ├── main/resources/  production resources
    └── test/java/       test source

The pom.xml (Project Object Model) identifies the project, its version, dependencies, and plugins. A minimal example is:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>edu.example</groupId>
  <artifactId>temperature-app</artifactId>
  <version>1.0.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.version>5.10.2</junit.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
      </plugin>
    </plugins>
  </build>
</project>

Versions should be selected deliberately and reviewed for security and compatibility. In a real project, use the current approved versions rather than copying an old example unchanged.

4.2 Maven lifecycle

Important Maven phases include:

  • validate: check project structure;
  • compile: compile production code;
  • test: compile and run tests;
  • package: create a JAR or other artifact;
  • verify: perform additional quality checks;
  • install: place the artifact in the local repository;
  • deploy: publish it to a configured remote repository.

The command mvn test runs the relevant earlier phases and tests. mvn clean package removes previous build output and creates a package. The generated target directory is normally excluded from Git because it is reproducible build output.

Dependencies have scopes. A test dependency is available to test code but is not included as a production dependency. Transitive dependencies are dependencies brought in by another dependency. They are convenient but must be monitored for conflicts and vulnerabilities.

4.3 Gradle comparison

Gradle commonly uses build.gradle or build.gradle.kts, supports incremental execution, and provides a programmable task model. Maven favors convention and XML configuration; Gradle favors a flexible DSL and task-oriented configuration. Both can compile Java, run JUnit tests, resolve dependencies, produce artifacts, and integrate with CI. The key professional skill is understanding the build model, not memorizing one tool’s syntax.

5. Version Control, Documentation, and Code Review

Git records changes as commits. A typical collaborative workflow is:

  1. create or update a branch for a small change;
  2. implement the change and tests;
  3. inspect the diff;
  4. commit a coherent unit with a descriptive message;
  5. push the branch and open a pull request;
  6. respond to review and automated checks;
  7. merge using the team’s agreed policy.

A commit should not contain passwords, API keys, generated binaries, or unrelated formatting changes. A .gitignore file commonly excludes target/, IDE metadata, logs, and local configuration. Secrets should be revoked if accidentally committed; merely deleting them in a later commit does not remove them from repository history.

A code review evaluates correctness, clarity, security, tests, maintainability, and compatibility with project conventions. Review comments should address the code rather than attack the author. Authors should provide context and respond constructively. Automated checks are valuable, but they do not replace human judgment about requirements and design.

Documentation should include a focused README, setup prerequisites, commands for building and testing, configuration instructions, usage examples, known limitations, and a support or contribution process. Public Java APIs should use Javadoc where its benefit justifies the maintenance cost. Documentation is part of the product: inaccurate instructions are a delivery defect.

6. Continuous Integration and Software Delivery

Continuous integration means that changes are integrated frequently and automatically checked in a shared environment. A basic CI pipeline can be represented as:

Commit → Checkout → Resolve dependencies → Compile → Unit tests
       → Static checks → Package artifact → Publish report

A pipeline should use a clean environment, a specified JDK version, repeatable commands, and protected secrets. At minimum it should fail when compilation or tests fail. Useful additions include formatting checks, static analysis, dependency vulnerability scanning, test-result reports, and artifact retention.

A practical delivery workflow has several stages:

  • Build: produce a versioned artifact.
  • Test: gather automated evidence.
  • Release: identify the artifact and its source revision.
  • Deploy: place it in a controlled environment.
  • Verify: perform smoke checks and inspect logs and metrics.
  • Recover: roll back or redeploy a known-good version if necessary.

A deployment artifact should be immutable: once version 1.0.0 is published, it should not silently change. Configuration such as database URLs and credentials belongs outside the artifact, normally in environment variables or a managed configuration service. Production secrets must not be written to logs.

Deployment strategies include a simple stop-and-start release, rolling deployment, blue-green deployment, and canary deployment. A certificate-level developer should understand the purpose of these strategies even if an operations team performs the actual deployment. Database migrations require particular care because application versions may overlap during a rolling deployment.

7. Applied Case: Reliable Library Checkout Service

The following is an illustrative case.

Background

A small library has a Java checkout service. It accepts a member ID, book ID, and due date, then records a loan. Early versions were tested manually and occasionally accepted invalid dates or created duplicate loans.

Problem

The team needs rapid feedback without connecting every test to the production database. It also needs a repeatable build and a controlled delivery process.

Analysis

The team identifies these rules:

  • a member and book must exist;
  • a book already on loan cannot be checked out;
  • the due date must be after the checkout date;
  • a successful checkout creates exactly one loan;
  • failure must not partially change the database.

The first rule set suggests unit tests for validation and decision logic, integration tests for repository behavior, and a small end-to-end test for the complete HTTP or command-line workflow.

Approach and solution

The service receives repository interfaces rather than constructing database connections itself. Unit tests use in-memory fakes. Boundary and negative cases include a due date equal to the checkout date, a due date one day later, a missing member, and an already-loaned book. assertThrows verifies invalid requests, while a successful test verifies the returned loan and repository interaction.

The project uses Maven with JUnit as a test-scoped dependency. The Git pull request must pass mvn clean verify. CI builds on a supported JDK, runs tests, publishes reports, and creates a versioned JAR only after verification. A staging deployment runs database-backed integration tests and a smoke test before production approval.

Outcome

The team detects a regression—an earlier implementation used >= instead of > for the due-date rule—before release. The build is reproducible, reviewers can inspect a small change set, and deployment can be traced to a commit and artifact version.

Learning points

Testing is strongest when requirements become explicit cases. Dependency injection improves isolation. Build automation makes quality gates repeatable. CI provides fast evidence, while review and acceptance testing address concerns automated tests cannot fully evaluate.

8. Limitations, Security, and Professional Responsibility

No test suite guarantees defect-free software. Code coverage measures which code was executed, not whether assertions were meaningful. A high coverage percentage can coexist with weak tests. Mutation testing, exploratory testing, threat modeling, and user evaluation can reveal gaps, but each requires time and judgment.

Dependencies create supply-chain risk. Teams should use trusted repositories, pin or centrally manage versions, review licenses, scan for known vulnerabilities, and remove unused libraries. A vulnerability report still requires analysis: not every reported issue is exploitable in the project, and every applicable issue requires timely remediation or documented risk acceptance.

CI systems often handle credentials and source code. Apply least privilege, protect branches, require review for sensitive changes, and avoid printing environment variables. Do not place personal data in test fixtures or logs unnecessarily. Testing data should be minimized, anonymized, and retained according to organizational policy and applicable privacy law.

AI-assisted coding can generate tests or build configuration, but generated code must be reviewed, compiled, tested, and checked for licensing, security, and inaccurate assumptions. Responsibility remains with the developer and organization, not the tool.

Best-Practice Checklist

  • Write tests for observable requirements, including invalid inputs.
  • Prefer fast, isolated unit tests and add integration tests where boundaries matter.
  • Use deterministic clocks, data, and environments.
  • Keep production and test dependencies clearly separated.
  • Commit source and build configuration, not generated output or secrets.
  • Run the same essential build command locally and in CI.
  • Pin or govern dependency versions and scan them regularly.
  • Keep pull requests small enough to review carefully.
  • Publish traceable, immutable artifacts.
  • Document setup, testing, configuration, limitations, and recovery steps.

Chapter Summary

Reliable Java delivery combines several practices rather than relying on testing alone. Unit tests verify focused behavior; integration, system, and acceptance tests address broader risks. JUnit 5 provides annotations, assertions, exception checks, and parameterized tests for expressing executable requirements. Maven and Gradle automate compilation, dependency resolution, testing, and packaging. Git supports traceable collaboration through focused commits and reviewed pull requests. Documentation makes software usable and maintainable. CI provides repeatable quality gates, while deployment workflows add versioned artifacts, external configuration, verification, and recovery. Finally, dependency security, privacy, secret management, and responsible use of AI are essential parts of professional software delivery.

Key Terms

Unit test: A test focused on a small software unit, usually in isolation.

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

System test: A test of the complete application or a major end-to-end workflow.

Acceptance test: A test of whether software satisfies user or business requirements.

Test double: A substitute object used to isolate a unit from a dependency.

Deterministic test: A test that gives repeatable results under the same conditions.

Equivalence partitioning: Dividing inputs into classes expected to behave similarly.

Boundary-value analysis: Testing values at and around decision boundaries.

JUnit: A Java framework and platform for automated testing.

Assertion: A statement that checks an expected condition in a test.

Parameterized test: A test executed repeatedly with supplied input data.

Maven: A Java build and dependency-management tool using a project model.

Gradle: A build automation tool using tasks and a configurable DSL.

Dependency scope: The context in which a library is available, such as test or production.

Git: A distributed version-control system.

Code review: Human examination of a proposed code change before integration.

Continuous integration: Frequent integration with automated build and quality checks.

Artifact: A versioned output such as a JAR produced by a build.

Immutable artifact: A published artifact that is not silently modified.

Smoke test: A brief test confirming that a deployed system basically operates.

Review Questions

Short Answer Questions

  1. What distinguishes a unit test from an integration test?
  2. Why are boundary values important in test design?
  3. What is the purpose of the test dependency scope in Maven?
  4. Why should generated build directories normally be excluded from Git?
  5. What is the role of a smoke test after deployment?

Long Answer Questions

  1. Explain the test pyramid and discuss why a project should not rely exclusively on end-to-end tests.
  2. Describe how JUnit 5 assertions, lifecycle methods, and parameterized tests support maintainable test suites.
  3. Explain the principal Maven lifecycle phases from compilation through packaging and verification.
  4. Describe a responsible Git pull-request and code-review workflow for a Java team.
  5. Explain how CI, artifact versioning, external configuration, and rollback contribute to reliable delivery.

Analytical / Application Questions

  1. A date-validation method accepts dates from tomorrow onward but has tests only for a date one month in the future. Design equivalence partitions and boundary tests that could reveal an error.
  2. A Maven build passes locally but fails in CI because it depends on the developer’s machine time zone and an undeclared environment variable. Diagnose the problem and propose a deterministic solution.
  3. A team reports 98% code coverage, yet users find that duplicate orders are created during retries. Analyze why coverage did not prevent the defect and propose unit, integration, and deployment checks to address it.

Multiple Choice Questions with Answers

Question 1

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

A. Unit testing

B. System testing

C. Acceptance testing

D. Load testing

Correct Answer: A. Unit testing

Question 2

Which values are most directly targeted by boundary-value analysis?

A. Random values only

B. Values at and around decision limits

C. Only typical valid values

D. Only previously observed production values

Correct Answer: B. Values at and around decision limits

Question 3

Which JUnit annotation identifies a test method?

A. @Check

B. @Run

C. @Test

D. @Verify

Correct Answer: C. @Test

Question 4

What does assertThrows verify?

A. That a method returns null

B. That a specified exception is raised

C. That all tests pass

D. That a dependency is available

Correct Answer: B. That a specified exception is raised

Question 5

Which Maven command removes previous build output and creates a package?

A. mvn clean package

B. mvn remove publish

C. mvn reset install

D. mvn delete compile

Correct Answer: A. mvn clean package

Question 6

What is the usual purpose of Maven test scope?

A. To make a library available only to test code

B. To prevent compilation of test code

C. To publish a library to production automatically

D. To encrypt dependency files

Correct Answer: A. To make a library available only to test code

Question 7

Which item should not be committed to a Git repository?

A. A focused source-code change

B. A README file

C. An API secret

D. A test case

Correct Answer: C. An API secret

Question 8

What is a central characteristic of continuous integration?

A. Changes are integrated and automatically checked frequently

B. Production is never tested

C. Only manual builds are permitted

D. Dependencies are always removed

Correct Answer: A. Changes are integrated and automatically checked frequently

Question 9

Why should a released artifact be immutable?

A. So its contents can silently change after deployment

B. So a version always identifies the same tested output

C. So source code cannot be reviewed

D. So configuration must be embedded in the artifact

Correct Answer: B. So a version always identifies the same tested output

Question 10

Which practice best protects deployment credentials?

A. Printing them in CI logs

B. Committing them beside source code

C. Using protected secret storage and least privilege

D. Putting them in test names

Correct Answer: C. Using protected secret storage and least privilege

Java Programming
Chapter 13 of 15