skip to content

You need a Mockito ArgumentCaptor for an argument of type List<String>. Explain the problem with ArgumentCaptor.forClass and what the alternatives are.

level: middleimportance: nice to knowfreq 33%

answer

  1. No List<String>.class - erasure
  2. forClass(List.class) = raw + unchecked cast
  3. @Captor reads the field's generic signature
  4. ArgumentCaptor.captor() since Mockito 5.7
  5. Generics are compile-time only; capture() matches anything

basics

~20 s

forClass takes a Class object, and List<String>.class does not exist - you can only pass List.class, which yields a raw-typed captor and an unchecked assignment warning. Use the @Captor annotation, whose field generics are read from the declaration, or ArgumentCaptor.captor() in newer Mockito versions.

solid answer

~40 s

`ArgumentCaptor.forClass(Class<T>)` needs a class literal, and Java erasure means there is no `List<String>.class`. So you write `ArgumentCaptor.forClass(List.class)`, get an `ArgumentCaptor<List>`, and either accept a raw type or add an unchecked cast plus `@SuppressWarnings`. The idiomatic fix is the annotation, which takes its type from the field's generic signature: ```java @Captor ArgumentCaptor<List<String>> captor; ``` It requires `@ExtendWith(MockitoExtension.class)` or `MockitoAnnotations.openMocks(this)` to be initialised. Recent Mockito versions (5.7 onward) also expose `ArgumentCaptor.captor()`, which infers the type from the assignment target: `ArgumentCaptor<List<String>> c = ArgumentCaptor.captor();`. Either way the generic parameter is compile-time sugar only - erasure means nothing checks that captured elements really are `String`s at runtime, so a heterogeneous list would only blow up when you read it.

code

java · 10 lines
java
// noisy: unchecked cast
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> a =
    ArgumentCaptor.forClass((Class<List<String>>)(Class<?>) List.class);

// idiomatic: annotation, needs MockitoExtension / openMocks
@Captor ArgumentCaptor<List<String>> b;

// Mockito 5.7+: inferred from the assignment target
ArgumentCaptor<List<String>> c = ArgumentCaptor.captor();

go deeper

for a junior

Say that class literals cannot carry type arguments, so use @Captor for generic types and remember it needs Mockito initialisation.

for a middle

Explain erasure and the forClass signature, contrast @Captor reading the field's generic signature, and mention ArgumentCaptor.captor() on Mockito 5.7+.

for a senior

Add that the type parameter is compile-time only, that capture() matches anything including null, and note the varargs component-type rule.

for a principal

Standardise on MockitoExtension plus @Captor across the codebase so generics and initialisation are uniform, and treat unchecked-cast captor idioms in review as a signal that the test setup convention was bypassed.

## The root cause: erasure `ArgumentCaptor.forClass` has the signature `static <U,S extends U> ArgumentCaptor<U> forClass(Class<S> clazz)`. It needs a runtime `Class` token, and Java provides class literals only for raw types: `List.class` exists, `List<String>.class` does not. So a captor for a parameterised type cannot be expressed through `forClass` without losing the type argument. What people write: ```java @SuppressWarnings("unchecked") ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass((Class<List<String>>)(Class<?>) List.class); ``` It works, but it is noise, and the suppression hides a real (if harmless here) unchecked operation. ## Option 1: the @Captor annotation ```java @ExtendWith(MockitoExtension.class) class ImporterTest { @Mock Repo repo; @Captor ArgumentCaptor<List<String>> captor; @Test void savesAllNames() { new Importer(repo).importAll(); verify(repo).saveAll(captor.capture()); assertThat(captor.getValue()).containsExactly("ann", "bob"); } } ``` Mockito reads the field's declared generic type via reflection on the field's `Type` (which survives erasure in the class file's signature attribute) and builds a correctly typed captor. No cast, no warning. Initialisation happens through `MockitoExtension` (JUnit 5), `MockitoAnnotations.openMocks(this)` in a `@BeforeEach`, or the older `MockitoJUnitRunner` on JUnit 4. Forgetting the initialisation leaves the field null and produces a `NullPointerException` on `capture()` - one of the most common Mockito setup mistakes. ## Option 2: ArgumentCaptor.captor() Mockito 5.7 added a factory that takes no class token and infers `T` from the assignment context: ```java ArgumentCaptor<List<String>> captor = ArgumentCaptor.captor(); ``` This keeps the captor local to the test method - useful when only one test needs it and a field would be dead weight elsewhere. It also has a varargs form for reified type arguments in some usages. On older versions the annotation remains the way. ## What the generic parameter does and does not buy It is **compile-time only**. `capture()` returns `T` so the argument type-checks at the call site, and `getValue()` returns `T` so no cast is needed when asserting. Nothing verifies at runtime that the captured list contains only `String`s: type arguments are erased, and Mockito's captor stores `Object`s internally. If production code passed a `List<Integer>` through a raw-typed path, capture would succeed and the failure would surface as a `ClassCastException` at the point you read an element. The same is true of the non-generic case in a subtler way: `forClass(Email.class)` does not enforce that the captured argument is an `Email` at capture time either - `capture()` matches any value, including `null`. If the mocked parameter type is `Object`, a captor declared for `Email` will happily capture something else and throw on read. ## Related typing gotchas **Varargs.** Capturing a varargs parameter is special: declare the captor for the component type (`ArgumentCaptor<String>` for a `String...` parameter) and `getAllValues()` returns the individual elements. Mixing this up with an array-typed captor is a frequent surprise. **Primitives.** `ArgumentCaptor.forClass(int.class)` is not what you want; use `Integer.class`, since Mockito stores boxed values. **Nulls.** `getValue()` can legitimately return `null` if the code passed `null`; that is distinct from "nothing captured", which throws a `MockitoException`. ## Interview framing The grader wants: erasure explains why `forClass` cannot express `List<String>`; `@Captor` reads the field's generic signature and is the idiomatic answer; `ArgumentCaptor.captor()` is the modern local-variable alternative; and the type parameter is compile-time convenience, not a runtime guarantee.

  • Does the captor's generic type give any runtime safety?
    No. Type arguments are erased and Mockito stores captured arguments as Objects, so capture() will record whatever was passed. The generic parameter only removes casts at the call site and on getValue(). A mismatched element type surfaces later as a ClassCastException when the value is read or as a confusing assertion failure.
  • What is required for a @Captor field to be non-null in a JUnit 5 test?
    The test class must be annotated with @ExtendWith(MockitoExtension.class), or the setup must call MockitoAnnotations.openMocks(this) in a @BeforeEach and close the returned resource if desired. Without initialisation the field stays null and captor.capture() throws NullPointerException, which is one of the most common Mockito wiring mistakes.

saying these in an interview costs you the question

  • Claiming List<String>.class is valid Java
  • Believing the captor's generic type is checked at capture time
  • Adding @SuppressWarnings without knowing why the cast is unchecked
  • Using @Captor without MockitoExtension or openMocks and being surprised by a NullPointerException
  • Declaring an array-typed captor for a varargs parameter instead of the component type

context