skip to content

How do you write integration tests for a Maven plugin, and what role does the maven-invoker-plugin play?

level: seniorimportance: should knowfreq 28%

answer

  1. src/it/<name>/pom.xml sample projects
  2. install goal -> staging local repo
  3. invoker.properties = goals/result
  4. verify.groovy post-build asserts
  5. gate behind run-its profile

basics

~20 s

Unit tests check the Mojo class in isolation, but integration tests run the plugin inside a real Maven build. The maven-invoker-plugin executes small sample projects under src/it/, runs goals against your freshly-installed plugin, and verifies the results.

solid answer

~50 s

Plugin integration tests run sample projects through a *real* Maven invocation, which is the only way to verify descriptor generation, parameter injection, and lifecycle binding end-to-end. The **maven-invoker-plugin** drives this: each test is a little project under `src/it/<name>/` with its own `pom.xml`. Its `install` goal first installs the plugin (and dependencies) into a local staging repo so the ITs resolve the version under test; then `integration-test`/`run` invokes each sample project's build. You assert outcomes with: `invoker.properties` (which goals to run, expected exit, profiles), a `verify.groovy`/`verify.bsh` post-build script for assertions, and `expected-results` / build-log greps. It's typically bound to the `integration-test` phase and gated behind a `run-its` profile so it doesn't slow normal builds. For pure-Java unit testing of a Mojo you'd instead use `maven-plugin-testing-harness` (`AbstractMojoTestCase`) or just construct the Mojo and set fields, but that can't validate the generated descriptor or real injection — hence invoker ITs complement it.

code

bash · 7 lines
bash
# Run the plugin's integration tests (sample projects under src/it/)
mvn clean install -Prun-its

# maven-invoker-plugin will:
#  1. install the freshly built plugin into target/local-repo
#  2. run each src/it/*/pom.xml build into target/it/*
#  3. execute verify.groovy assertions and fail on any IT failure

go deeper

for a junior

Knows plugins need integration tests that run real builds, not just unit tests.

for a middle

Lays out src/it projects with invoker.properties and a verify script and wires the invoker plugin.

for a senior

Designs the staging-repo isolation, run-its profiling, and chooses harness vs. invoker per concern.

for a principal

Establishes plugin test strategy/CI matrix (multiple Maven/JDK versions) and reproducibility standards across the org's plugins.

## Two layers of testing 1. **Unit tests** — instantiate the Mojo, set its fields, call `execute()`, assert. The **maven-plugin-testing-harness** (`AbstractMojoTestCase`) can build a Mojo from a test POM and inject parameters. Fast, but it does *not* exercise the generated `plugin.xml`, real dependency resolution, or lifecycle binding. 2. **Integration tests** — run a complete Maven build that uses your plugin, end to end. This is what the **maven-invoker-plugin** is for. ## How maven-invoker-plugin works It runs a set of **sample projects** (one folder each) through real `mvn` invocations and checks the results. **Layout:** ``` src/it/ settings.xml # points at the staging local repo simple-it/ pom.xml # uses ${project.version} of the plugin under test invoker.properties # goals, expected result, profiles verify.groovy # post-build assertions ``` **Typical goals/bindings:** - `install` — copies the just-built plugin + its dependencies into an isolated local repository (`target/local-repo`) so the IT projects resolve the *version under test*, not a stale released one. - `integration-test` (alias `run`) — clones each `src/it/*` project to `target/it/*` and runs its build. - `verify` — fails the outer build if any IT failed. **Per-test control — `invoker.properties`:** ```properties invoker.goals = clean verify invoker.buildResult = success invoker.profiles = ci ``` **Post-build assertions — `verify.groovy`:** ```groovy assert new File(basedir, 'target/hello/greeting.txt').exists() assert new File(basedir, 'build.log').text.contains('Hello, Ada!') ``` ## Wiring it (plugin POM) ```xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-invoker-plugin</artifactId> <version>3.x.x</version> <configuration> <projectsDirectory>src/it</projectsDirectory> <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo> <localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath> <settingsFile>src/it/settings.xml</settingsFile> <postBuildHookScript>verify</postBuildHookScript> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>install</goal> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin> ``` ## Why ITs matter for plugins Only a real build proves that: the descriptor was generated correctly, `@Parameter` injection works from actual POM XML and `-D`, `requiresDependencyResolution` populated artifacts, the goal bound to the right phase, and the goal interoperates with other plugins. Gate them behind a `run-its` profile so normal `mvn test` stays fast and CI runs them explicitly.

  • Why does the invoker plugin install the plugin into an isolated local repo before running the ITs?
    So the sample projects resolve the exact version just built (via an isolated target/local-repo and custom settings.xml), not a previously released version sitting in the user's ~/.m2, guaranteeing tests exercise the code under test.
  • How is invoker IT different from maven-plugin-testing-harness?
    The harness unit-tests the Mojo class in-process (fast, but bypasses the real descriptor, resolution, and lifecycle). Invoker runs a full real Maven build of sample projects, validating end-to-end behavior including descriptor generation and parameter injection.

saying these in an interview costs you the question

  • Claiming unit tests alone fully validate a plugin (they skip the descriptor and real injection)
  • Forgetting to install the plugin into a staging repo so ITs use a stale released version
  • Running ITs in the default lifecycle so every build is slow
  • Confusing maven-invoker-plugin with maven-failsafe-plugin (failsafe is for app ITs, not plugin sample-project ITs)

context