View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/Apache Maven
Tools Library · We run this

Apache Maven review: what it is, tutorial and pitfalls

Apache Maven is the convention-based build and dependency tool for Java, configured through a declarative pom.xml. For test automation it is how most JVM suites are run: Surefire runs unit tests, Failsafe runs integration and end-to-end tests, and both write JUnit XML that every CI server reads. Maven 3.10.0, released on 27 September 2026, is the current release; Maven 4 is still a release candidate.

QAble runs this tool on client engagements. The assessment below is from that experience.

last reviewed
13 October 2026
version covered
Apache Maven 3.10
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Apache Maven at a glance

Official site
Vendor
Apache Software Foundation
Category
Build tools
Licence
Open source (Apache 2.0)
Platforms
JVM
Languages
XML
Integrates with
JUnit, TestNG, Jenkins, SonarQube, Selenium
First released
2004
Version reviewed
Apache Maven 3.10

Verdict

Our verdict on Apache Maven

Strengths

  • Convention over configuration: tests in src/test/java with standard names run with no setup
  • Surefire and Failsafe separate fast tests from integration suites, and Failsafe lets teardown run before failing
  • rerunFailingTestsCount reruns failures and records flaky tests in the XML instead of hiding them
  • JUnit XML reports in target/surefire-reports are read natively by every major CI server
  • forkCount runs test classes in several JVMs, with a fork number for isolating ports and schemas
  • Free under Apache 2.0, with the Maven Wrapper pinning one Maven version for every machine

Weaknesses

  • XML configuration is verbose, and custom logic means writing or finding a plugin
  • Nearest-wins dependency mediation causes runtime errors that look unrelated to recent changes
  • Misnamed test classes are skipped silently and the build still reports success
  • No incremental testing in core Maven: every build reruns every test in the modules it builds
  • Surefire 3.6.0 dropped TestNG suite XML files and JUnit versions before 4.12

Best for

Java test suites, Selenium, REST Assured, Appium or Playwright for Java, where the team wants a build that any Java engineer can read and that every IDE and CI server understands without plugins. The Surefire and Failsafe split is the right model for automation: fast tests fail early in the test phase, while integration and UI suites defer failure to verify so environment teardown still runs. Built-in reruns that record flaky tests in the XML make it a sound base for a regression suite.

Not for

Builds that need real programming logic, many custom tasks or incremental test runs on a large codebase, where Gradle’s task model, up-to-date checks and build cache save real time. Maven runs every test on every build unless you add extensions. It is also the wrong tool for non-JVM test suites despite plugins existing; a Playwright TypeScript suite belongs with npm, not wrapped in a POM.

Setup cost

Low. An archetype or a minimal POM gives a working build in minutes, and the standard layout means tests in src/test/java are found without configuration. Configure four things on day one: the Java release, source encoding, explicit versions for Surefire and Failsafe, and the Maven Wrapper, so every machine and CI runner uses the same Maven. For a UI suite, also decide early which tests are Failsafe integration tests by naming them *IT.

Maintenance burden

Moderate, concentrated in dependencies and plugins. Transitive dependency conflicts are resolved by nearest definition rather than newest version, which surfaces as NoSuchMethodError at runtime long after the change that caused it. Plugin majors occasionally change behaviour substantially: Surefire 3.6.0 removed its legacy providers and TestNG suite file support, which breaks any Selenium suite that selects its tests through a testng.xml file. Pin everything and upgrade deliberately, one plugin at a time.

Honest limitations

Verbose XML and little room for custom logic without writing a plugin. No incremental test execution in Maven itself, so unchanged modules rerun their tests. A test class whose name misses the default patterns is silently never run, and the build still passes, because failIfNoTests defaults to false. Parallelism inside one JVM depends on the test framework, and parallel forks need tests designed not to share ports, files or data.

How it works

What is Apache Maven and how does it work?

The POM and convention over configuration

A project is described by pom.xml: its coordinates, dependencies, plugins and properties. Everything not declared comes from conventions and from the Super POM that every project inherits, which is why a nearly empty POM can compile, test and package a project. Sources live in src/main/java, tests in src/test/java, test resources in src/test/resources, and build output in target. Parent POMs and the dependencyManagement and pluginManagement sections let an organisation fix versions and settings once for many projects. The effective POM, printed by mvn help:effective-pom, is the merged result Maven actually runs, and it is the first thing to check when a setting seems to be ignored.

Lifecycle phases bound to plugin goals

Maven builds by walking the default lifecycle: validate, compile, test, package, verify, install and deploy, with finer phases in between. Running a phase runs every phase before it, so mvn verify compiles, runs unit tests, packages and runs integration tests. Phases do nothing themselves; plugin goals are bound to them, such as surefire:test to test, and failsafe:integration-test and failsafe:verify to the phases of the same names. Between package and verify sit pre-integration-test and post-integration-test, where plugins can start and stop an application server, a container or a mock service around the integration suite.

Dependency resolution and scopes

Dependencies are fetched from remote repositories into the local repository, ~/.m2/repository by default, along with their own dependencies, transitively. When two paths bring different versions of one library, Maven picks the one nearest to your project in the tree, not the newest, which is the root cause of most NoSuchMethodError and ClassNotFoundException failures in test suites. Scopes limit where a dependency is visible: test scope keeps JUnit, Selenium and AssertJ off the production classpath. Importing a bill of materials such as junit-bom aligns a family of artefacts to one version. Maven 3.10 moved to Resolver 2, the same resolver line as Maven 4, with stricter validation of coordinates and POMs.

Surefire, Failsafe and the forked test JVM

Surefire runs tests in the test phase, in a forked JVM separate from Maven, and by default includes classes named Test*, *Test, *Tests and *TestCase. Failsafe runs classes named IT*, *IT and *ITCase in the integration-test phase, records results without failing, and fails the build in verify after post-integration-test has run. Both write one XML file per test class, to target/surefire-reports and target/failsafe-reports. Since 3.6.0, both run every framework through one JUnit Platform provider: JUnit 5 and 6 natively, JUnit 4.12 or later through the Vintage engine, and TestNG 6.14.3 or later through its JUnit Platform engine.

Strengths

What it does well

Flaky test reruns that leave a record

Setting rerunFailingTestsCount reruns failed tests after the main run, up to the given count. A test that passes on a rerun is reported as a flake: the console summary counts Flakes, and the XML records each failed attempt as flakyFailure inside the test case. failOnFlakeCount fails the build when flakes exceed a threshold, so retries absorb noise without letting flakiness grow unseen.

Parallel forks with per-fork isolation

forkCount sets how many test JVMs run at once, and a value such as 1C scales with CPU cores. The placeholder surefire.forkNumber can be used in system properties, argLine and environment variables, so each fork gets its own database schema, port or download folder. Process-level parallelism avoids the thread-safety problems of running tests concurrently inside one JVM.

Tag and test selection from the command line

groups and excludedGroups take JUnit tags or tag expressions, such as smoke | api, and TestNG groups, so one suite can serve pull request, nightly and release runs. -Dtest selects classes or methods with patterns, and surefire.failIfNoSpecifiedTests controls whether a selection that matches nothing fails, which matters in multi-module builds.

Environment setup around integration suites

Because Failsafe defers failure to verify, plugins bound to pre-integration-test and post-integration-test can start a server or container before the suite and always stop it afterwards. Running mvn verify, not mvn integration-test, is what guarantees the teardown runs.

Profiles for environments and suites

Profiles switch properties, plugin configuration or dependencies by name or condition. A QA project can define a profile per target environment that sets the base URL and credentials source, and a profile per suite that changes groups and forkCount, all selected in CI with -P without editing the POM.

One Maven version everywhere with the wrapper

The Maven Wrapper adds mvnw scripts and a properties file naming the Maven version. Anyone, including a CI runner, runs ./mvnw and gets exactly that version, downloaded on first use. Moving a team to Maven 3.10 then becomes one reviewed commit rather than a request to reinstall.

Pricing

Apache Maven pricing and licensing

Maven is free and open source under the Apache 2.0 licence, maintained by the Apache Software Foundation, and so are Surefire, Failsafe and the other core plugins. There are no paid tiers. Costs come from around it: CI time spent resolving dependencies and running tests, an internal repository manager if you mirror Maven Central or publish internal libraries, and the engineering time to keep plugins and dependencies current.

  • Licence: Apache 2.0 for Maven, its core plugins and the Maven Wrapper.
  • Free tier: everything. There is no commercial edition from the project.
  • Infrastructure: a repository manager or mirror, hosted by you or bought as a service, is common in organisations and is a separate cost.
  • CI cost scales with test time and dependency downloads; caching ~/.m2/repository in CI is the cheapest saving available.
  • Optional commercial tooling, such as Develocity build scans and test acceleration, supports Maven builds and is priced by its vendor.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Apache Maven tutorial: getting started

Setup, step by step

  1. Install a JDK; Maven 3.10 runs on Java 8 or later, and JUnit 6 needs Java 17 or later.
  2. Install Maven, check it with mvn --version, and generate a project from the quickstart archetype.
  3. Add the Maven Wrapper with mvn wrapper:wrapper and commit it, then use ./mvnw from here on.
  4. Set the Java release and encoding, import the JUnit BOM and pin the Surefire version.
  5. Write a test class whose name ends in Test and run ./mvnw test.
  6. Open target/surefire-reports to see the XML your CI server will publish.

Install

mvn --version
mvn archetype:generate -DgroupId=com.example.qa -DartifactId=qa-suite \
  -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.5 -DinteractiveMode=false
cd qa-suite
mvn wrapper:wrapper
./mvnw test

A first example

<!-- pom.xml: the parts that matter for a test suite -->
<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
  <dependencies>
    <!-- One version for every JUnit artefact -->
    <dependency>
      <groupId>org.junit</groupId>
      <artifactId>junit-bom</artifactId>
      <version>6.1.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <!-- Pin it: an unpinned plugin can change behaviour with no commit -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.6.0</version>
    </plugin>
  </plugins>
</build>

What trips people up first

Tests that never run. Surefire only picks up classes matching Test*, *Test, *Tests or *TestCase, so a class called LoginChecks or LoginSpec compiles, is never executed, and the build reports success with Tests run: 0. Check the summary line on the first run, name classes to the convention or set includes, and consider failIfNoTests in CI. The second early trap is the wrong plugin: integration and UI tests named *IT are run by Failsafe, which is not active until you add it to the POM with its integration-test and verify goals, and which only runs on mvn verify.

In practice

Using Apache Maven in practice

Structuring a test automation project

For a standalone suite, keep the standard layout and organise packages by feature or service, with page objects, API clients and test data builders in their own packages under src/test/java. Name fast checks *Test for Surefire and browser or environment-dependent tests *IT for Failsafe, so mvn test gives quick feedback and mvn verify runs everything. For larger estates, use a multi-module build: a shared module for the framework code, then one module per product area, built with -pl and -am to run one area and what it depends on. Keep versions in a parent POM’s dependencyManagement and pluginManagement so every module agrees.

Configuring Failsafe, reruns and parallel forks

This configuration runs UI tests in Failsafe with up to two reruns, fails the build if more than three tests needed a rerun, and runs two forked JVMs, each with its own browser download directory. groups is left out of the POM on purpose: it reads the groups user property, so CI can pass -Dgroups=smoke for pull requests and omit it at night to run every test. The fork number placeholder is what makes forkCount safe for browser suites.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.6.0</version>
  <configuration>
    <rerunFailingTestsCount>2</rerunFailingTestsCount>
    <!-- Fail once flakiness passes a threshold instead of hiding it -->
    <failOnFlakeCount>3</failOnFlakeCount>
    <forkCount>2</forkCount>
    <systemPropertyVariables>
      <!-- 1, 2, ... per fork: keeps parallel browsers apart -->
      <downloadDir>${project.build.directory}/downloads-${surefire.forkNumber}</downloadDir>
    </systemPropertyVariables>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Running Maven suites in CI

Run through the wrapper in batch mode so logs carry no download progress, cache the local repository between runs, and publish the XML from both report folders even when the build fails. In GitHub Actions, setup-java’s cache input handles the repository cache, and a JUnit reporting action turns the XML into a check run. Pass environment details as properties or profiles rather than editing files in the pipeline, and keep secrets in the CI system’s secret store, read by the suite as environment variables.

# .github/workflows/regression.yml
name: regression
on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:
permissions:
  contents: read
  checks: write
jobs:
  ui:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: '21'
          cache: maven
      - run: ./mvnw -B -Pstaging -Dgroups=regression verify
      - uses: mikepenz/action-junit-report@v6
        if: ${{ !cancelled() }}
        with:
          report_paths: '**/target/*-reports/TEST-*.xml'

Upgrading to Maven 3.10 and Surefire 3.6

Maven 3.10 was released on 27 September 2026 and is designed as a low-friction upgrade, but three changes can bite a test project. Credentials in settings.xml are now scoped to the repositories declared with exactly the same server id, so an id that relied on prefix matching stops authenticating; use the new aliases or repositoryOrigins elements. Resolver validation is stricter, and maven.resolver.validation set to mild or off is a temporary escape hatch while you fix flagged coordinates. A plugin that requests a misconfigured toolchain now fails rather than falling back. Upgrade Surefire and Failsafe to 3.6.0 separately: it requires JUnit 4.12 or later and TestNG 6.14.3 or later, and suiteXmlFiles is no longer supported.

Diagnosing dependency and report problems

Most confusing failures in Maven test suites come from the classpath. mvn dependency:tree -Dverbose shows which version won and which were omitted for conflict, and adding the library to dependencyManagement pins the version you need. help:effective-pom shows the configuration Maven actually applied. When a test passes alone but fails in the suite, rerun with forkCount=1 and reuseForks=false to rule out shared static state, then fix the leak rather than keeping the slow setting. Keep the XML reports as CI artefacts: they include system-out and, for flakes, every failed attempt.

Pitfalls

Common pitfalls

Unpinned plugin versions

Why it happens: Without a version, Maven uses defaults that change with the Maven release, so upgrading Maven can change Surefire behaviour with no edit to the POM.

The fix: Pin Surefire, Failsafe and the compiler plugin in pluginManagement, and upgrade them in their own commits with the release notes open.

Test classes silently skipped

Why it happens: Only default name patterns are included, and failIfNoTests defaults to false, so a misnamed class never runs and the build stays green.

The fix: Follow the naming patterns or set includes, watch Tests run in the summary, and set failIfNoTests to true in CI for test-only modules.

TestNG suites broken after upgrading Surefire to 3.6.0

Why it happens: 3.6.0 removed the legacy providers and no longer supports suiteXmlFiles. TestNG now runs through the JUnit Platform and needs 6.14.3 or later.

The fix: Move suite selection to groups or JUnit Platform suites, upgrade TestNG, or stay on 3.5.x, or use the transitional legacy provider, while you migrate.

Running mvn integration-test directly

Why it happens: It stops before post-integration-test, so servers or containers started for the suite are never torn down, and the next run finds ports in use.

The fix: Always run mvn verify for integration and UI suites, so teardown runs before Failsafe fails the build.

NoSuchMethodError after adding a dependency

Why it happens: Nearest-wins mediation chose an older transitive version of a shared library, such as a Selenium or Jackson module, that lacks the method another library calls.

The fix: Inspect with dependency:tree -Dverbose, align families with a BOM, and pin the version in dependencyManagement.

Repository authentication failing after the 3.10 upgrade

Why it happens: Maven 3.10 scopes server credentials to repositories with exactly the same id and no longer shares them across ids with a common prefix.

The fix: Match server ids exactly to repository and mirror ids, or declare aliases or repositoryOrigins on the server entry in settings.xml.

Parallel forks colliding on shared resources

Why it happens: With forkCount above 1, several JVMs run at once and fight over the same port, download folder or test account.

The fix: Derive per-fork values from surefire.forkNumber, give each fork its own test data, and keep tests independent of execution order.

Compared

Apache Maven alternatives compared

ToolBuild definitionLicenceFlaky test retriesParallel testsSkips unchanged testsTest result output
Apache MavenDeclarative XML POMApache 2.0Built in: rerunFailingTestsCountforkCount JVMs, framework threadsNo, not in core MavenJUnit XML per test class
GradleKotlin or Groovy DSLApache 2.0Official test-retry pluginmaxParallelForks, parallel project buildsYes, up-to-date checks and build cacheJUnit XML plus HTML report
BazelStarlark BUILD filesApache 2.0flaky attribute: up to three attemptsshard_count across test runner processesYes, passing results are cachedXML per test target

Head-to-head comparisons

Gradle vs Maven

FAQ

Frequently asked questions

What is Apache Maven?

Apache Maven is the convention-based build and dependency tool for Java, configured through a declarative pom.xml. For test automation it is how most JVM suites are run: Surefire runs unit tests, Failsafe runs integration and end-to-end tests, and both write JUnit XML that every CI server reads. Maven 3.10.0, released on 27 September 2026, is the current release; Maven 4 is still a release candidate.

What are the best Apache Maven alternatives?

The alternatives most often weighed against Apache Maven are Gradle and Bazel. The comparison on this page sets them side by side on build definition, licence, flaky test retries, parallel tests, skips unchanged tests and test result output, and the verdict above says where Apache Maven is the wrong choice and which of them to pick instead.

Is Maven free?

Yes. Apache Maven, Surefire, Failsafe and the Maven Wrapper are open source under the Apache 2.0 licence, with no paid edition. The costs that come with it are indirect: CI time for dependency downloads and test runs, a repository manager if your organisation mirrors Maven Central or publishes internal libraries, and the time to keep plugins and dependencies up to date.

Maven or Gradle for a test automation project?

For a standalone Java test suite, Maven is usually the simpler choice: the POM is predictable, every Java engineer can read it, and Surefire’s built-in reruns and flaky reporting cover what most suites need. Choose Gradle when the suite lives inside a Gradle codebase, especially Android, or when incremental builds and the build cache would save meaningful CI time on a large multi-project build.

What changed in Maven 3.10?

Maven 3.10.0, released on 27 September 2026, moves to Maven Resolver 2, the resolver line used by Maven 4, with stricter validation and an escape hatch, maven.resolver.validation. It scopes settings.xml credentials to repositories with exactly matching ids, makes reproducible builds the default, lists every module in the reactor summary with failures last, and fails when a plugin requests a misconfigured toolchain. It still runs on Java 8 or later.

Should I move to Maven 4?

Not for production builds yet. On 13 October 2026, Maven 4 was still at release candidate 7, published on 24 September 2026, while 3.10 is the current stable release. Maven 3.10 shares Resolver 2 with Maven 4, which makes it a sensible stepping stone. Try Maven 4 on a branch to find problems early, but keep CI on 3.10 until a final 4.0.0 is released.

How do I run a single test with Maven?

Use -Dtest with a class or method pattern, for example ./mvnw test -Dtest=LoginTest or -Dtest=LoginTest#rejectsBadPassword. For Failsafe integration tests use -Dit.test instead and run verify. In a multi-module build, add -Dsurefire.failIfNoSpecifiedTests=false so modules without a match do not fail, or limit the build with -pl.

How do I rerun flaky tests in Maven?

Set rerunFailingTestsCount on Surefire or Failsafe, or pass -Dsurefire.rerunFailingTestsCount=2 on the command line. Failed tests are rerun after the main run; one that then passes is counted as a flake in the summary and recorded with flakyFailure entries in the XML. Add failOnFlakeCount to fail the build when too many tests needed a rerun. It works for JUnit 4.12 and later, JUnit 5 and 6, and TestNG.

How do I run Maven tests in parallel?

Two ways, which can be combined. forkCount runs test classes in several JVMs at once, for example 2 or 1C for one per core, and is the safer option for UI suites because forks share no memory. Inside a JVM, enable the framework’s own parallelism, such as JUnit Platform’s junit.jupiter.execution.parallel.enabled through Surefire’s configurationParameters. Separately, -T parallelises modules in a multi-module build.

Alternatives

Apache Maven alternatives worth weighing

Resources

Where to go next

Keep going

Prepare, or get it done

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor