当前位置:   article > 正文

Springboot中JUNIT5单元测试+Mockito详解_mockito+junit5+springboot

mockito+junit5+springboot

目录


友情提醒:部分代码手写,部分代码为Ai生成,基本不可能侵权,雷同纯属巧合
先看文章目录,大致了解文章知识点结构,点击文章目录可直接跳转到文章指定位置。

第一章、快速了解JUnit单元测试

1.1)JUnit单元测试和集成测试的区别

①JUnit是什么:
一种帮助我们编写测试的标准测试框架。

②JUnit单元测试是什么:
针对方法(最小功能单元)的测试,针对代码中的单个模块或函数进行测试验证这些模块或函数的行为是否符合预期,单元测试通常在一个隔离的环境中运行,不依赖外部资源,例如数据库、网络等,以确保测试结果的稳定性和可重复性。由程序员自己写。

③JUnit集成测试是什么:
会加载Spring上下文,同时用于验证多个模块或组件之间的交互和集成后的行为。集成测试可能涉及到数据库、文件系统、网络等外部资源,以确保整个系统的各个部分能够协同工作,达到预期的功能和性能要求。

1.2)使用JUnit测试框架的原因

①在主方法中调用测试
不使用单元测试时,我们测试一个方法能否成功运行,我们会在主方法里调用这个方法,看看控制台的运行结果。

public class Test {
    public static void main(String[] args) {
        System.out.println(Test(1));
    }
    public static int Test(int i){
        return ++i;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

②直接将代码写在主方法
或者更直接的把需要测试代码放到主方法里,看能不能运行成功。

public class Test {
    public static void main(String[] args) {
            System.out.println("test");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

以上两种的局限性:
1、只有一个main方法无法有组织有层次的测试不同的方法。
2、测试的输出结果不够直观。
所以我们需要专门的JUnit来编写单元测试避免这些局限性。

第二章、Springboot中JUnit5框架示例

IDE工具为IDEA

2.1)在pom文件中导入依赖

SpringBoot2.2x之后的版本中spring-boot-starter-test包中,默认集成了JUnit5依赖包,Mockito: Java Mock框架依赖,AssertJ流式断言依赖包等,所以正常来说我们不需要再导入关于单元测试的依赖包了。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

版本关系如图:对静态类进行mock的时候:mockito-core的版本要在3.4以上。

在这里插入图片描述

2.2)新建测试类

①查看扫描路径是否正确

如果不正确需要手动添加:File-project structure–>moudles–>选中想要的模块–>Sources–>test下的java文件夹右击设置为tests

在这里插入图片描述

②快速生成测试类
选中需要测试的service包中类的类名,右键选择Generate

在这里插入图片描述

选择Test

在这里插入图片描述

③勾选以下配置选项:
1、选择JUnit5。
2、测试类的命名规范为BidInfoServiceImpl类名后面加Test也就是BidInfoServiceImplTest。3、我们需要在test包下建立相同的路径
4、勾选setUp/@Before
5、勾选需要测试的方法
6、选择OK

在这里插入图片描述

④查看test下的路径
已经替我们生成了对应的测试类了

在这里插入图片描述

2.3)新建测试方法

①测试BidInfoServiceImpl类中的fact方法

public class BidInfoServiceImpl implements BidInfoService {
 public int fact(int i){
        int a=++i;
        return a;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

②添加注解
@ExtendWith注解在刚刚新建的BidInfoServiceImplTest测试类上面@InjectMock注释在要测试的实现类上,
@Test注释在测试的方法上


// 在junit4的时候使用@Runwith注解,在junit5的时候使用的是@ExtendWith(MockitoExtension.class)注解

@ExtendWith(MockitoExtension.class)
//@RunWith(MockitoJUnitRunner.class)
public class BidInfoServiceImplTest {
    @InjectMocks
    BidInfoServiceImpl bidInfoServiceImpl;

    @Test
    public void testFact(){
//第一个参数是预期结果:2。第二个参数是需要测试的方法,传入值1
        assertEquals(2,bidInfoServiceImpl.fact(1));
        assertEquals(4,bidInfoServiceImpl.fact(3));
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

③运行测试方法
右键红框处,点击RUN运行,assertEquals()方法中第一个参数是预期结果2。第二个参数是需要测试的方法,传入参数1
在这里插入图片描述
④测试通过(即结果与预期相符)
出现如下提示:
在这里插入图片描述

第三章、JUnit框架提供的注解

3.1)注释在类上的注解

3.1.1)JUnit5注释在类上的注解

集成测试:@SpringBootTest

用于加载整个应用程序上下文的注解

@SpringBootTest注解:

用于指定Spring Boot应用程序的集成测试。
会加载完整的Spring应用程序上下文,并提供Mock Web Environment,包括所有的bean和配置。
通常用于在JUnit 5中进行Spring Boot应用程序的集成测试。
  • 1
  • 2
  • 3
  • 4
  • 5

如果测试时不启动Spring上下文可以进行设置:

//@SpringBootTest(webEnvironment = WebEnvironment.NONE)
//禁用Web环境可以将集成测试转换为更接近于单元测试的测试,而不是测试整个Web应用程序。
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class MyIntegrationTest {

    @Test
    public void testSomething() {
        // Your test code here
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
集成测试:@ExtendWith(SpringExtension.class)

@ExtendWith(SpringExtension.class)和@SpringBootTest注解结合使用的效果是在JUnit 5测试中启用Spring的测试框架特性,并且会启动完整的Spring应用程序上下文。

如果你只需要启用Spring支持而不需要加载整个应用程序上下文,可以只单独使用@ExtendWith(SpringExtension.class)。而不使用@SpringBootTest注解,这意味着测试中可以使用Spring的依赖注入、自动装配和其他Spring特性,但无法访问完整的Spring Bean和配置。

单元测试:ExtendWith(MockitoExtension.class)

希望在JUnit 5中使用Mockito框架进行单元测试时可以使用@ExtendWith(MockitoExtension.class)来确保正确的测试环境和行为。以便初始化模拟对象并处理严格的存根。这个扩展类类似于JUnit 4中的MockitoJUnitRunner,它可以在测试类中自动初始化模拟对象,并处理严格的存根。

切片测试:@WebMvcTest和@DataJpaTest

@WebMvcTest用于对Spring MVC控制器进行测试,它会限制测试范围,只会加载与Spring MVC相关的组件,例如控制器、拦截器等,而不会加载整个应用程序上下文。这样可以加快测试速度,并且可以专注于对Web层的测试。


@ExtendWith(SpringExtension.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testUserController() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/users"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

@DataJpaTest用于对JPA持久化层进行测试,它会限制测试范围,只会加载与JPA相关的组件,例如实体类、仓储接口等,而不会加载整个应用程序上下文。这样可以加快测试速度,并且可以专注于对持久化层的测试。

@ExtendWith(SpringExtension.class)
@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    public void whenFindByName_thenReturnUser() {
        // 测试代码
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
手动添加bean到测试上下文:@TestConfiguration

作用是告诉Spring框架,在测试环境中,这个配置类中定义的bean应该覆盖主应用程序中相同名称的bean,或者提供测试专用的bean定义。

在测试中,定义了一个@TestConfiguration类,通常情况下,它可以放在与被测试类相同的包结构下用于提供测试专用的bean定义。接着通过@Autowired注解将TestConfig配置类引入测试上下文,从而在测试环境中使用其中定义的bean。

    @TestConfiguration
    static class TestConfig {
        @Bean
        public MyService myService() {
            return new MyService();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过@Autowired注解将TestConfig配置类引入测试上下文

@SpringBootTest
public class IntegrationTestExample {

    @Autowired
    private MyService myService;

    @Test
    public void testIntegration() {
        String result = myService.doSomething();
        assertEquals("expectedResult", result);
    }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.1.2)JUnit4注释在类上的注解(简单介绍)

JUnit4集成测试:@RunWith(SpringJUnit4ClassRunner.class)

注意:在JUnit 5中,不再使用@RunWith注解,而是使用更强大的@ExtendWith注解用于扩展测试的行为

@RunWith注解:

用于指定JUnit 4测试类的运行器。
可以通过@RunWith注解指定不同的测试运行器,
例如Spring提供的SpringJUnit4ClassRunner
通常用于在JUnit 4中加载Spring上下文进行集成测试。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
JUnit4单元测试:@RunWith(MockitoJUnitRunner.class)

注意:告诉JUnit在运行测试时使用Mockito运行器,以便正确处理Mockito的注解和行为,通常用于单元测试中,以便对单个组件进行测试而不涉及外部依赖。

@RunWith(MockitoJUnitRunner.class)
public class YourTestClass {

    @Test
    public void yourTestMethod() {
        // Your test method code here
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2)注释在成员变量上的注解

3.2.1)模拟依赖注入

@Mock

@Mock:用于模拟依赖项,模拟Spring应用程序上下文中的bean。它会创建一个模拟对象,用于替代应用程序上下文中的真实bean。(mock后真实的方法不再调用)不会执行对象的方法(即使方法报错,test只会使用你设置的返回值,不影响流程)

但用Mockito.when(service.方法名(参数)).thenCallRealMethod();还是可以调真实的方法

@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
//模拟注入的依赖项
    @Mock
    private MyDependency myDependency;
    @Test
    public void testQueryUserCount(){
		//待测试的代码。。。
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
@MockBean

注解依赖项时,@MockBean注解和@Mock注解的效果是一样的。它们都可以用于模拟依赖项,以便进行单元测试。
@MockBean注解用于模拟Spring应用程序上下文中的bean,并将模拟对象注入到Spring容器中。通常用于模拟service层或repository层的依赖项

@SpringBootTest
public class MyServiceTest {
//MyDependency被用@MockBean注解模拟了,
//可以在不启动Spring Boot应用程序的情况下进行方法的测试。
//也可以使用@Mock注解
    @MockBean
    private MyDependency myDependency;
    @Test
    public void testMyService() {
        // 测试代码
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
@Spy

@Spy:用于模拟依赖项,创建一个真实对象的部分模拟。这意味着对象的实际实现将被保留,但您可以选择模拟特定的方法或行为。即会真实的执行对象的方法(如果方法报错,test直接报错)

但用Mockito.doReturn(“不执行此方法”).when(service).方法名(参数);还是可以不调用真实的方法

public class MyServiceTest {
    @Spy
    private MyDependency myDependency;
    @Test
    public void testSomething() {
        // 在这里使用 myDependency 进行测试
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2.2)注入真实的bean

@Autowired

用于将实际的Spring bean注入到被测试的类中,以便进行真实的依赖注入。这意味着@Autowired会注入实际的依赖项,而不是模拟对象。

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @Test
    public void testSomething() {
        // 使用myService进行单元测试
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3.2.3)注入被测试类的实例对象:@InjectMocks

@InjectMocks

用于创建被测试类的实例,注释的是要测试的实现类并注入模拟对象作为其依赖项。通常用于创建被测试类的实例,并将模拟的依赖项注入到被测试类中,以进行单元测试。

@RunWith(MockitoJUnitRunner.class)
public class UserServiceImplTest {
//需要测试的是UserServiceImpl类,使用 @InjectMocks模拟创建实例
    @InjectMocks
    UserServiceImpl userService;
//模拟注入的依赖项
    @Mock
    UserMapper userMapper;

    @Test
    public void testQueryUserCount(){
		//待测试的方法
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
避免@Autowired和@InjectMock同时使用

1、在test之前的@BeforeEach注解方法中执行MockitoAnnotations.openMocks(this)
以在每个测试方法之前的初始化操作,包括对Mock对象的初始化。
2、测试的时候要用this.serviceimpl.方法名()测试 否则mock还会使用真实的方法

public class YourTestClass {

    @InjectMocks
    private YourServiceImpl serviceImpl;
    @BeforeEach
    public void init() {
        MockitoAnnotations.openMocks(this);
    }
    // 测试方法
    @Test
    public void testYourServiceMethod() {
        // 调用被测方法
        this.serviceImpl.yourServiceMethod();
        // 断言和验证
        // ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.3)注释在方法上的注解

3.3.1)标记测试方法

@Test:标记测试方法

用于标识单元测试方法。JUnit将会执行被@Test注解标记的方法,并对它们进行断言和验证。JUnit 5中的@Test注解使得编写和执行单元测试变得非常简单和直观。

@ParameterizedTest:参数化测试

通过使用 @ParameterizedTest 注解,可以轻松地在单个测试方法中执行多组输入和预期输出的测试。

@ParameterizedTest 注解标记了 testAddition 方法,该方法使用 @CsvSource 提供了多组输入参数。在这个例子中,@CsvSource 提供了三组输入参数,每组参数包括两个加数和预期的结果。在测试方法中,使用提供的输入参数进行计算,并使用断言来验证计算结果是否符合预期。

public class ParameterizedTestExample {

    @ParameterizedTest
    @CsvSource({ "1, 1, 2", "2, 3, 5", "5, 5, 10" })
    void testAddition(int a, int b, int expectedResult) {
        Calculator calculator = new Calculator();
        int result = calculator.add(a, b);
//expectedResult期望结果,result实际的计算结果,Lambda表达式,用于生成断言失败时的错误消息。
assertEquals(expectedResult, result, () -> a + " + " + b + " should equal " + expectedResult);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
@RepeatedTest:多次重复进行测试

@RepeatedTest 注解用于指定重复测试

public class RepeatedTestExample {
//下面的测试方法将被重复执行五次
    @RepeatedTest(5)
    void repeatedTest() {
        // 测试逻辑
        assertTrue(true);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.3.2)标记运行顺序

@BeforeEach:在每个测试方法运行前执行的方法。

@BeforeEach注解用于标记一个方法,@BeforeEach执行多次,在每个测试方法执行之前都会被执行。这样可以确保在每个测试方法执行前都有一致的初始化操作。

public class ExampleTest {

    private String message;

    @BeforeEach
    public void init() {
        message = "Hello, World!";
    }

    @Test
    public void testMessage() {
        assertEquals("Hello, World!", message);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
@AfterEach:在每个测试方法运行后执行的方法。

使用@AfterEach注解的目的是确保在每个测试方法执行之后都有一致的清理操作。这有助于避免测试方法之间的相互影响,以及确保每个测试方法都在一个干净的状态下执行。

public class ExampleTest {

    private String message;

    @AfterEach
    public void cleanUp() {
        message = null;
    }

    @Test
    public void testMessage() {
        message = "Hello, World!";
        assertEquals("Hello, World!", message);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
@BeforeAll:在所有测试方法运行前执行的方法。

@BeforeAll只在第一次运行测试方法时执行一次,后面都不再执行。这意味着它用于执行一次性的全局初始化操作
而@BeforeEach会执行多次,每次运行测试方法都会执行。

public class ExampleTest {

    private static String message;

    @BeforeAll
    public static void init() {
        message = "Hello, World!";
    }

    @Test
    public void testMessage() {
        assertEquals("Hello, World!", message);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
@AfterAll:在所有测试方法运行后执行的方法。

@AfterAll注解标记的方法在整个测试类中的所有测试方法都执行之后执行一次,用于执行全局的清理操作。

如果我有4个测试方法,执行了3个,这时候是不会触发@AfterAll注解的方法的。

public class ExampleTest {

    private static String message;

    @AfterAll
    public static void cleanUp() {
        message = null;
    }

    @Test
    public void testMessageIsNull() {
        assertEquals(null, message);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.3.3)其他功能

@Disabled:指定禁用测试方法或测试类。

用于禁用单个测试方法或测试类,这意味着被注解的测试方法或测试类将不会被执行。这在临时禁用某些测试时非常有用,例如当测试方法出现问题或需要进行调整时。

public class ExampleTest {

    @Disabled("This test is currently disabled")
    @Test
    void disabledTest() {
        // Test logic that should be disabled
    }

    @Test
    void enabledTest() {
        // Test logic that should be enabled
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
@DisplayName:显示测试名称

@DisplayName(“Custom Test Name”) 用于指定测试方法的显示名称为 “Custom Test Name”。这个显示名称将会在测试报告、IDE中的测试运行结果以及构建工具(如Gradle、Maven)生成的测试报告中显示。

public class DisplayNameExample {

    @Test
    @DisplayName("Custom Test Name")
    void customTestName() {
        // 测试逻辑
        assertEquals(2, 1 + 1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
@Tag:指定测试方法的标签。

在JUnit 5中,使用@Tag注解在测试方法添加标签,也可以为整个测试类添加@Tag注解标签,以便更好地组织和筛选测试。

@Tag("development")
public class ExampleTest {
    
    @Test
    @Tag("unit")
    void test1() {
        // 测试方法1的测试逻辑
    }
    
    @Test
    @Tag("integration")
    void test2() {
        // 测试方法2的测试逻辑
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

通过为测试方法或测试类添加标签,可以更轻松地选择性地运行特定标签的测试,或者排除特定标签的测试。使用Maven在命令行中:

mvn test -Dgroups=tagName
//实例:
mvn test -Dgroups=unit
  • 1
  • 2
  • 3

通过Maven Surefire插件配置选择性运行标签的测试:在maven-surefire-plugin的groups,如果要运行标签为"tagName"的测试,可以将groups参数设置为"tagName"。再执行mvn test命令将会根据配置运行具有指定标签的测试。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M5</version>
            <configuration>
                <groups>tagName</groups>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

第四章、Mockito和JUnit中的方法介绍

Mockito测试框架介绍:允许开发人员创建模拟对象,以模拟外部依赖,并在单元测试中创建受控环境。有以下优势:
安全的重构:重构方法名称或交换参数不会改变测试代码,因为模拟对象是在运行时创建的。
异常支持:Mockito支持异常处理,可以利用堆栈跟踪查找异常的原因。
注解支持:Mockito可以使用注解如@Mock来创建模拟对象。
顺序支持:Mockito可以检查方法调用的顺序。

4.1)模拟对象

①mock()方法:

用于创建给定类或接口的模拟对象,创建对象的另一种方式就是直接用@Mock/InjectMocks注解来创建模拟对象,第三章有介绍。

注意:无法mock final类和final方法,静态方法和静态对象,私有方法。

import java.util.List;
import static org.mockito.Mockito.*;

public class MockitoMockMethodExample {

    public void testMultipleMocks() {
        // 使用 Mockito.mock() 方法创建多个模拟对象
        List<String> mockList1 = mock(List.class);
        List<String> mockList2 = mock(List.class);
        
        // 设置第一个模拟对象的预期行为
        when(mockList1.size()).thenReturn(5);
        
        // 设置第二个模拟对象的预期行为
        when(mockList2.size()).thenReturn(10);
        
        // 断言模拟对象的行为符合预期
        assert(mockList1.size() == 5);
        assert(mockList2.size() == 10);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
②spy()方法:

用于部分模拟对象,可以对真实对象的特定方法进行覆盖。它保留了真实对象的部分行为。Spy对象既可以模拟方法的返回值,也可以保留方法的实际行为。

示例中:spy() 方法创建了一个 ArrayList 对象的部分模拟,然后使用 when().thenReturn() 方法对 size() 方法进行了模拟。最后,调用部分模拟对象的真实方法时,模拟的方法返回了预期的值。

import static org.mockito.Mockito.*;

// 示例:创建一个部分模拟的对象
List<String> list = new ArrayList<>();
List<String> listSpy = spy(list);

// 示例:模拟部分方法
when(listSpy.size()).thenReturn(10);

// 示例:调用部分模拟对象的真实方法
listSpy.add("one");
System.out.println(listSpy.size()); // 输出 10,因为 size() 方法被模拟为返回 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4.2)定义模拟对象行为

4.2.1)模拟方法调用的返回值

①when()方法

用于指定模拟对象的方法调用,并设置相应的操作,例如返回值、异常等。
when():此方法需要一个方法调用作为参数(通常是模拟对象的一个方法)。它会记录下这个方法调用,并允许你接下来定义这个方法调用的行为。

when(service.方法名(参数))
  • 1
②thenReturn(要返回的值)

thenReturn(T value),这个方法需要你想要返回的值作为参数。当 when() 中指定的方法被调用时,模拟对象将返回这个值。

当使用模拟对象(用@Mock注释),选择thenReturn模拟有返回值(非void)的方法

如果使用监视对象(用@Spy注释),when(…) thenReturn(…)在返回指定值之前会进行实际方法调用。就需要处理这个方法可能抛出的异常。所以有异常的方法一般不使用thenReturn

when(service.方法名(参数)).thenReturn(要返回的值)
  • 1

示例:

public class MockitoExample {
    @Test
    void testWhenThenReturn() {
        // 创建模拟对象
        List mockedList = Mockito.mock(List.class);
        
        // 定义当调用mockedList.get(0)时返回"first"
        Mockito.when(mockedList.get(0)).thenReturn("first");

        // 使用模拟对象
        System.out.println(mockedList.get(0));  // 输出 "first"
        System.out.println(mockedList.get(1));  // 输出 null,因为我们没有定义get(1)的行为
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
③doReturn()

当使用模拟对象(用@Mock注释),选择thenReturn模拟无返回值(void)的方法

如果使用监视对象(用@Spy注释),doReturn(…) when(…)不会真正调用该方法。这意味着即使被调用的真实方法抛出异常,也不会影响测试。所以doReturn() 方法通常用于对部分模拟对象(spy)进行方法模。

doReturn(10).when(listSpy).size();
  • 1

示例:

import static org.mockito.Mockito.*;

// 示例:创建一个部分模拟的对象
List<String> list = new ArrayList<>();
List<String> listSpy = spy(list);

// 示例:使用doReturn方法模拟部分方法
doReturn(10).when(listSpy).size();

// 示例:调用部分模拟对象的真实方法
listSpy.add("one");
System.out.println(listSpy.size()); // 输出 10,因为 size() 方法被模拟为返回 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4.2.2)替换原方法的行为

①thenAnswer()

如果在方法调用中需要固定的返回值,则应使用thenReturn()。 如果需要执行某些操作或替换原方法的行为,则应使用thenAnswer()

Mockito.when(timeService.getCurrentTime()).thenAnswer(answer);
  • 1

如下示例:在调用getCurrentTime方法时作用是获取当前的日期和时间。我想使用Instant.now().toEpochMilli();方法替换getCurrentTime方法的内部代码逻辑。
1、实现接口org.mockito.stubbing.Answer的类的对象。
2、在方法answer(…)内部自定义行为。
3、我们从模拟对象中调用模拟方法getCurrentTime时,实际上是执行answer中的代码逻辑。

import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.time.Instant;

public class TimeServiceTest {
    @Test
    public void testGetCurrentTime() {
        // 创建TimeService类的模拟对象
        TimeService timeService = Mockito.mock(TimeService.class);

        // 创建一个 Answer 对象,实现 answer 方法来返回当前系统时间
        Answer<Long> answer = new Answer<Long>() {
            public Long answer(InvocationOnMock invocation) throws Throwable {
                return Instant.now().toEpochMilli();
            }
        };

        // 使用 thenAnswer() 指定模拟对象的方法返回当前系统时间
Mockito.when(timeService.getCurrentTime()).thenAnswer(answer);

        // 调用模拟对象的方法,实际执行的是answer中的代码逻辑
        long currentTime = timeService.getCurrentTime();

        // 验证方法是否被调用
        Mockito.verify(timeService).getCurrentTime();
        // 进行进一步的断言
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

我们可以也使用Java 8 lambda功能来实现answer方法。

public class TimeServiceTest {
    @Test
    public void testGetCurrentTime() {
        // 创建TimeService类的模拟对象
        TimeService timeService = Mockito.mock(TimeService.class);

        // 创建一个 Answer 对象,实现 answer 方法来返回当前系统时间
        Answer<Long> answer = new Answer<Long>() {
            public Long answer(InvocationOnMock invocation) throws Throwable {
                return Instant.now().toEpochMilli();
            }
        };

        // 使用 thenAnswer() 指定模拟对象的方法返回当前系统时间
        Mockito.when(timeService.getCurrentTime()).thenAnswer(answer);

        // 调用模拟对象的方法,实际执行的是answer中的代码逻辑
        long currentTime = timeService.getCurrentTime();

        // 验证方法是否被调用
        Mockito.verify(timeService).getCurrentTime();
        // 进行进一步的断言
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
②doAnswer

更适用于对 void 方法进行模拟。你可以使用 doAnswer 来执行额外的操作,比如触发回调、记录日志等,因为 void 方法本身没有返回值。
创建一个 Answer 对象,实现 answer 方法来指定特定的操作或返回值,

示例中:通过Mockito的模拟,我们成功地替换了原来calculate方法的实际行为

doAnswer(answer).when(service).方法(Mockito.anyInt(), Mockito.anyInt());
  • 1

示例:

// 假设有一个名为Service的类,其中包含一个方法
public class Service {
    public int calculate(int a, int b) {
        // 一些复杂的计算
        return a + b;
    }
}

// 在测试中,使用Mockito来模拟Service类的行为
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class ServiceTest {
    @Test
    public void testCalculate() {
        // 创建Service类的模拟对象
        Service service = Mockito.mock(Service.class);
        
        // 创建一个 Answer 对象,实现 answer 方法来指定特定的操作或返回值
        Answer<Integer> answer = new Answer<Integer>() {
            public Integer answer(InvocationOnMock invocation) throws Throwable {
                int a = invocation.getArgument(0);
                int b = invocation.getArgument(1);
                // 在 answer 方法中执行特定的操作或返回特定的值
                return a * b;
            }
        };
        
//doAnswer表示当calculate方法被调用时,执行特定的操作   
Mockito.doAnswer(answer).when(service).calculate(Mockito.anyInt(), Mockito.anyInt());
        
        // 调用模拟对象的方法
        //当调用service.calculate(2, 3)时,实际执行的是2 * 3,结果为6
        int result = service.calculate(2, 3);
        
        // 验证方法是否被调用,并且返回了预期的值
        Mockito.verify(service).calculate(2, 3);
        assertEquals(6, result);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

4.2.3)部分模拟时是否调用真实方法

①thenCallRealMethod()

用于部分模拟对象(spy)时,当调用指定方法时,实际调用对象的真实方法而不是模拟方法。特别是当需要部分模拟对象并且希望某些方法执行真实逻辑时。

when(service.方法名(参数)).thenCallRealMethod()
  • 1

示例:

// 假设有一个名为UserService的类
public class UserService {
    public String process(String input) {
        // 实际的方法逻辑
        return "Processed: " + input;
    }
}

// 创建UserService的部分模拟对象
UserService userService = Mockito.spy(UserService.class);

// 当调用process方法时,执行实际的方法逻辑
Mockito.when(userService.process("input")).thenCallRealMethod();

// 调用部分模拟对象的方法
String result = userService.process("input");

// 输出结果,将会是"Processed: input",因为实际的方法逻辑被执行了
System.out.println(result);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
②doCallRealMethod()

用于部分模拟对象(spy)时,用于调用真实对象的实际方法,而不是模拟方法的行为。这在部分模拟对象(spy)中特别有用,因为它允许部分模拟对象调用其真实的方法,而不是模拟方法的行为。

doCallRealMethod().when(someObject).someMethod();
  • 1

示例:

// 创建一个部分模拟对象
SomeClass someObject = mock(SomeClass.class);
doCallRealMethod().when(someObject).someMethod();

// 调用真实方法
someObject.someMethod();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4.2.4)模拟抛出异常

①thenThrow()

thenThrow 适用于有出参的方法,thenThrow方法指定了在调用该方法时抛出指定的异常,而 thenThrow() 方法更适合在特定条件下抛出异常。

when(mockedList.get(0)).thenThrow(new RuntimeException())
  • 1

示例:

import static org.mockito.Mockito.*;

public class ExampleTest {
    @Test
    public void testMethod() {
        // 创建被mock的对象
        SomeClass mockObj = mock(SomeClass.class);
        
        // 设置当调用mockObj.method时抛出异常
        when(mockObj.method(anyString())).thenThrow(new RuntimeException("Something went wrong"));
        
        // 调用被mock的方法这里会抛出异常
        mockObj.method("test");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
②doThrow()

doThrow 适用于 没有出参的方法,在测试中模拟抛出异常的情况。doThrow() 方法更适合在调用特定方法时强制抛出异常

doThrow(new RuntimeException()).when(mockedList).get(0)
  • 1

示例:

import static org.mockito.Mockito.*;

// 创建mock对象
List<String> mockedList = mock(List.class);

// 指定当调用mockedList的get()方法时抛出异常
doThrow(new RuntimeException()).when(mockedList).get(0);

// 调用被测方法
mockedList.get(0); // 这里会抛出指定的异常
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.2.5)模拟构造函数和静态方法

①模拟构造函数MockedConstruction

在单元测试中,当需要在测试中模拟某个类的实例时,使用MockedConstruction可以模拟构造函数的行为,如果没有模拟构造函数,那么在使用new关键字创建对象时,会执行实际的构造函数

作用:当需要在测试中模拟第三方库的对象时,可以使用MockedConstruction来模拟构造函数,而不实际调用第三方库的构造函数,从而避免对外部资源的依赖。

import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;

public class ExampleTest {

    @Test
    void testWithMockedConstruction() {
    //模拟构造函数行为
        try (MockedConstruction<MyClass> mocked = Mockito.mockConstruction(MyClass.class)) {
        //模拟完以后再创建对象,此时执行的就是模拟的构造函数,而不是实际的
            MyClass myClass = new MyClass();
            //调用get方法
            Mockito.when(myClass.getName()).thenReturn("mocked value");
            // 在这里使用myClass进行测试
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
②模拟静态方法:MockedStatic

MockedStatic是Mockito框架中的一个类,用于模拟静态方法的行为。通过使用MockedStatic,您可以模拟静态方法的返回值,以及验证静态方法的调用次数。

import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class Example {
    public void exampleMethod() {
    //YourClassWithStaticMethod是包含静态方法的类
try (MockedStatic<YourClassWithStaticMethod> mockedStatic = Mockito.mockStatic
        (YourClassWithStaticMethod.class)) {
        
mockedStatic.when(YourClassWithStaticMethod::staticMethod("param1")).thenReturn("someValue");
            // 调用静态方法
YourClassWithStaticMethod.staticMethod("param1");
            // 验证静态方法是否被调用过
mockedStatic.verify(YourClassWithStaticMethod::staticMethod("param1"));
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

4.2.6)其他方法

①使void方法什么也不做doNothing()

doNothing() 方法的作用是用于设置模拟对象的 void 方法不执行任何操作。因为 mock 对象中,void 函数就是什么都不做,所以该方法更适合 spy 对象。

Mockito.doNothing().when(service).方法名(参数)
  • 1

示例1:


    static class ExampleClass {
        public void hello() {
            System.out.println("吃过了");
        }
    }
/
public class MockDemo {
    @Test
    public void test() {
        ExampleClass exampleClass = spy(new ExampleClass ());
        ExampleClass .hello();  //打印吃过了
        // 通过doNothein让方法什么都不做
        doNothing().when(exampleClass ).hello();
        exampleClass.hello(); // 什么都不输出
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

示例2:这是复制的国外一个博主的代码

List list = new LinkedList(); 
List spy = spy(list); 
//let's make clear() do nothing 
doNothing().when(spy).clear(); 
spy.add("one"); 
//clear() does nothing, so the list still contains "one" 
spy.clear();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
②重置模拟对象状态reset()

用于重置模拟对象的状态或者将对象恢复到初始状态

import org.junit.Test;
import org.mockito.Mockito;

// 创建一个模拟对象
List<String> mockedList = Mockito.mock(List.class);

// 对模拟对象进行方法调用
mockedList.add("one");
System.out.println(mockedList.size()); // 输出 1

// 重置模拟对象的状态
Mockito.reset(mockedList);

// 再次对模拟对象进行方法调用
System.out.println(mockedList.size()); // 输出 0,因为模拟对象的状态已被重置
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

4.3)模拟对象行为验证

4.3.1)验证调用次数

①verify()方法:

用于验证模拟对象的指定方法是否被调用。可以进一步验证方法的调用次数和参数。

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.List;

public class MockitoExample {
    @Test
    void testMock() {
        // 创建模拟对象
        List mockedList = Mockito.mock(List.class);

        // 使用模拟对象
        mockedList.add("one");
        Mockito.verify(mockedList).add("one");

        // 验证模拟对象的某些行为是否发生过
        Mockito.verify(mockedList, Mockito.times(1)).add("one");
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
②verify(object, Mockito.times(2))方法

是一个 Mockito 框架中的验证方法,用于验证某个行为在模拟对象上被调用了几次。times() 方法需要一个整数参数,代表你期望的调用次数。然后,将返回一个 VerificationMode 实例,这个实例可以和 verify() 方法一起使用,来检查某个操作是否被执行了指定次数。

下述代码;Mockito.times(2) 指的是,我们期望 add() 方法在模拟对象 mockedList 上被调用了两次。如果 add() 方法的实际调用次数不匹配我们的期望(例如,它只被调用了一次,或者三次),那么代码会抛出一个 MockitoAssertionError 异常。

public class MockitoExample {
    @Test
    void testTimes() {
        // 创建模拟对象
        List mockedList = Mockito.mock(List.class);

        // 模拟方法调用
        mockedList.add("one");
        mockedList.add("one");

        // 验证该方法是否被调用了两次
        Mockito.verify(mockedList, Mockito.times(2)).add("one");
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

下述代码:不会抛出异常因为add了两次,与预期相符

import java.util.List;
import static org.mockito.Mockito.*;

public class MockitoTimesExample {

    public void testMethodInvocation() {
        // 创建模拟对象
        List<String> mockList = mock(List.class);
        
        // 调用模拟对象的方法
        mockList.add("item1");
        mockList.add("item2");
        
        // 验证方法调用次数
        verify(mockList, times(2)).add(anyString());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

4.3.2)验证是否发生交互

①verifyNoMoreInteractions()方法:

verifyNoMoreInteractions() 方法用于验证mock对象在特定交互之后是否没有发生任何其他交互。它确保在测试中,mock对象在预期的交互之后没有进行多余交互。

多余交互指的是对于被mock的对象,测试代码中发生了未被预期的方法调用。这可能是因为测试代码中的某些逻辑导致了额外的方法调用。

import org.junit.Test;
import org.mockito.Mockito;

public class ProcessorTest {
  @Test
  public void processTest() {
      // 创建 MyService 的 mock 对象
      MyService myMockService = Mockito.mock(MyService.class);
      
      // 创建 MyProcessor 对象,并传入 mock 对象
      MyProcessor myProcessor = new MyProcessor(myMockService);
      
      // 调用 MyProcessor 的 process() 和 process2() 方法
      myProcessor.process();
      myProcessor.process2();

      // 验证 mock 对象的交互
      Mockito.verify(myMockService).doSomething();
      Mockito.verify(myMockService).doSomething2();

      // 确保没有更多的交互发生
      Mockito.verifyNoMoreInteractions(myMockService);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
②verifyZeroInteractions()方法:

用于验证模拟对象上是否没有发生任何交互。它确保在测试中,mock对象没有与任何其他对象进行交互。关注的是整个测试过程中是否有任何交互。

import static org.mockito.Mockito.*;

// 创建mock对象
List<String> mockedList = mock(List.class);

// 调用被测方法
mockedList.clear();

// 验证是否发生了交互
verify(mockedList).clear();
verifyNoMoreInteractions(mockedList); // 确保没有其他交互
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.4)Assert结果断言

4.4.1)验证结果

①验证是否相等:Equals

assertEquals(expected, actual):验证两个对象是否相等。
assertNotEquals(unexpected, actual):验证两个对象是否不相等。

示例:验证 Calculator.add(3, 5) 的结果是否等于 8。如果不相等,测试用例将会失败,并输出指定的失败信息。

import static org.junit.jupiter.api.Assertions.assertEquals;

@Test
void testAddition() {
    int result = Calculator.add(3, 5);
    assertEquals(8, result, "The addition result should be 8");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

assertArrayEquals(expectedArray, resultArray):验证两个数组是否相等。
assertArrayEquals(expecteds, actuals, delta):验证两个浮点数数组是否相等,可以指定误差范围。

示例:验证 expectedArray 和 resultArray 是否相等。如果两个数组不相等,测试用例将会失败,并输出指定的失败信息。

import static org.junit.jupiter.api.Assertions.assertArrayEquals;

@Test
void testArrayEquality() {
    int[] expectedArray = {1, 2, 3};
    int[] resultArray = {1, 2, 3};
    assertArrayEquals(expectedArray, resultArray, "The arrays should be equal");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
②验证结果真假

assertTrue(): 期待结果为true
assertFalse(): 期待结果为false

示例:验证 number 是否大于 0。如果条件不满足,测试用例将会失败,并输出指定的失败信息。

import static org.junit.jupiter.api.Assertions.assertTrue;

@Test
void testIsPositive() {
    int number = 10;
    assertTrue(number > 0, "The number should be positive");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
③验证结果是否为null

assertNull(object):期待结果为空。
assertNotNull(object): 期待结果为非空

示例:验证 obj 是否为空。如果对象不为空,测试用例将会失败,并输出指定的失败信息。

import static org.junit.jupiter.api.Assertions.assertNull;

@Test
void testObjectNull() {
    Object obj = null;
    assertNull(obj, "对象应为空");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
④验证对象引用

assertSame(object1, object2):验证两个对象引用是否指向同一个对象。
assertNotSame(object1, object2):验证两个对象引用是否不指向同一个对象。

示例:验证 obj1 和 obj2 是否引用同一个对象。如果两个对象引用不同的对象,测试用例将会失败,并输出指定的失败信息。

import static org.junit.jupiter.api.Assertions.assertSame;

@Test
void testObjectReference() {
    Object obj1 = new Object();
    Object obj2 = obj1;
    assertSame(obj1, obj2, "两个对象应该引用同一个对象");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
⑤验证是否满足指定条件

assertThat(actual, matcher):使用Matcher对象验证实际值是否满足指定条件。
assertThat 方法接受两个参数:实际值和Matcher对象。Matcher对象定义了对实际值的特定条件,例如包含特定子串、大于某个值等。通过使用Matcher对象,可以实现更加灵活和具体的断言验证。
示例1:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

// 示例:验证字符串是否包含指定子串
String actual = "Hello World";
Matcher<String> containsString = containsString("Hello");
assertThat(actual, containsString);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

示例2:


// 示例:验证数字是否大于指定值
int actualNumber = 10;
Matcher<Integer> greaterThan = greaterThan(5);
assertThat(actualNumber, greaterThan);
  • 1
  • 2
  • 3
  • 4
  • 5

4.4.2)使测试直接失败

①使测试用例失败:fail()

直接使测试用例失败,可用于标记测试用例未通过的情况。

import static org.junit.jupiter.api.Assertions.fail;

@Test
void testSomething() {
    // 执行一些测试逻辑
    // 如果满足特定条件,则强制测试失败
    if (conditionIsMet) {
        fail("Test 测试 失败 because xxxx");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第五章、参数匹配器和参数捕捉器

5.1)参数匹配器

5.1.1)参数匹配器列表

Mockito框架中的参数匹配器是用于在测试中进行灵活验证和存根设置的工具。如果使用了参数匹配器,方法中的所有参数都必须是匹配器。

verify(mockClass).someMethod(anyInt(), anyString(), eq("third argument"));
  • 1

参数匹配器列表:

参数匹配器解释说明
any()参数为任意类型
anyXxx()包括anyInt、anyBoolean、anyByte、anyChar、anyFloat、Double、anyString、anyList、anyIterable等等
any(Class<T> type)任意指定的Class类型,除了null
isA(Class<T> type)指定类型的实现对象
eq(value)参数匹配指定的值
same(expectedObject)参数和给定的值是同一个对象
isNull()参数是null值
notNull()参数非null
nullable(Class clazz)null 或者给定的类型
contains(String substring)参数包含指定的字符串
matches(String regex)匹配正则表达式
endsWith(String suffix)以xx结尾
startsWith(String prefix)以xx开头
argThat(ArgumentMatcher matcher)自定义匹配器
times()匹配精确的次数

5.1.2)参数匹配器示例

①anyInt()

示例:使用anyInt()方法来指定参数的范围,从而实现了对mockList.get()方法的灵活验证和存根设置。

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.junit.Test;
import java.util.List;

public class TestList {
    @Test
    public void testList_Argument_Matchers() {
        List<String> mockList = mock(List.class);
        when(mockList.get(anyInt())).thenReturn("Mockito");
        assertEquals("Mockito", mockList.get(0));
        assertEquals("Mockito", mockList.get(1));
        assertEquals("Mockito", mockList.get(2));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
②any(Class<> type)

示例:使用参数匹配器any(Class type)来存根方法

import static org.mockito.Mockito.*;

public class Example {
    public void testMethod() {
        // 创建一个mock对象
        MyClass myClass = mock(MyClass.class);
        
        // 使用参数匹配器any(Class<T> type)来存根方法
        when(myClass.method(any(String.class))).thenReturn("mocked");

        // 调用被存根的方法
        String result = myClass.method("input");

        // 验证方法是否按预期调用
        verify(myClass).method(any(String.class));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
③eq()

示例:使用参数匹配器eq(value)来存根方法

import static org.mockito.Mockito.*;

public class Example {
    public void testMethod() {
        // 创建一个mock对象
        MyClass myClass = mock(MyClass.class);
        
        // 使用参数匹配器eq(value)来存根方法
        when(myClass.method(eq("input"))).thenReturn("mocked");

        // 调用被存根的方法
        String result = myClass.method("input");

        // 验证方法是否按预期调用
        verify(myClass).method(eq("input"));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
④same(expectedObject)
import org.junit.Test;
import static org.mockito.Mockito.*;

public class ExampleTest {

    @Test
    public void testWithSameMatcher() {
        // 创建mock对象
        MyClass myClass = mock(MyClass.class);

        // 创建预期参数对象
        Object expectedObject = new Object();

        // 设置预期行为,使用参数匹配器same(value)来确保方法调用的参数是同一个对象
        when(myClass.methodWithArgument(same(expectedObject))).thenReturn("mockedResult");

        // 调用被测试方法
        String result = myClass.methodWithArgument(expectedObject);

        // 验证预期行为
        verify(myClass).methodWithArgument(same(expectedObject));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
⑤endsWith()
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.endsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class ExampleTest {

    @Test
    public void testMethodWithEndsWithMatcher() {
        // 创建mock对象
        MyClass myClass = mock(MyClass.class);

        // 调用被测试方法
        myClass.myMethod("test123");

        // 验证方法是否被调用,并且传入的参数以特定后缀结尾
        verify(myClass).myMethod(endsWith("123"));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

特殊的匹配器,用于验证实际对存根的调用,例如times()、never()、atLeast()等。

5.2)参数捕捉器

帮助我们捕捉传递给模拟对象方法的参数,并且使我们能够对这些参数进行额外的断言。在Java中,Mockito库提供了@Captor注解和ArgumentCaptor类来实现参数捕捉器的功能。

5.2.1)@Captor

@Captor是Mockito框架中的一个注解,用于捕获方法调用时传入的参数,以便在测试中对参数进行断言或验证。
参数捕获:在测试中,使用@Captor可以捕获方法调用时传递的参数,以便在后续的断言中验证参数的值。
参数验证:通过捕获参数,可以对传递给方法的参数进行验证,确保方法得到了期望的参数值。
灵活性:@Captor提供了一种灵活的方式来处理方法调用时的参数,使得测试更加精确和可靠。

// 示例代码中使用@Captor注解捕获参数
@ExtendWith(MockitoExtension.class)
class ExampleTest {

    @Captor
    //@Captor注解创建了一个ArgumentCaptor对象
    private ArgumentCaptor<String> stringCaptor;

    @Mock
    private SomeClass someClass;

    @Test
    void testSomething() {
        // 调用被测试方法
        someClass.doSomething("test");

        //verify(someClass)验证someClass对象的doSomething方法是否被调用
 //通过stringCaptor.capture()捕获doSomething方法的参数
        verify(someClass).doSomething(stringCaptor.capture());
        //通过stringCaptor.getValue()获取捕获的参数值
        assertEquals("test", stringCaptor.getValue());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

5.2.2)ArgumentCaptor类

ArgumentCaptor argument = ArgumentCaptor.forClass(Class clazz) 创建指定类型的参数捕获器

argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值

argument.getAllValues() 方法进行多次调用后,返回多个参数值

@Test
public void argumentCaptor() {
    // 创建模拟对象
    List mockList = mock(List.class);
    List mockList1 = mock(List.class);

    // 在模拟对象上调用add方法
    mockList.add("666");
    mockList1.add("aaa");
    mockList1.add("ttt");

    // 获取方法参数
    ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
    verify(mockList).add(argument.capture());
    System.out.println(argument.getValue());    //666
    // 输出捕获的参数值,预期为"666"

    // 多次调用获取最后一次
    ArgumentCaptor argument1 = ArgumentCaptor.forClass(String.class);
    verify(mockList1, times(2)).add(argument1.capture());
    System.out.println(argument1.getValue());    //ttt
    // 输出捕获的参数值,预期为"ttt"

    // 获取所有调用参数
    System.out.println(argument1.getAllValues());    //[aaa, ttt]
    // 输出捕获的所有参数值,预期为["aaa", "ttt"]
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

第六章、使用Mockito的测试实战

6.1)需要测试的方法

需要进行测试的方法
测试如下方法:

@Component
@RequestMapping(path = "/BidInfoService")
public class BidInfoServiceImpl implements BidInfoService {

    @Autowired
    BidInfoMapper bidInfoMapper;

    @Autowired(required = false)
    RedisTemplate redisTemplate;


    // 累计成交额:总金额
    @Override
    @GetMapping("/queryBidMoneySum")
    @ResponseBody
    public Double queryBidMoneySum() {
        //通过工具类常量对应的值,获得展示值
Double bidMoneySum = (Double) redisTemplate.opsForValue().get(Constants.BID_MONEY_SUM);
        if (bidMoneySum != null) {
            return bidMoneySum;
        }
        if (bidMoneySum == null) {
            //如果缓存中值不存在,访问数据库得到值
            bidMoneySum = bidInfoMapper.selectBidMoneySum();
            if (bidMoneySum != null) {
                //将从数据库获得的值设置到缓存,有效时间42秒
redisTemplate.opsForValue().set(Constants.BID_MONEY_SUM, bidMoneySum, 42, TimeUnit.SECONDS);
            } else {
redisTemplate.opsForValue().set(Constants.BID_MONEY_SUM, null, 42, TimeUnit.SECONDS);
            }
        }
        return bidMoneySum;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

6.2)编写单元测试

每一行都详细写了注释,认真看就知道怎么用咯

package com.bjpowernode.money.service;


/**
 * Tesr for {@link BidInfoServiceImpl}
 * @author bms
 * @since 1.0
 */

// 在junit4的时候使用@Runwith,在junit5的时候使用@SpringBootTest,
//作用是加载web Application Context并提供Mock Web Environment
@RunWith(MockitoJUnitRunner.class)
public class BidInfoServiceImplTest {
    //把要测试的类通过mock注入
    @InjectMocks
    BidInfoServiceImpl bidInfoServiceImpl;
    //在运行时,需要用到的对象,使用@mock造个假对象
    @Mock
    RedisTemplate redisTemplate;
    //在运行时,需要用到的对象,使用@mock造个假对象
    @Mock
    BidInfoMapper bidInfoMapper;


    @Test
    public void testQueryBidMoneySum(){
        //缓存不为空的情况:
        //造一个假的值
        ValueOperations value = mock(ValueOperations.class);
        //redisTemplate调用opsForValue,获得(返回)这个mock值
        when(redisTemplate.opsForValue()).thenReturn(value);
        //获得这个mock值value后,调用get方法。得到返回值是5.345
        when(value.get(Constants.BID_MONEY_SUM)).thenReturn(5.345);
        //本类的对象调用需要测试的方法queryBidMoneySum,期望返回值是5.345
        assertEquals(5.345, bidInfoServiceImpl.queryBidMoneySum());

        //缓存为空的情况
        when(redisTemplate.opsForValue()).thenReturn(value);
        //获得这个mock值value后,调用get方法。得到返回值是null
        when(value.get(Constants.BID_MONEY_SUM)).thenReturn(null);
        //当返回值是null,执行selectBidMoneySum去数据库查询,得到返回值是8.88
        when(bidInfoMapper.selectBidMoneySum()).thenReturn(8.88);
        //将从数据库获得的值设置到缓存,有效时间42秒。 因为没有返回值所以使用doNothing().when(value)
        doNothing().when(value).set(Constants.BID_MONEY_SUM, 8.88, 42, TimeUnit.SECONDS);
        //本类的对象调用需要测试的方法queryBidMoneySum,期望返回值是8.88
        assertEquals(8.88, bidInfoServiceImpl.queryBidMoneySum());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/581062
推荐阅读
相关标签
  

闽ICP备14008679号