skip to content

How would you implement 're-run failed tests' from a previous Tooling API run? Explain withTests and TestOperationDescriptor.

level: seniorimportance: should knowfreq 25%

answer

  1. addProgressListener + OperationType.TEST
  2. TestFinishEvent.result -> TestFailureResult
  3. collect TestOperationDescriptor
  4. withTests(descriptors) re-runs exactly
  5. robust for parameterized/dynamic tests

basics

~10 s

During the first run, attach a progress listener for TEST events and collect the TestOperationDescriptors of failed tests. On the next run, pass those descriptors to TestLauncher.withTests(...) to re-execute exactly those tests.

solid answer

~40 s

The Tooling API streams **test events** during a run if you call `addProgressListener(listener, OperationType.TEST)`. Each event carries a `TestOperationDescriptor` identifying a specific test (its display name, class, method, and parent). A finish event also carries a result (success/failure/skipped). To implement 're-run failed', you capture the descriptors of tests whose result was a failure during run #1. For run #2 you build a new `TestLauncher` and call `withTests(failedDescriptors)` (or `withTests(Iterable)`), which re-runs exactly those identified tests — precisely, including the right task path, because the descriptor pins the test's origin. This is more robust than reconstructing class/method names by hand, since descriptors disambiguate parameterized tests, multiple source sets, and dynamic tests that a string class name can't.

code

kotlin · 13 lines
kotlin
val failed = mutableListOf<TestOperationDescriptor>()
connection.newTestLauncher()
    .withJvmTestClasses("com.acme.SuiteTest")
    .addProgressListener({ e ->
        if (e is TestFinishEvent && e.result is TestFailureResult) {
            failed += e.descriptor
        }
    }, setOf(OperationType.TEST))
    .run()

if (failed.isNotEmpty()) {
    connection.newTestLauncher().withTests(failed).run()
}

go deeper

for a junior

Know that you can listen for test results and re-run selected tests.

for a middle

Wire a TEST progress listener and feed captured names back into a new launcher.

for a senior

Use TestOperationDescriptor + withTests for fidelity with parameterized/dynamic and cross-source-set tests.

for a principal

Design the re-run-failed feature end to end: event capture, descriptor persistence within a session, and debug integration.

## Capturing what ran Every launcher (build, test) accepts progress listeners. For tests specifically: ```kotlin val failed = mutableListOf<TestOperationDescriptor>() connection.newTestLauncher() .withJvmTestClasses("com.acme.SuiteTest") .addProgressListener( ProgressListener { event -> if (event is TestFinishEvent) { val desc = event.descriptor // TestOperationDescriptor if (event.result is TestFailureResult) failed += desc } }, setOf(OperationType.TEST) ) .run() ``` - `OperationType.TEST` scopes the stream to test start/finish events (you can combine with `TASK`, etc.). - A `TestOperationDescriptor` knows the test's **display name, class name, method name, and parent descriptor** — enough to re-identify it exactly. - The finish event's `result` distinguishes `TestSuccessResult`, `TestFailureResult`, `TestSkippedResult`. ## Re-running exactly those tests ```kotlin if (failed.isNotEmpty()) { connection.newTestLauncher() .withTests(failed) // re-runs precisely the captured tests .run() } ``` `withTests(TestOperationDescriptor...)` / `withTests(Iterable<...>)` takes the descriptors and reconstructs the exact selection, including the originating task. This sidesteps the ambiguity you'd hit by re-deriving `withJvmTestMethods` strings: - **Parameterized / dynamic tests** don't have stable plain method names; the descriptor captures the concrete invocation. - **Same class in multiple source sets** is disambiguated because the descriptor carries its task origin. ## Why this is the senior answer A naive implementation collects `class#method` strings and feeds them to `withJvmTestMethods`. That works for simple cases but loses fidelity for parameterized/dynamic tests and cross-source-set duplicates. Descriptor-based `withTests` is the fidelity-preserving path and is what mature IDE integrations use for 're-run failed'. ## Tie-in to the rest of the API This combines the **selection** API (TestLauncher), the **events** API (progress listeners), and optionally **debugTestsOn** to debug just the re-run failures.

  • Why is withTests(descriptors) better than re-deriving withJvmTestMethods strings?
    Descriptors preserve exact identity for parameterized/dynamic tests and disambiguate the same class across source sets, which plain class#method strings cannot.
  • How do you receive test results during the run?
    Register a ProgressListener with OperationType.TEST; TestFinishEvent carries a result you can match against TestFailureResult/TestSuccessResult/TestSkippedResult.
  • Can you debug only the re-run failures?
    Yes — build the second TestLauncher with withTests(failed) and add debugTestsOn(port) so only those tests run under the debugger.

saying these in an interview costs you the question

  • Reconstructing class#method strings and assuming they cover parameterized/dynamic tests.
  • Forgetting to scope the progress listener to OperationType.TEST.
  • Reusing descriptors from a different project connection/build where they no longer resolve.

context