Mockito’s Java 8 Features – Mockito’的Java 8功能

最后修改: 2017年 3月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Java 8 introduced a range of new, awesome features, like lambda and streams. And naturally, Mockito leveraged these recent innovations in its 2nd major version.

Java 8引入了一系列新的、令人敬畏的功能,如lambda和流。当然,Mockito也在其第二大版本中利用了这些最新的创新。

In this article, we are going to explore everything this powerful combination has to offer.

在这篇文章中,我们将探讨这一强大组合所能提供的一切。

2. Mocking Interface With a Default Method

2.用默认方法嘲弄接口

From Java 8 onwards we can now write method implementations in our interfaces. This might be a great new functionality, but its introduction to the language violated a strong concept that was part of Java since its conception.

从Java 8开始,我们现在可以在我们的接口中编写方法实现。这可能是一个很好的新功能,但它的引入违反了一个强大的概念,这个概念从Java的概念开始就是它的一部分。

Mockito version 1 was not ready for this change. Basically, because it didn’t allow us to ask it to call real methods from interfaces.

Mockito第1版还没有准备好应对这种变化。基本上,因为它不允许我们要求它从接口中调用真正的方法。

Imagine that we have an interface with 2 method declarations: the first one is the old-fashioned method signature we’re all used to, and the other is a brand new default method:

想象一下,我们有一个带有2个方法声明的接口:第一个是我们都习惯了的老式方法签名,另一个是一个全新的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;
        }
    }
}

Notice that the assignJobPosition() default method has a call to the unimplemented findCurrentJobPosition() method.

请注意,assignJobPosition() default方法有一个对未实现的findCurrentJobPosition() 方法的调用。

Now, suppose we want to test our implementation of assignJobPosition() without writing an actual implementation of findCurrentJobPosition(). We could simply create a mocked version of JobService, then tell Mockito to return a known value from the call to our unimplemented method and call the real method when assignJobPosition() is called:

现在,假设我们想测试我们对assignJobPosition()的实现,而无需编写findCurrentJobPosition()的实际实现。我们可以简单地创建一个模拟版本的JobService,然后告诉Mockito从调用我们未实现的方法中返回一个已知值,并在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()));
    }
}

This is perfectly reasonable and it would work just fine given we were using an abstract class instead of an interface.

这是完全合理的,如果我们使用的是一个抽象类而不是一个接口,那么它就可以很好地工作。

However, the inner workings of Mockito version 1 were just not ready for this structure. If we were to run this code with Mockito pre version 2 we would get this nicely described error:

然而,Mockito第1版的内部运作还没有为这种结构做好准备。如果我们用Mockito第2版之前的版本运行这段代码,我们会得到这个很好描述的错误。

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.

Mockito is doing its job and telling us it can’t call real methods on interfaces since this operation was unthinkable before Java 8.

Mockito正在做它的工作,告诉我们它不能调用接口上的真实方法,因为这种操作在Java 8之前是不可想象的。

The good news is that just by changing the version of Mockito we’re using we can make this error go away. Using Maven, for example, we could use version 2.7.5 (the latest Mockito version can be found here):

好消息是,只要改变我们所使用的Mockito的版本,就可以让这个错误消失。例如,使用Maven,我们可以使用2.7.5版本(最新的Mockito版本可以在这里找到)。

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

There is no need to make any changes to the code. The next time we run our test, the error will no longer occur.

不需要对代码做任何修改。下次我们运行我们的测试时,错误将不再发生。

3. Return Default Values for Optional and Stream

3.返回可选的默认值

Optional and Stream are other Java 8 new additions. One similarity between the two classes is that both have a special type of value that represent an empty object. This empty object makes it easier to avoid the so far omnipresent NullPointerException.

OptionalStream是其他Java 8新增加的功能。这两个类的一个相似之处是,都有一个特殊类型的值,代表一个空对象。这个空对象使我们更容易避免迄今为止无处不在的NullPointerException.

3.1. Example With Optional

3.1.有选项的例子

Consider a service that injects the JobService described in the previous section and has a method that calls JobService#findCurrentJobPosition():

考虑一个服务,它注入了上一节所述的JobService,并有一个方法调用JobService#findCurrentJobPosition()

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();
    }
}

Now, assume we want to create a test to check that, when a person has no current job position, they are entitled to the unemployment support.

现在,假设我们想创建一个测试来检查,当一个人目前没有工作岗位时,他们是否有权获得失业支持。

In that case, we would force findCurrentJobPosition() to return an empty Optional. Before Mockito version 2, we were required to mock the call to that method:

在这种情况下,我们将强制findCurrentJobPosition() 返回一个空的Optional在Mockito第2版之前,我们需要对该方法的调用进行模拟。

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));
    }
}

This when(…).thenReturn(…) instruction on line 13 is necessary because Mockito’s default return value for any method calls to a mocked object is null. Version 2 changed that behavior.

第13行的when(…).thenReturn(…)指令是必要的,因为Mockito对被模拟对象的任何方法调用的默认返回值是null。第二版改变了这一行为。

Since we rarely handle null values when dealing with Optional, Mockito now returns an empty Optional by default. That is the exact same value as the return of a call to Optional.empty().

由于我们在处理Optional时很少处理空值,Mockito现在默认返回一个空的Optional 。这与调用Optional.empty()的返回值完全相同。

So, when using Mockito version 2, we could get rid of line 13 and our test would still be successful:

因此,当使用Mockito第2版时,我们可以去掉第13行,我们的测试仍然会成功。

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

3.2. Example With Stream

3.2.使用Stream的例子

The same behavior occurs when we mock a method that returns a Stream.

当我们模拟一个返回Stream的方法时,也会发生同样的行为。

Let’s add a new method to our JobService interface that returns a Stream representing all the job positions that a person has ever worked at:

让我们为我们的JobService接口添加一个新方法,返回一个代表一个人曾经工作过的所有工作岗位的Stream。

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

This method is used on another new method that will query if a person has ever worked on a job that matches a given search string:

这个方法用在另一个新的方法上,这个方法将查询一个人是否曾经从事过与给定搜索字符串相匹配的工作。

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();
    }
}

So, assume we want to properly test the implementation of searchJob(), without having to worry about writing the listJobs() and assume we want to test the scenario when the person hasn’t work at any jobs yet. In that case, we would want listJobs() to return an empty Stream.

所以,假设我们想正确地测试searchJob()的实现,而不必担心编写listJobs(),并假设我们想测试这个人还没有工作的情况。在这种情况下,我们希望listJobs()能返回一个空的Stream

Before Mockito version 2, we would need to mock the call to listJobs() to write such test:

在Mockito第2版之前,我们需要模拟对listJobs()的调用来编写此类测试。

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());
    }
}

If we upgrade to version 2, we could drop the when(…).thenReturn(…) call, because now Mockito will return an empty Stream on mocked methods by default:

如果我们升级到版本2,我们可以放弃when(…).thenReturn(…)调用,因为现在Mockito默认会在模拟方法上返回一个空的Stream

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

4. Leveraging Lambda Expressions

4.利用Lambda表达式

With Java 8’s lambda expressions we can make statements much more compact and easier to read. When working with Mockito, 2 very nice examples of the simplicity brought in by lambda expressions are ArgumentMatchers and custom Answers.

通过Java 8的lambda表达式,我们可以使语句更紧凑,更容易阅读。在使用Mockito时,羔羊表达式带来的两个非常好的例子是ArgumentMatchers和自定义Answers

4.1. Combination of Lambda and ArgumentMatcher

4.1.Lambda和ArgumentMatcher的组合

Before Java 8, we needed to create a class that implemented ArgumentMatcher, and write our custom rule in the matches() method.

在Java 8之前,我们需要创建一个实现ArgumentMatcher的类,并在matches()方法中编写我们的自定义规则。

With Java 8, we can replace the inner class with a simple lambda expression:

在Java 8中,我们可以用一个简单的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. Combination of Lambda and Custom Answer

4.2.Lambda和自定义答案的结合

The same effect can be achieved when combining lambda expressions with Mockito’s Answer.

将lambda表达式与Mockito的Answer相结合,也能达到同样的效果。

For example, if we wanted to simulate calls to the listJobs() method in order to make it return a Stream containing a single JobPosition if the Person‘s name is “Peter”, and an empty Stream otherwise, we would have to create a class (anonymous or inner) that implemented the Answer interface.

例如,如果我们想模拟对listJobs()方法的调用,以使它在Person的名字是 “Peter “的情况下返回一个包含单个JobPositionStream,否则返回一个空Stream,我们将不得不创建一个实现Answer接口的类(匿名或内部)。

Again, the use of a lambda expression, allow us to write all the mock behavior inline:

同样,使用lambda表达式,允许我们将所有的模拟行为写入内联。

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")));
    }
}

Notice that, in the implementation above, there is no need for the PersonAnswer inner class.

注意,在上面的实现中,不需要PersonAnswer内类。

5. Conclusion

5.结论

In this article, we covered how to leverage new Java 8 and Mockito version 2 features together to write cleaner, simpler and shorter code. If you are not familiar with some of the Java 8 features we saw here, check some of our articles:

在这篇文章中,我们介绍了如何利用Java 8和Mockito第2版的新特性来编写更干净、更简单、更短的代码。如果你不熟悉我们在这里看到的一些Java 8特性,请查看我们的一些文章。

Also, check the accompanying code on our GitHub repository.

另外,请在我们的GitHub资源库上查看配套代码。