1. 概述
本文将深入介绍 Mockito 提供的 AdditionalAnswers
类及其核心方法。这个类在编写单元测试时非常实用,尤其当你需要让 mock 方法返回其参数本身,而不是硬编码的返回值时。
简单来说,AdditionalAnswers
提供了一组开箱即用的 Answer
实现,让你可以灵活地控制 mock 方法的返回行为——比如直接返回第几个参数,或者基于函数式接口构建更复杂的响应逻辑。
✅ 适用场景:
- 模拟类似
save(entity)
这种“传入什么就返回什么”的方法 - 需要根据参数动态决定返回值
- 简化
thenAnswer()
中的冗余代码
下面我们通过实际代码来演示它的用法。
2. 返回方法参数
AdditionalAnswers
最常见的用途就是让 mock 方法返回其调用时传入的某个参数。这在模拟 DAO 或 Repository 层的 save
、update
方法时特别有用。
示例模型与依赖类
我们先定义一个简单的 Book
实体类:
public class Book {
private Long bookId;
private String title;
private String author;
private int numberOfPages;
// constructors, getters and setters
}
接着是 BookRepository
,它包含几个典型方法:
public class BookRepository {
public Book getByBookId(Long bookId) {
return new Book(bookId, "To Kill a Mocking Bird", "Harper Lee", 256);
}
public Book save(Book book) {
return new Book(book.getBookId(), book.getTitle(), book.getAuthor(), book.getNumberOfPages());
}
public Book selectRandomBook(Book bookOne, Book bookTwo, Book bookThree) {
List<Book> selection = new ArrayList<>();
selection.add(bookOne);
selection.add(bookTwo);
selection.add(bookThree);
Random random = new Random();
return selection.get(random.nextInt(selection.size()));
}
}
对应的 BookService
类:
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public Book getByBookId(Long id) {
return bookRepository.getByBookId(id);
}
public Book save(Book book) {
return bookRepository.save(book);
}
public Book selectRandomBook(Book book1, Book book2, Book book3) {
return bookRepository.selectRandomBook(book1, book2, book3);
}
}
测试类基础结构如下(使用 JUnit + Mockito):
@RunWith(MockitoJUnitRunner.class)
public class BookServiceUnitTest {
@InjectMocks
private BookService bookService;
@Mock
private BookRepository bookRepository;
// test methods
}
2.1 返回第一个参数:returnsFirstArg()
当我们 mock 一个 save
方法时,通常期望它原样返回传入的对象(类似持久化后的实体)。这时可以用 AdditionalAnswers.returnsFirstArg()
:
@Test
public void givenSaveMethodMocked_whenSaveInvoked_ThenReturnFirstArgument_UnitTest() {
Book book = new Book("To Kill a Mocking Bird", "Harper Lee", 256);
Mockito.when(bookRepository.save(any(Book.class))).then(AdditionalAnswers.returnsFirstArg());
Book savedBook = bookService.save(book);
assertEquals(savedBook, book);
}
✅ 效果:bookRepository.save()
被调用时,直接返回第一个参数 book
,无需手动 new 对象或复制字段。
⚠️ 踩坑提醒:如果方法没有参数,使用该 Answer 会抛出异常。
2.2 返回第二个参数:returnsSecondArg()
对于多参数方法,你可以指定返回第二个参数。例如模拟 selectRandomBook
但强制返回第二个入参:
@Test
public void givenCheckifEqualsMethodMocked_whenCheckifEqualsInvoked_ThenReturnSecondArgument_UnitTest() {
Book book1 = new Book(1L, "The Stranger", "Albert Camus", 456);
Book book2 = new Book(2L, "Animal Farm", "George Orwell", 300);
Book book3 = new Book(3L, "Romeo and Juliet", "William Shakespeare", 200);
Mockito.when(bookRepository.selectRandomBook(any(Book.class), any(Book.class),
any(Book.class))).then(AdditionalAnswers.returnsSecondArg());
Book secondBook = bookService.selectRandomBook(book1, book2, book3);
assertEquals(secondBook, book2);
}
✅ 用途:绕过随机逻辑,确保测试可预测。
2.3 返回最后一个参数:returnsLastArg()
顾名思义,返回参数列表中的最后一个参数:
@Test
public void givenCheckifEqualsMethodMocked_whenCheckifEqualsInvoked_ThenReturnLastArgument_UnitTest() {
Book book1 = new Book(1L, "The Stranger", "Albert Camus", 456);
Book book2 = new Book(2L, "Animal Farm", "George Orwell", 300);
Book book3 = new Book(3L, "Romeo and Juliet", "William Shakespeare", 200);
Mockito.when(bookRepository.selectRandomBook(any(Book.class), any(Book.class),
any(Book.class))).then(AdditionalAnswers.returnsLastArg());
Book lastBook = bookService.selectRandomBook(book1, book2, book3);
assertEquals(lastBook, book3);
}
适用于变长参数或你只关心末尾参数的场景。
2.4 返回指定索引的参数:returnsArgAt(int index)
最灵活的方式是通过索引指定返回哪个参数(从 0 开始):
@Test
public void givenCheckifEqualsMethodMocked_whenCheckifEqualsInvoked_ThenReturnArgumentAtIndex_UnitTest() {
Book book1 = new Book(1L, "The Stranger", "Albert Camus", 456);
Book book2 = new Book(2L, "Animal Farm", "George Orwell", 300);
Book book3 = new Book(3L, "Romeo and Juliet", "William Shakespeare", 200);
Mockito.when(bookRepository.selectRandomBook(any(Book.class), any(Book.class),
any(Book.class))).then(AdditionalAnswers.returnsArgAt(1));
Book bookOnIndex = bookService.selectRandomBook(book1, book2, book3);
assertEquals(bookOnIndex, book2);
}
✅ 索引为 1 → 返回第二个参数 book2
。
⚠️ 注意边界:索引越界会抛出 IndexOutOfBoundsException
。
3. 从函数式接口创建 Answer
从 Java 8 开始,Mockito 提供了更简洁的方式来通过函数式接口定义 Answer
行为。AdditionalAnswers
中的 answer()
和 answerVoid()
就是为了这个目的。
⚠️ 注意:这两个方法被标注为
@Incubating
,意味着未来 API 可能会调整,请关注版本更新。
3.1 使用 AdditionalAnswers.answer()
适用于有返回值的方法。它能自动推断泛型类型,简化 Answer
的写法。
示例:mock getByBookId
并根据传入 ID 构建返回的 Book
:
@Test
public void givenMockedMethod_whenMethodInvoked_thenReturnBook() {
Long id = 1L;
when(bookRepository.getByBookId(anyLong())).thenAnswer(answer(BookServiceUnitTest::buildBook));
assertNotNull(bookService.getByBookId(id));
assertEquals("The Stranger", bookService.getByBookId(id).getTitle());
}
private static Book buildBook(Long bookId) {
return new Book(bookId, "The Stranger", "Albert Camus", 456);
}
✅ 原理:answer()
接收一个 Answer1<Book, Long>
类型的方法引用,自动绑定到 getByBookId(Long)
方法。
对比传统写法:
when(repo.getByBookId(anyLong())).thenAnswer(invocation -> {
Long id = invocation.getArgument(0);
return new Book(id, "The Stranger", "Albert Camus", 456);
});
👉 显然 answer()
更简洁,减少样板代码。
3.2 使用 AdditionalAnswers.answerVoid()
用于无返回值(void)方法的 mock 配置。
示例:mock getByBookId
并打印传入的 ID:
@Test
public void givenMockedMethod_whenMethodInvoked_thenReturnVoid() {
Long id = 2L;
when(bookRepository.getByBookId(anyLong())).thenAnswer(answerVoid(BookServiceUnitTest::printBookId));
bookService.getByBookId(id);
verify(bookRepository, times(1)).getByBookId(id);
}
private static void printBookId(Long bookId) {
System.out.println(bookId);
}
✅ 输出:2
👉 answerVoid()
接收 VoidAnswer1<Long>
类型的函数式接口,适合执行副作用操作(如日志、计数等)。
4. 总结
AdditionalAnswers
是 Mockito 中一个低调但极其实用的工具类,尤其适合以下场景:
- ✅ 模拟
save/update
方法时返回入参对象(returnsFirstArg
) - ✅ 控制多参数方法返回特定位置的参数(
returnsArgAt
) - ✅ 减少
thenAnswer
的样板代码,使用函数式接口提升可读性(answer
/answerVoid
)
虽然部分方法处于孵化阶段(@Incubating),但在实际项目中已被广泛使用,稳定性良好。
📌 建议集合本文,下次写单元测试时可以直接套用这些技巧,简单粗暴地提升 mock 效率。
示例代码已托管至 GitHub:https://github.com/techblog/mockito-examples