Testing, Build Tools, and Software DeliveryIntroductionA 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 ObjectivesBy 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 MindsetSoftware 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| Level | Main subject | Typical purpose |
|---|
| Unit | A class or small method in isolation | Find local logic errors quickly | | Integration | Several components together | Check interfaces, persistence, or external services | | System | The complete application | Evaluate end-to-end behavior | | Acceptance | Business or user requirements | Decide 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 pyramidThe 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: - Preconditions: the starting state;
- Stimulus: the input or action;
- Expected result: the observable behavior;
- 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 TestsTesting every possible input is usually impossible. Good test design selects cases that provide high information value. Equivalence partitioningInputs 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 analysisDefects 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 testsA 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 isolationA 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 5JUnit 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 testsParameterized 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 AutomationA 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 structureA conventional Maven project uses: project/
├── pom.xml
└── src/
├── main/java/ production source
├── main/resources/ production resources
└── test/java/ test sourceThe 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 lifecycleImportant 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 comparisonGradle 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 ReviewGit records changes as commits. A typical collaborative workflow is: - create or update a branch for a small change;
- implement the change and tests;
- inspect the diff;
- commit a coherent unit with a descriptive message;
- push the branch and open a pull request;
- respond to review and automated checks;
- 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 DeliveryContinuous 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 reportA 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 ServiceThe following is an illustrative case. BackgroundA 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. ProblemThe team needs rapid feedback without connecting every test to the production database. It also needs a repeatable build and a controlled delivery process. AnalysisThe 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 solutionThe 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. OutcomeThe 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 pointsTesting 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 ResponsibilityNo 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 SummaryReliable 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 TermsUnit 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 QuestionsShort Answer Questions- What distinguishes a unit test from an integration test?
- Why are boundary values important in test design?
- What is the purpose of the
test dependency scope in Maven? - Why should generated build directories normally be excluded from Git?
- What is the role of a smoke test after deployment?
Long Answer Questions- Explain the test pyramid and discuss why a project should not rely exclusively on end-to-end tests.
- Describe how JUnit 5 assertions, lifecycle methods, and parameterized tests support maintainable test suites.
- Explain the principal Maven lifecycle phases from compilation through packaging and verification.
- Describe a responsible Git pull-request and code-review workflow for a Java team.
- Explain how CI, artifact versioning, external configuration, and rollback contribute to reliable delivery.
Analytical / Application Questions- 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.
- 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.
- 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 AnswersQuestion 1Which 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 2Which 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 3Which JUnit annotation identifies a test method? A. @Check B. @Run C. @Test D. @Verify Correct Answer: C. @Test Question 4What 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 5Which 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 6What 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 7Which 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 8What 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 9Why 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 10Which 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
|