1. Overview
1.概述
When writing unit tests, sometimes we’ll encounter a situation where it can be useful to return a mock when we construct a new object. For example, when testing legacy code with tightly coupled object dependencies.
在编写单元测试时,有时我们会遇到这样的情况:当我们构造一个新对象时,返回一个 mock 会很有用。例如,在测试具有紧密耦合对象依赖关系的遗留代码时。
In this tutorial, we’ll take a look at a relatively new feature of Mockito that lets us generate mocks on constructor invocations.
在本教程中,我们将了解 Mockito 的一个相对较新的功能,它可以让我们在构造函数调用时生成模拟。
To learn more about testing with Mockito, check out our comprehensive Mockito series.
要了解有关使用 Mockito 进行测试的更多信息,请查看我们的 Mockito 系列。
2. Dependencies
2.依赖关系
First, we’ll need to add the mockito dependency to our project:
首先,我们需要在项目中添加 mockito 依赖项:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
If we’re using a version of Mockito inferior to version 5, then we’ll also need to explicitly add Mockito’s mock maker inline dependency:
如果我们使用的 Mockito 版本低于版本 5,那么我们还需要显式添加 Mockito 的 mock maker 内联 依赖关系:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
3. A Quick Word on Mocking Constructor Calls
3.关于模拟构造函数调用的简要说明
Generally speaking, some might say that when writing clean object-orientated code, we shouldn’t need to return a mock instance when creating an object. This could typically hint at a design issue or code smell in our application.
一般来说,有人可能会说,在编写简洁的面向对象代码时,我们不应该在创建对象时返回模拟实例。这通常会暗示我们的应用程序中存在设计问题或代码缺陷。
Why? First, a class depending on several concrete implementations could be tightly coupled, and second, this nearly always leads to code that is difficult to test. Ideally, a class should not be responsible for obtaining its dependencies, and if possible, they should be externally injected.
为什么呢?首先,一个依赖于多个具体实现的类可能是紧密耦合的;其次,这几乎总是会导致代码难以测试。理想情况下,一个类不应该负责获取其依赖项,如果可能的话,应该从外部注入依赖项。
So, it’s always worth investigating if we can refactor our code to make it more testable. Of course, this isn’t always possible, and sometimes, we need to temporarily replace the behavior of a class after constructing it.
因此,如果我们能够重构代码,使其更具可测试性,这一点始终值得研究。当然,这并不总是可行的,有时我们需要在构建一个类后临时替换它的行为。
This might be particularly useful in several situations:
这在几种情况下可能特别有用:
- Testing difficult-to-reach scenarios – particularly if our class under test has a complex object hierarchy
- Testing interactions with external libraries or frameworks
- Working with legacy code
In the following sections, we’ll see how we can use Mockito’s MockConstruction to combat some of these situations in order to control the creation of objects and specify how they should behave when constructed.
在下面的章节中,我们将了解如何使用 Mockito 的 MockConstruction 来应对其中的一些情况,以便控制对象的创建并指定它们在构建时的行为方式。
4. Mocking Constructors
4.模拟构造函数
Let’s start by creating a simple Fruit class, which will be the focus of our first unit test:
让我们先创建一个简单的 Fruit 类,这将是我们第一个单元测试的重点:
public class Fruit {
public String getName() {
return "Apple";
}
public String getColour() {
return "Red";
}
}
Now let’s go ahead and write our test where we mock the constructor call made to our Fruit class:
现在,让我们继续编写测试,在测试中模拟调用 Fruit 类的构造函数:
@Test
void givenMockedContructor_whenFruitCreated_thenMockIsReturned() {
assertEquals("Apple", new Fruit().getName());
assertEquals("Red", new Fruit().getColour());
try (MockedConstruction<Fruit> mock = mockConstruction(Fruit.class)) {
Fruit fruit = new Fruit();
when(fruit.getName()).thenReturn("Banana");
when(fruit.getColour()).thenReturn("Yellow");
assertEquals("Banana", fruit.getName());
assertEquals("Yellow", fruit.getColour());
List<Fruit> constructed = mock.constructed();
assertEquals(1, constructed.size());
}
}
In our example, we start by checking that a real Fruit object returns the desired values.
在我们的示例中,我们首先要检查一个真实的 Fruit 对象是否会返回所需的值。
Now, to make mocking object constructions possible, we’ll use the Mockito.mockConstruction() method. This method takes a non-abstract Java class for the constructions we’re about to mock. In this case, a Fruit class.
现在,为了模拟对象构造,我们将使用 Mockito.mockConstruction() 方法。该方法使用一个非抽象 Java 类来表示我们将要模拟的构造。在本例中,它是一个 Fruit 类。
We define this within a try-with-resources block. This means that when our code calls the constructor of a Fruit object inside the try statement, it returns a mock object. We should note that the constructor won’t be mocked by Mockito outside our scoped block.
我们在try-with-resources块中定义了这一点。这意味着,当我们的代码在 try 语句中调用 Fruit 对象的构造函数时,它将返回一个 mock 对象。我们需要注意的是,在我们的作用域代码块之外,构造函数不会被 Mockito 模拟。
This is a particularly nice feature since it ensures that our mock remains temporary. As we know, if we’re playing around with mock constructor calls during our test runs, this could likely lead to adverse effects on our test results due to the concurrent and sequential nature of running tests.
这是一个特别好的功能,因为它能确保我们的 mock 保持临时性。我们知道,如果我们在测试运行过程中随意调用 mock 构造函数,由于测试运行的并发性和顺序性,这很可能会对测试结果产生不利影响。
5. Mocking Constructors Inside Another Class
5.在另一个类中模拟构造函数
A more realistic scenario is when we have a class under test, which creates some objects inside that we would like to mock.
更现实的情况是,我们有一个正在测试的类,该类内部创建了一些我们想要模拟的对象。
Typically, inside the constructor of our class under test, we might create instances of new objects that we would like to mock from our tests. In this example, we’ll see how we can do this.
通常情况下,我们会在被测类的构造函数中创建新对象的实例,以便在测试中模拟这些对象。在本例中,我们将了解如何做到这一点。
Let’s start by defining a simple coffee-making application:
让我们先来定义一个简单的咖啡制作应用程序:
public class CoffeeMachine {
private Grinder grinder;
private WaterTank tank;
public CoffeeMachine() {
this.grinder = new Grinder();
this.tank = new WaterTank();
}
public String makeCoffee() {
String type = this.tank.isEspresso() ? "Espresso" : "Americano";
return String.format("Finished making a delicious %s made with %s beans", type, this.grinder.getBeans());
}
}
Next, we define the Grinder class:
接下来,我们定义 Grinder 类:
public class Grinder {
private String beans;
public Grinder() {
this.beans = "Guatemalan";
}
public String getBeans() {
return beans;
}
public void setBeans(String beans) {
this.beans = beans;
}
}
Finally, we add the WaterTank class:
最后,我们添加 WaterTank 类:
public class WaterTank {
private int mils;
public WaterTank() {
this.mils = 25;
}
public boolean isEspresso() {
return getMils() < 50;
}
//Getters and Setters
}
In this trivial example, our CoffeeMachine creates a grinder and tank at construction time. We have one method, makeCoffee(), which prints out a message about the brewed coffee.
在这个微不足道的示例中,我们的 CoffeeMachine 会在构建时创建磨Bean机和咖啡罐。我们有一个方法,makeCoffee() ,该方法会打印出关于冲泡咖啡的信息。
Now, we can go ahead and write a couple of tests:
现在,我们可以继续编写几个测试:
@Test
void givenNoMockedContructor_whenCoffeeMade_thenRealDependencyReturned() {
CoffeeMachine machine = new CoffeeMachine();
assertEquals("Finished making a delicious Espresso made with Guatemalan beans", machine.makeCoffee());
}
In this first test, we’re checking that when we don’t use MockedConstruction, our coffee machine returns real dependencies inside.
在第一个测试中,我们将检查当我们不使用 MockedConstruction 时,我们的咖啡机是否会返回内部的真实依赖关系。
Now let’s see how we can return mocks for those dependencies:
现在让我们看看如何返回这些依赖项的模拟:
@Test
void givenMockedContructor_whenCoffeeMade_thenMockDependencyReturned() {
try (MockedConstruction<WaterTank> mockTank = mockConstruction(WaterTank.class);
MockedConstruction<Grinder> mockGrinder = mockConstruction(Grinder.class)) {
CoffeeMachine machine = new CoffeeMachine();
WaterTank tank = mockTank.constructed().get(0);
Grinder grinder = mockGrinder.constructed().get(0);
when(tank.isEspresso()).thenReturn(false);
when(grinder.getBeans()).thenReturn("Peruvian");
assertEquals("Finished making a delicious Americano made with Peruvian beans", machine.makeCoffee());
}
}
In this test, we use mockConstruction to return mocks instances when we call the constructors of Grinder and WaterTank. Then, we specify the expectations of these mocks using standard when notation.
在此测试中,当我们调用 Grinder 和 WaterTank 的构造函数时,我们使用 mockConstruction 返回模拟实例。然后,我们使用标准的 when 符号指定这些模拟的期望值。
This time around, when we run our test, Mockito ensures that the constructors of Grinder and WaterTank return the mocked instances with the specified behavior, allowing us to test the makeCoffee method in isolation.
这一次,当我们运行测试时,Mockito 会确保 Grinder 和 WaterTank 的构造函数返回具有指定行为的模拟实例,从而允许我们隔离测试 makeCoffee 方法。
6. Dealing with Constructor Arguments
6.处理构造函数参数
Another common use case is to be able to deal with a constructor which takes an argument.
另一个常见的用例是能够处理需要一个参数的构造函数。
Thankfully, mockedConstruction provides a mechanism allowing us to access the arguments passed to the constructor:
值得庆幸的是,mockedConstruction 提供了一种机制,允许我们访问传递给构造函数的参数:
Let’s add a new constructor to our WaterTank:
让我们为 WaterTank 添加一个新的构造函数:
public WaterTank(int mils) {
this.mils = mils;
}
Likewise, let’s also add a new constructor to our Coffee application:
同样,让我们为 Coffee 应用程序添加一个新的构造函数:
public CoffeeMachine(int mils) {
this.grinder = new Grinder();
this.tank = new WaterTank(mils);
}
Finally, we can add another test:
最后,我们可以再增加一个测试:
@Test
void givenMockedContructorWithArgument_whenCoffeeMade_thenMockDependencyReturned() {
try (MockedConstruction<WaterTank> mockTank = mockConstruction(WaterTank.class,
(mock, context) -> {
int mils = (int) context.arguments().get(0);
when(mock.getMils()).thenReturn(mils);
});
MockedConstruction<Grinder> mockGrinder = mockConstruction(Grinder.class)) {
CoffeeMachine machine = new CoffeeMachine(100);
Grinder grinder = mockGrinder.constructed().get(0);
when(grinder.getBeans()).thenReturn("Kenyan");
assertEquals("Finished making a delicious Americano made with Kenyan beans", machine.makeCoffee());
}
}
This time around, we use a lambda expression to handle the WaterTank constructor with arguments. The lambda receives the mock instance and the construction context, allowing us to access the arguments passed to the constructor.
这次,我们使用 lambda 表达式来处理带有参数的 WaterTank 构造函数。lambda 接收 mock 实例和构造上下文,允许我们访问传递给构造函数的参数。
We can then use these arguments to set up the desired behavior for the getMils method.
然后,我们可以使用这些参数为 getMils 方法设置所需的行为。
7. Changing the Default Mocking Behaviour
7.更改默认模拟行为
It’s important to note that for methods, we don’t stub a mock return null by default. We can take our Fruit example one step further and let the mock behave like a real Fruit instance:
值得注意的是,对于方法而言,我们默认不会将 mock 存根返回为空。我们可以将 Fruit 的示例向前推进一步,让 mock 的行为与真正的 Fruit 实例一样:
@Test
void givenMockedContructorWithNewDefaultAnswer_whenFruitCreated_thenRealMethodInvoked() {
try (MockedConstruction<Fruit> mock = mockConstruction(Fruit.class, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS))) {
Fruit fruit = new Fruit();
assertEquals("Apple", fruit.getName());
assertEquals("Red", fruit.getColour());
}
}
This time, we pass an extra parameter MockSettings to the mockConstruction method to tell it to create a mock that will behave like a real Fruit instance for methods that we didn’t stub.
这次,我们向 mockConstruction 方法传递了一个额外的参数 MockSettings 来告诉它创建一个 mock,该 mock 将像真正的 Fruit 实例一样,用于我们没有存根的方法。
8. Conclusion
8.结论
In this quick article, we’ve seen a couple of examples of how we can use Mockito to mock constructor calls. To summarise, Mockito provides a graceful solution to generate mocks on constructor invocations within the current thread and a user-defined scope.
在这篇短文中,我们看到了几个如何使用 Mockito 来模拟构造函数调用的示例。总而言之,Mockito 提供了一种优雅的解决方案,可以在当前线程和用户定义的范围内生成构造函数调用模拟。
As always, the full source code of the article is available over on GitHub.
一如既往,本文的完整源代码可在 GitHub 上获取。