1. 概述

本文将深入介绍 Mockito 提供的 AdditionalAnswers 类及其核心方法。这个类在编写单元测试时非常实用,尤其当你需要让 mock 方法返回其参数本身,而不是硬编码的返回值时。

简单来说,AdditionalAnswers 提供了一组开箱即用的 Answer 实现,让你可以灵活地控制 mock 方法的返回行为——比如直接返回第几个参数,或者基于函数式接口构建更复杂的响应逻辑。

✅ 适用场景:

  • 模拟类似 save(entity) 这种“传入什么就返回什么”的方法
  • 需要根据参数动态决定返回值
  • 简化 thenAnswer() 中的冗余代码

下面我们通过实际代码来演示它的用法。


2. 返回方法参数

AdditionalAnswers 最常见的用途就是让 mock 方法返回其调用时传入的某个参数。这在模拟 DAO 或 Repository 层的 saveupdate 方法时特别有用。

示例模型与依赖类

我们先定义一个简单的 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


原始标题:Introduction to Mockito’s AdditionalAnswers