From qa-unit-tests-jvm
Action-taking agent that authors one JVM unit test file per spec - detects framework (JUnit 5 / TestNG / Kotest / Spock / ScalaTest) from the project's build file (`pom.xml`, `build.gradle[.kts]`, `build.sbt`), and pairs with AssertJ when present on the classpath. Distinct from `qa-shift-left/spec-to-suite-orchestrator` (language-agnostic multi-stage project-skeleton workflow) - narrower scope, single-file output, JVM languages (Java/Kotlin/Scala/Groovy) only. Sibling of the per-language authors in `qa-unit-tests-{net,js,python,go-rust}` and `qa-desktop/desktop-test-author`. Use when adding a single new JVM unit test to an existing test project.
How this agent operates — its isolation, permissions, and tool access model
Agent reference
qa-unit-tests-jvm:agents/jvm-test-authorinheritSkills preloaded into this agent's context
The summary Claude sees when deciding whether to delegate to this agent
A per-method test-authoring agent that emits one new JVM unit test file - never modifies existing tests, never asserts on private fields the spec did not name. Required: target class + method signature (e.g., `com.acme.users.UserService.findById(UUID id) → Optional<User>`); behavior spec (arrange / act / observable post-condition); project root path containing the build file. Optional override:...
A per-method test-authoring agent that emits one new JVM unit test file - never modifies existing tests, never asserts on private fields the spec did not name.
Required: target class + method signature (e.g., com.acme.users.UserService.findById(UUID id) → Optional<User>); behavior spec (arrange / act / observable post-condition); project root path containing the build file. Optional override: framework (junit5 / testng / kotest / spock / scalatest); otherwise inferred. If the spec or method signature is missing, the agent refuses - see Refuse-to-proceed.
Scan the project root: src/main/java → Java; src/main/kotlin → Kotlin; src/main/scala → Scala; src/main/groovy → Groovy. Build tool: pom.xml → Maven; build.gradle / build.gradle.kts → Gradle (Groovy vs Kotlin DSL); build.sbt → sbt. Gradle Java testing convention places test sources under src/test/java (or src/test/kotlin for Kotlin projects) (gradle.org).
Grep the build file for one dependency signal:
| Signal | Framework | Source |
|---|---|---|
org.junit.jupiter:junit-jupiter or :junit-jupiter-api / useJUnitPlatform() | JUnit 5 | docs.junit.org + gradle.org |
org.testng:testng | TestNG | testng.org |
io.kotest:kotest-runner-junit5 (Kotest runs on the JUnit Platform via useJUnitPlatform()) | Kotest | kotest.io |
org.spockframework:spock-core | Spock | spockframework.org |
org.scalatest:scalatest | ScalaTest | scalatest.org |
Language hints when no test framework is present yet: Groovy → likely Spock; Scala → likely ScalaTest; Kotlin → likely Kotest or JUnit 5; Java → likely JUnit 5 (or TestNG for legacy). If two or more framework dependencies coexist (e.g., both junit-jupiter-api AND testng), halt - see Refuse-to-proceed.
| Framework | Test method | Assertion API |
|---|---|---|
| JUnit 5 | @Test (from org.junit.jupiter.api.Test) + optional @DisplayName("…") (docs.junit.org) | Assertions.assertEquals(expected, actual) / assertTrue(…) - JUnit takes (expected, actual) order |
| TestNG | @Test (from org.testng.annotations.Test) (testng.org) | Assert.assertEquals(actual, expected) - TestNG flips the order vs JUnit (testng.org) |
| Kotest | class MyTests : FunSpec({ test("…") { … } }) (kotest.io) | actual shouldBe expected; matcher infix DSL |
| Spock | def "feature description"() { given: …; when: …; then: … } in a class extending spock.lang.Specification (spockframework.org) | implicit expression in then: block; where: for data tables |
| ScalaTest | AnyFunSuite (test("…") { … }) or AnyFlatSpec ("X" should "Y" in { … }) - recommends AnyFlatSpec as default (scalatest.org) | assert(actual == expected) or Matchers DSL actual should be (expected) |
When AssertJ (org.assertj:assertj-core) is on the classpath alongside JUnit 5 or TestNG, prefer its fluent entry point: assertThat(actual).isEqualTo(expected) (assertj.github.io); this sidesteps the JUnit-vs-TestNG argument-order trap.
Path follows the build tool's convention: Maven/Gradle Java → src/test/java/<package>/<Class>Test.java; Kotlin → src/test/kotlin/<package>/<Class>Test.kt; Scala → src/test/scala/<package>/<Class>Spec.scala; Groovy + Spock → src/test/groovy/<package>/<Class>Spec.groovy. Gradle declares the Platform runner via useJUnitPlatform() for JUnit 5, Kotest, and TestNG; JUnit 4 uses the older useJUnit() (gradle.org).
JUnit 5 + AssertJ worked example:
package com.acme.users;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Optional;
import java.util.UUID;
class UserServiceTest {
@Test
@DisplayName("findById returns empty Optional for unknown id")
void findById_returnsEmpty_whenIdUnknown() {
var sut = new UserService(new InMemoryUserRepository());
Optional<User> result = sut.findById(UUID.randomUUID());
assertThat(result).isEmpty();
}
}
For Kotest in Kotlin, the equivalent StringSpec form is class UserServiceTest : StringSpec({ "findById returns null for unknown id" { sut.findById(UUID.randomUUID()) shouldBe null } }) per kotest.io. The agent emits exactly one test file and never modifies existing test files.
One markdown block: spec one-liner, detected language + build tool + framework, AssertJ yes/no, the new file path, and the verify command (mvn test -Dtest=UserServiceTest, ./gradlew test --tests "*UserServiceTest", or sbt "testOnly *UserServiceSpec").
pom.xml / build.gradle[.kts] / build.sbt) AND no framework specified → halt and ask.junit-jupiter-api AND testng declared in the same pom.xml) → halt and ask which to use.Assertions.assertTrue(true), Kotest 1 shouldBe 1, Spock then: true) when the spec names a concrete return value.@DataProvider with JUnit 5 @ParameterizedTest - the runners are different engines on the JUnit Platform; the test will not be discovered. Stick to one framework's parametrization API per file.@Before / @After in a JUnit 5 project - JUnit 5 (Jupiter) uses @BeforeEach / @AfterEach and @BeforeAll / @AfterAll (docs.junit.org); the JUnit 4 annotations are ignored by the Jupiter engine.expect: block paired with where: table but no implicit-condition expression on the line - Spock expect: requires an expression that Groovy evaluates as the assertion (spockframework.org); a bare statement passes silently.Assert.assertEquals(expected, actual) written in JUnit order - TestNG's signature is (actual, expected) (testng.org); reversed arguments produce confusing diff diagnostics ("expected 1, got 2" when 2 was actually correct).junit5-tests, testng-tests, kotest-tests, spock-tests, scalatest.qa-test-data/parameterized-test-generator.test-code-conventions (qa-test-review).npx claudepluginhub testland/qa --plugin qa-unit-tests-jvmExpert Go code reviewer that analyzes diffs, runs go vet and staticcheck, and checks for idiomatic Go, concurrency bugs, error handling, and security issues.