2. 接口默认方法的模拟

Java 8 开始允许在接口中定义默认方法实现。这个新特性虽然强大,却打破了 Java 早期设计的核心概念。Mockito 1.x 版本对此毫无准备——它根本不支持调用接口中的真实方法。

假设我们有一个包含两个方法的接口:一个是传统方法签名,另一个是全新的 default 方法:

public interface JobService {
 
    Optional<JobPosition> findCurrentJobPosition(Person person);
    
    default boolean assignJobPosition(Person person, JobPosition jobPosition) {
        if(!findCurrentJobPosition(person).isPresent()) {
            person.setCurrentJobPosition(jobPosition);
            return true;
        } else {
            return false;
        }
    }
}

注意 assignJobPosition() 这个默认方法调用了未实现的 findCurrentJobPosition() 方法。

现在我们想测试 assignJobPosition() 的逻辑,但又不想实现 findCurrentJobPosition()。我们可以这样操作:

  1. 创建 JobService 的 mock 对象
  2. findCurrentJobPosition() 返回预设值
  3. 调用真实的 assignJobPosition() 方法
public class JobServiceUnitTest {
 
    @Mock
    private JobService jobService;

    @Test
    public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(person))
              .thenReturn(Optional.of(new JobPosition()));

        doCallRealMethod().when(jobService)
          .assignJobPosition(
            Mockito.any(Person.class), 
            Mockito.any(JobPosition.class)
          );

        assertFalse(jobService.assignJobPosition(person, new JobPosition()));
    }
}

✅ 这段代码在抽象类上完全没问题,但在接口上直接翻车。Mockito 1.x 会这样报错:

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

⚠️ 原因很简单:Java 8 之前,接口不可能有实现方法!Mockito 1.x 的设计基于这个前提。

解决方案:升级到 Mockito 2.x(比如 2.7.5):

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.5</version>
    <scope>test</scope>
</dependency>

升级后无需修改任何代码,测试直接通过。这就是版本升级带来的红利。

3. Optional 和 Stream 的默认返回值

OptionalStream 是 Java 8 的两大神器。它们都有特殊的"空值"表示(Optional.empty()Stream.empty()),能大幅减少 NullPointerException

3.1. Optional 示例

看这个服务类,它依赖前面定义的 JobService

public class UnemploymentServiceImpl implements UnemploymentService {
 
    private JobService jobService;
    
    public UnemploymentServiceImpl(JobService jobService) {
        this.jobService = jobService;
    }

    @Override
    public boolean personIsEntitledToUnemploymentSupport(Person person) {
        Optional<JobPosition> optional = jobService.findCurrentJobPosition(person);
        return !optional.isPresent();
    }
}

现在测试"无工作可领失业金"的场景。在 Mockito 1.x 中,我们必须手动 mock 返回空 Optional

public class UnemploymentServiceImplUnitTest {
 
    @Mock
    private JobService jobService;

    @InjectMocks
    private UnemploymentServiceImpl unemploymentService;

    @Test
    public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(any(Person.class)))
          .thenReturn(Optional.empty());
        
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

👉 Mockito 1.x 默认返回 null,而 Optional 最怕 null

Mockito 2.x 的改进:默认返回空 Optional(相当于 Optional.empty())。所以测试可以简化为:

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();
 
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

3.2. Stream 示例

JobService 添加一个返回 Stream 的方法:

public interface JobService {
    Stream<JobPosition> listJobs(Person person);
}

在服务类中实现职位搜索功能:

public class UnemploymentServiceImpl implements UnemploymentService {
   
    @Override
    public Optional<JobPosition> searchJob(Person person, String searchString) {
        return jobService.listJobs(person)
          .filter((j) -> j.getTitle().contains(searchString))
          .findFirst();
    }
}

测试"无工作经历"场景时,Mockito 1.x 需要手动 mock:

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
        Person person = new Person();
        when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());
        
        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

Mockito 2.x 的改进:默认返回空 Stream(相当于 Stream.empty())。测试代码进一步简化:

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();
        
        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

4. Lambda 表达式的高阶玩法

Java 8 的 Lambda 让代码更紧凑。在 Mockito 中,结合 ArgumentMatcher 和自定义 Answer 使用效果拔群。

4.1. Lambda + ArgumentMatcher

Java 8 之前,需要创建实现 ArgumentMatcher 的类。现在用 Lambda 一行搞定:

public class ArgumentMatcherWithLambdaUnitTest {
 
    @Test
    public void whenPersonWithJob_thenIsNotEntitled() {
        Person peter = new Person("Peter");
        Person linda = new Person("Linda");
        
        JobPosition teacher = new JobPosition("Teacher");

        when(jobService.findCurrentJobPosition(
          ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
          .thenReturn(Optional.of(teacher));
        
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
        assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
    }
}

4.2. Lambda + 自定义 Answer

同样,自定义 Answer 也能用 Lambda 简化。比如让 listJobs() 根据人名返回不同结果:

public class CustomAnswerWithLambdaUnitTest {
 
    @Before
    public void init() {
        when(jobService.listJobs(any(Person.class))).then((i) ->
          Stream.of(new JobPosition("Teacher"))
          .filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
    }
}

👉 完全不需要定义 PersonAnswer 内部类,逻辑直接内联!

5. 总结

Mockito 2.x 对 Java 8 特性的支持让测试代码更简洁:

  • 接口默认方法可直接调用真实实现
  • Optional/Stream 默认返回空值而非 null
  • Lambda 表达式大幅简化参数匹配和自定义逻辑

这些改进不仅减少了样板代码,还让测试意图更清晰。如果你还在用 Mockito 1.x,升级吧——这波升级绝对不亏!

延伸阅读

本文代码已上传至 GitHub 仓库


原始标题:Mockito's Java 8 Features

« 上一篇: Guava 反射工具类