PowerMock的初使用

本文介绍如何使用PowerMock框架解决Mockito无法模拟静态方法、私有方法及final方法的问题。通过添加特定依赖并使用PowerMockito.mockStatic()方法,可以实现对静态方法的模拟,使单元测试更加灵活。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在写UT的时候,会经常使用到Mockito来写测试用例,但是Mockito无法对静态方法、私有方法以及final方法进行mock。这里可以使用功能强大的PowerMock来实现。
 PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。通过使用自定义的类加载器,简化采用的IDE或持续集成服务器不需要做任何改变。目前PowerMock支持EasyMock和Mockito。
 闲话少说,先上代码:
 这里需要用到一些依赖包,项目用maven管理,添加如下依赖

 		<dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.powermock/powermock-core -->
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-core</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>2.0.2-beta</version>
            <scope>test</scope>
        </dependency>
		 <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

先是一个util类:Calculate.java

public class CalculateUtil {

    public static int add(int a, int b){
        return a + b;
    }
}

然后是测试类

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;

import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.junit.Assert.*;


@RunWith(PowerMockRunner.class)
@PrepareForTest( CalculateUtil.class )
public class CalculateUtilTest {

    @Test
    public void add() {
        PowerMockito.mockStatic(CalculateUtil.class);
        PowerMockito.when(CalculateUtil.add(1, 1)).thenReturn(0);
        assertEquals(0,CalculateUtil.add(1,1));
    }
}

这里可以看出,我们让add方法在执行1+1操作时,返回的结果为0,然后使用assertEquals返回的是相等。可以让静态方法返回指定值了。

异常:
当执行报如下异常时:

java.lang.NoClassDefFoundError: org/powermock/core/classloader/annotations/Mock

	at org.powermock.api.extension.listener.AnnotationEnabler.getMockAnnotations(AnnotationEnabler.java:128)
	at org.powermock.api.extension.listener.AnnotationEnabler.standardInject(AnnotationEnabler.java:84)
	at org.powermock.api.extension.listener.AnnotationEnabler.beforeTestMethod(AnnotationEnabler.java:63)
	at org.powermock.tests.utils.impl.PowerMockTestNotifierImpl.notifyBeforeTestMethod(PowerMockTestNotifierImpl.java:82)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.executeTest(PowerMockJUnit44RunnerDelegateImpl.java:308)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.executeTestInSuper(PowerMockJUnit47RunnerDelegateImpl.java:131)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.access$100(PowerMockJUnit47RunnerDelegateImpl.java:59)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner$TestExecutorStatement.evaluate(PowerMockJUnit47RunnerDelegateImpl.java:147)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.evaluateStatement(PowerMockJUnit47RunnerDelegateImpl.java:107)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.executeTest(PowerMockJUnit47RunnerDelegateImpl.java:82)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.runBeforesThenTestThenAfters(PowerMockJUnit44RunnerDelegateImpl.java:298)
	at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:87)
	at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:50)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.invokeTestMethod(PowerMockJUnit44RunnerDelegateImpl.java:218)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.runMethods(PowerMockJUnit44RunnerDelegateImpl.java:160)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$1.run(PowerMockJUnit44RunnerDelegateImpl.java:134)
	at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:34)
	at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:44)
	at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.run(PowerMockJUnit44RunnerDelegateImpl.java:136)
	at org.powermock.modules.junit4.common.internal.impl.JUnit4TestSuiteChunkerImpl.run(JUnit4TestSuiteChunkerImpl.java:117)

这里应该是导入的包出错了。我当时就遇到了这个情况,是

<dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

导入错了,导入成了

<dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito</artifactId>
            <version>2.0.2</version>
            <scope>test</scope>
        </dependency>

注意:两者的artifctld的不一样的,要使用有2的这个

<think>我们被要求为`updateTenant`方法编写单元测试,并且要覆盖其中调用的私有方法。但是,我们不能使用PowerMock,只能使用Mockito或其他测试框架。由于Mockito本身不能直接模拟私有方法,因此我们需要通过以下方式来处理: 1. 通过公有方法间接测试私有方法:即测试`updateTenant`方法时,自然会调用到私有方法,我们可以通过设置输入参数和模拟依赖,使得私有方法中的逻辑被执行到。 2. 使用反射来调用私有方法:如果确实需要单独测试私有方法,我们可以使用反射来调用它们,但这通常不是最佳实践,因为单元测试应该关注公共接口。 但是,根据问题要求,我们需要覆盖私有方法。由于我们无法使用PowerMock,所以我们将采用以下策略: - 对于私有方法,我们不直接模拟,而是通过控制输入和模拟依赖,使得在测试`updateTenant`方法时,私有方法中的各个分支都被执行到。 - 如果私有方法中包含了复杂的逻辑,我们可以考虑将这些逻辑抽取到一个单独的类中(使用策略模式等),然后通过模拟这个类来间接控制。但这里我们暂时不重构代码,而是通过设置不同的测试场景来覆盖。 由于私有方法`validTenant`, `deleteList`, `updateList`, `addList`, `createTenantUserConfigList`都是在`updateTenant`中被调用的,我们可以通过设计不同的测试用例,使得这些私有方法中的每一条分支都被覆盖。 因此,我们需要设计多个测试用例,例如: 1. 测试租户启用时修改名称和子产品,期望抛出异常。 2. 测试租户名称已存在,期望抛出异常。 3. 测试子产品已存在,期望抛出异常。 4. 测试正常更新租户信息,并且租户管理员列表有变化(包括需要删除、更新、新增的情况)。 但是,在测试中,我们需要模拟`iTenantInfoDao`和`iTenantUserConfigDao`等依赖,以避免与数据库交互。 另外,由于`RequestContext`是获取当前用户上下文的,我们也需要模拟它。 由于我们无法模拟私有方法,所以我们需要通过设置不同的参数和模拟依赖的返回值,使得私有方法中的条件分支都得到执行。 步骤: 1. 使用Mockito的`@Mock`来模拟依赖的DAO和Service。 2. 使用`@InjectMocks`来创建被测类的实例,并将模拟的依赖注入进去。 3. 在测试方法中,设置不同的测试场景,包括: a. 模拟`RequestContext`中的用户信息。 b. 设置输入参数`tenantDto`的不同属性。 c. 模拟DAO层方法的不同返回值,以触发不同的分支。 由于代码较长,我们只编写一个测试类,并在其中编写多个测试方法来覆盖不同的情况。 注意:由于私有方法`validTenant`中会调用`iTenantInfoDao.getTenantInfo`等方法,我们需要模拟这些方法的返回值。 下面是一个示例测试类,我们将覆盖几个主要场景: 由于代码量较大,我们分步骤编写: 1. 测试租户启用时修改名称或子产品,期望抛出异常。 2. 测试租户名称已存在,期望抛出异常。 3. 测试子产品已存在,期望抛出异常。 4. 测试正常更新租户信息,且租户管理员列表有变化(包括删除、更新、新增)。 由于`updateTenant`方法中调用了多个DAO方法,我们需要模拟: - `iTenantInfoDao.updateTenant` - `iTenantUserConfigDao.updateTenantUserSubProductCode` - `iTenantUserConfigDao.getAllTenantUserInfo` - `userAuthService.deleteRole` - `iTenantUserConfigDao.updateTenantUserOwnerFlag` - `iTenantUserConfigDao.createTenantUserInfo` 另外,还需要模拟`RequestContext`。 由于`RequestContext`可能是静态方法,我们可以使用`MockedStatic`(需要Mockito 3.4+)来模拟静态类。但注意,如果使用Mockito的内联MockMaker,可以模拟静态方法。我们这里使用Mockito的`mockStatic`。 首先,确保依赖: ```xml <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>3.8.0</version> <scope>test</scope> </dependency> ``` 然后,在测试类中: ```java import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class TenantServiceTest { @Mock private ITenantInfoDao iTenantInfoDao; @Mock private ITenantUserConfigDao iTenantUserConfigDao; @Mock private UserAuthService userAuthService; @InjectMocks private TenantService tenantService; // 假设updateTenant方法在TenantService类中 // 测试租户启用时修改名称,期望抛出异常 @Test public void testUpdateTenantWhenEnabledAndChangeName_ThrowsException() { TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("NewName"); tenantDto.setSubProductCode("Sub1"); tenantDto.setOwners(Collections.singletonList(100L)); TenantInfoVO tenantInfo = new TenantInfoVO(); tenantInfo.setStatus("1"); // 启用 tenantInfo.setName("OldName"); tenantInfo.setSubProductCode("Sub1"); // 模拟DAO返回已存在的租户信息(启用状态) when(iTenantInfoDao.getTenantInfo(any(TenantDto.class))).thenReturn(tenantInfo); // 执行并验证异常 Exception exception = assertThrows(IllegalArgumentException.class, () -> { tenantService.updateTenant(tenantDto); }); assertEquals("租户启用中,不能修改租户名称及子产品", exception.getMessage()); } // 测试租户名称已存在,期望抛出异常 @Test public void testUpdateTenantWhenNameExists_ThrowsException() { TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("ExistingName"); tenantDto.setSubProductCode("Sub1"); tenantDto.setOwners(Collections.singletonList(100L)); TenantInfoVO tenantInfo = new TenantInfoVO(); tenantInfo.setStatus("0"); // 未启用,所以不会触发第一个异常 when(iTenantInfoDao.getTenantInfo(any(TenantDto.class))).thenReturn(tenantInfo); // 模拟查询其他租户中是否存在相同名称 TenantDto nameDto = new TenantDto(); nameDto.setId(1L); nameDto.setName("ExistingName"); when(iTenantInfoDao.getTenantInfoNot(nameDto)).thenReturn(new TenantInfoVO()); // 返回非空,表示存在 Exception exception = assertThrows(IllegalArgumentException.class, () -> { tenantService.updateTenant(tenantDto); }); assertEquals("租户名称已存在", exception.getMessage()); } // 测试子产品已存在,期望抛出异常 @Test public void testUpdateTenantWhenSubProductCodeExists_ThrowsException() { TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("NewName"); tenantDto.setSubProductCode("ExistingSubProductCode"); tenantDto.setOwners(Collections.singletonList(100L)); TenantInfoVO tenantInfo = new TenantInfoVO(); tenantInfo.setStatus("0"); when(iTenantInfoDao.getTenantInfo(any(TenantDto.class))).thenReturn(tenantInfo); // 模拟名称检查通过 when(iTenantInfoDao.getTenantInfoNot(argThat(dto -> dto.getName() != null))).thenReturn(null); // 模拟子产品检查不通过 TenantDto subProductCodeDto = new TenantDto(); subProductCodeDto.setId(1L); subProductCodeDto.setSubProductCode("ExistingSubProductCode"); when(iTenantInfoDao.getTenantInfoNot(subProductCodeDto)).thenReturn(new TenantInfoVO()); Exception exception = assertThrows(IllegalArgumentException.class, () -> { tenantService.updateTenant(tenantDto); }); assertEquals("子产品已存在", exception.getMessage()); } // 测试正常更新租户,并且租户管理员有变化(需要删除、更新、新增) @Test public void testUpdateTenant_SuccessWithOwnerChanges() { try (MockedStatic<RequestContext> mockedStatic = mockStatic(RequestContext.class)) { // 模拟RequestContext RequestContext requestContext = mock(RequestContext.class); User user = mock(User.class); when(user.getUserId()).thenReturn(123L); when(requestContext.getUser()).thenReturn(user); mockedStatic.when(RequestContext::getCurrent).thenReturn(requestContext); TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("NewName"); tenantDto.setSubProductCode("Sub1"); tenantDto.setOwners(Arrays.asList(101L, 102L)); // 新的管理员列表 // 模拟validTenant通过 TenantInfoVO tenantInfo = new TenantInfoVO(); tenantInfo.setStatus("0"); // 未启用 when(iTenantInfoDao.getTenantInfo(any(TenantDto.class))).thenReturn(tenantInfo); when(iTenantInfoDao.getTenantInfoNot(any(TenantDto.class))).thenReturn(null); // 名称和子产品都不存在 // 模拟数据库中已有的租户管理员 TenantUserConfigVO existingOwner1 = new TenantUserConfigVO(); existingOwner1.setUserId("100"); // 这个用户不在新的管理员列表中,且是owner,所以会被删除 existingOwner1.setRoleId("defaultRole"); existingOwner1.setOwnerFlag("1"); TenantUserConfigVO existingOwner2 = new TenantUserConfigVO(); existingOwner2.setUserId("101"); // 这个用户在新的管理员列表中,所以会保留(更新) existingOwner2.setRoleId("defaultRole"); existingOwner2.setOwnerFlag("1"); List<TenantUserConfigVO> existingOwners = Arrays.asList(existingOwner1, existingOwner2); when(iTenantUserConfigDao.getAllTenantUserInfo(any(TenantUserConfigVO.class))).thenReturn(existingOwners); // 模拟默认角色 // 注意:在私有方法中调用了getDefaultRole(),我们需要确保它返回一个非空值,或者我们可以模拟它? // 由于getDefaultRole()可能是被测类的一个方法,我们无法直接模拟。如果它是一个可配置的,我们可以通过设置来固定返回值。 // 假设getDefaultRole()返回一个包含itemName为"defaultRole"的对象,我们在测试中直接使用即可。 // 执行更新 tenantService.updateTenant(tenantDto); // 验证更新租户信息的调用 verify(iTenantInfoDao).updateTenant(any(TenantInfoVO.class)); // 验证更新子产品编码 verify(iTenantUserConfigDao).updateTenantUserSubProductCode(any(TenantUserConfigVO.class)); // 验证删除:应该删除用户100 ArgumentCaptor<List<UserAuthDto>> deleteCaptor = ArgumentCaptor.forClass(List.class); verify(userAuthService).deleteRole(deleteCaptor.capture()); List<UserAuthDto> deletedUsers = deleteCaptor.getValue(); assertEquals(1, deletedUsers.size()); assertEquals(100L, deletedUsers.get(0).getUserId()); // 验证更新:用户101和102?注意:在updateList中,我们返回的是在数据库中存在且也在新列表中的用户,这里只有101存在,所以会更新?但是更新什么? // 实际上,在updateList方法中,我们只是过滤出需要更新的用户ID(即已经存在的用户),然后调用updateTenantUserOwnerFlag方法。 // 注意:在updateList方法中,我们只是返回了那些已经存在的用户ID(这里101已经存在,102不存在?注意102不在已有的列表中,所以不会在updateList中) // 根据我们的模拟数据,existingOwners中只有100和101,所以updateList应该返回101(因为101在新的列表tenantDto.getOwners()中,且已经在数据库中存在) // 因此,我们期望更新101的记录(实际上,更新操作只是更新了lastUpdateBy和roleId?)但是注意,在updateTenantUserOwnerFlag方法中,我们传入的updateList是[101],然后执行更新。 ArgumentCaptor<TenantUserConfigVO> configCaptor = ArgumentCaptor.forClass(TenantUserConfigVO.class); ArgumentCaptor<List> updateListCaptor = ArgumentCaptor.forClass(List.class); verify(iTenantUserConfigDao).updateTenantUserOwnerFlag(configCaptor.capture(), updateListCaptor.capture()); TenantUserConfigVO configVO = configCaptor.getValue(); assertEquals(1L, configVO.getTenantId()); assertEquals(123L, configVO.getLastUpdateBy()); assertEquals("defaultRole", configVO.getRoleId()); assertEquals("Sub1", configVO.getSubProductCode()); List<Long> updatedIds = updateListCaptor.getValue(); assertEquals(1, updatedIds.size()); assertEquals(101L, updatedIds.get(0)); // 验证新增:102不在原有的列表中,所以会新增 ArgumentCaptor<List<TenantUserConfigVO>> createCaptor = ArgumentCaptor.forClass(List.class); verify(iTenantUserConfigDao).createTenantUserInfo(createCaptor.capture()); List<TenantUserConfigVO> createdList = createCaptor.getValue(); assertEquals(1, createdList.size()); assertEquals("102", createdList.get(0).getUserId()); } } } ``` 注意:上面的测试用例中,我们假设`getDefaultRole()`返回一个包含`itemName`为"defaultRole"的对象。如果这个方法是外部依赖,我们需要在测试中模拟它?但是,由于它是被测类的方法,我们无法模拟,所以需要确保在测试环境中它返回一个有效的对象。如果这个方法依赖于其他状态,我们需要设置这些状态。 另外,由于我们使用了`MockedStatic`来模拟`RequestContext`,我们需要在测试方法中使用try-with-resources来确保静态模拟在测试结束后关闭。 由于代码复杂,我们可能需要根据实际情况调整。但是,通过上述测试用例,我们覆盖了`validTenant`中的三个异常情况,以及正常更新中租户管理员变化的场景,从而间接覆盖了私有方法`deleteList`, `updateList`, `addList`和`createTenantUserConfigList`。 最后,我们还需要注意,在测试正常更新时,我们模拟了`RequestContext`,设置了当前用户ID,并验证了在更新租户信息时设置了`lastUpdateBy`。 由于测试代码较长,可能需要根据实际代码调整,例如`TenantDto`、`TenantInfoVO`等类的属性以及DAO方法的参数匹配等。 通过以上测试用例,我们覆盖了`updateTenant`方法的主要分支,包括校验失败和成功更新的情况,并且在成功更新中覆盖了租户管理员变化的处理逻辑。</think>### 使用 Mockito 测试 updateTenant 方法(覆盖私有方法) 下面是为 `updateTenant` 方法设计的单元测试方案,使用 Mockito 框架覆盖所有私有方法逻辑,无需使用 PowerMock: ```java import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class TenantServiceTest { @Mock private ITenantInfoDao iTenantInfoDao; @Mock private ITenantUserConfigDao iTenantUserConfigDao; @Mock private UserAuthService userAuthService; @InjectMocks private TenantService tenantService; // 包含 updateTenant 方法的类 // 测试租户启用时修改名称和子产品 @Test void updateTenant_WhenEnabledAndChangeName_ThrowsException() { // 准备测试数据 TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("NewName"); tenantDto.setSubProductCode("NewSubProduct"); tenantDto.setOwners(Collections.singletonList(100L)); // 模拟数据库返回已启用的租户 TenantInfoVO enabledTenant = new TenantInfoVO(); enabledTenant.setStatus("1"); enabledTenant.setName("OldName"); enabledTenant.setSubProductCode("OldSubProduct"); when(iTenantInfoDao.getTenantInfo(any())).thenReturn(enabledTenant); // 验证异常 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> tenantService.updateTenant(tenantDto)); assertEquals("租户启用中,不能修改租户名称及子产品", exception.getMessage()); } // 测试租户名称冲突 @Test void updateTenant_WhenNameExists_ThrowsException() { // 准备测试数据 TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("ExistingName"); tenantDto.setOwners(Collections.singletonList(100L)); // 模拟校验通过但名称冲突 TenantInfoVO disabledTenant = new TenantInfoVO(); disabledTenant.setStatus("0"); when(iTenantInfoDao.getTenantInfo(any())).thenReturn(disabledTenant); when(iTenantInfoDao.getTenantInfoNot(argThat(dto -> dto.getName() != null))) .thenReturn(new TenantInfoVO()); // 模拟名称已存在 // 验证异常 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> tenantService.updateTenant(tenantDto)); assertEquals("租户名称已存在", exception.getMessage()); } // 测试子产品冲突 @Test void updateTenant_WhenSubProductExists_ThrowsException() { // 准备测试数据 TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setSubProductCode("ExistingSubProduct"); tenantDto.setOwners(Collections.singletonList(100L)); // 模拟校验通过但子产品冲突 TenantInfoVO disabledTenant = new TenantInfoVO(); disabledTenant.setStatus("0"); when(iTenantInfoDao.getTenantInfo(any())).thenReturn(disabledTenant); when(iTenantInfoDao.getTenantInfoNot(argThat(dto -> dto.getSubProductCode() != null))) .thenReturn(new TenantInfoVO()); // 模拟子产品已存在 // 验证异常 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> tenantService.updateTenant(tenantDto)); assertEquals("子产品已存在", exception.getMessage()); } // 测试管理员列表变更(新增/更新/删除) @Test void updateTenant_WhenOwnersChanged_UpdatesCorrectly() { // 准备测试数据 TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setName("ValidName"); tenantDto.setSubProductCode("ValidSubProduct"); tenantDto.setOwners(Arrays.asList(101L, 102L)); // 新管理员列表 // 模拟校验通过 TenantInfoVO disabledTenant = new TenantInfoVO(); disabledTenant.setStatus("0"); when(iTenantInfoDao.getTenantInfo(any())).thenReturn(disabledTenant); when(iTenantInfoDao.getTenantInfoNot(any())).thenReturn(null); // 模拟现有管理员数据 TenantUserConfigVO existingOwner1 = new TenantUserConfigVO(); existingOwner1.setUserId("101"); // 保留的管理员 existingOwner1.setRoleId("defaultRole"); TenantUserConfigVO existingOwner2 = new TenantUserConfigVO(); existingOwner2.setUserId("103"); // 将被删除的管理员 existingOwner2.setRoleId("defaultRole"); when(iTenantUserConfigDao.getAllTenantUserInfo(any())) .thenReturn(Arrays.asList(existingOwner1, existingOwner2)); // 执行方法 tenantService.updateTenant(tenantDto); // 验证删除操作 ArgumentCaptor<List<UserAuthDto>> deleteCaptor = ArgumentCaptor.forClass(List.class); verify(userAuthService).deleteRole(deleteCaptor.capture()); assertEquals(1, deleteCaptor.getValue().size()); assertEquals(103L, deleteCaptor.getValue().get(0).getUserId()); // 验证更新操作 ArgumentCaptor<TenantUserConfigVO> updateConfigCaptor = ArgumentCaptor.forClass(TenantUserConfigVO.class); ArgumentCaptor<List<Long>> updateListCaptor = ArgumentCaptor.forClass(List.class); verify(iTenantUserConfigDao).updateTenantUserOwnerFlag( updateConfigCaptor.capture(), updateListCaptor.capture() ); assertEquals(1, updateListCaptor.getValue().size()); assertEquals(101L, updateListCaptor.getValue().get(0)); // 验证新增操作 ArgumentCaptor<List<TenantUserConfigVO>> createCaptor = ArgumentCaptor.forClass(List.class); verify(iTenantUserConfigDao).createTenantUserInfo(createCaptor.capture()); assertEquals(1, createCaptor.getValue().size()); assertEquals("102", createCaptor.getValue().get(0).getUserId()); } // 测试无现有管理员时新增所有 @Test void updateTenant_WhenNoExistingOwners_AddsAll() { // 准备测试数据 TenantDto tenantDto = new TenantDto(); tenantDto.setId(1L); tenantDto.setOwners(Arrays.asList(101L, 102L)); // 模拟无现有管理员 when(iTenantUserConfigDao.getAllTenantUserInfo(any())).thenReturn(Collections.emptyList()); // 执行方法 tenantService.updateTenant(tenantDto); // 验证新增操作 ArgumentCaptor<List<TenantUserConfigVO>> createCaptor = ArgumentCaptor.forClass(List.class); verify(iTenantUserConfigDao).createTenantUserInfo(createCaptor.capture()); assertEquals(2, createCaptor.getValue().size()); } } ``` ### 关键测试策略说明 1. **私有方法覆盖策略**: - 通过控制输入参数和模拟依赖行为,间接覆盖私有方法逻辑 - 使用 `ArgumentCaptor` 验证方法间参数传递的正确性 - 设计边界条件触发私有方法中的异常分支 2. **验证点设计**: - 异常校验:覆盖 `validTenant` 中的各种校验失败场景 - 列表操作:验证 `deleteList`/`updateList`/`addList` 计算的正确性 - 数据持久化:检查 DAO 层方法调用参数的正确性 - 业务规则:确保租户状态约束得到遵守 3. **Mockito 高级特性应用**: - `ArgumentMatchers`:灵活匹配各种参数类型 - `ArgumentCaptor`:捕获复杂对象进行深度验证 - `verify` 机制:精确验证方法调用次数和参数 - 链式调用:模拟依赖方法的级联返回值 ### 测试覆盖场景 | 测试场景 | 覆盖的私有方法 | 验证要点 | |------------------------------|------------------------|------------------------------| | 租户启用时修改名称/子产品 | `validTenant` | 业务规则约束 | | 租户名称冲突 | `validTenant` | 唯一性校验逻辑 | | 子产品冲突 | `validTenant` | 资源冲突处理 | | 管理员列表变更 | `deleteList`/`updateList`/`addList` | 集合操作逻辑 | | 无现有管理员 | `addList` | 边界条件处理 | | 完整更新流程 | `createTenantUserConfigList` | 对象构建和持久化 | ### 注意事项 1. **测试数据构建**: ```java // 示例:构建复杂的嵌套对象 TenantDto dto = new TenantDto(); dto.setOwners(Arrays.asList(101L, 102L, 103L)); dto.setSubProductCode("P123"); // 构建模拟的数据库返回对象 TenantInfoVO dbResponse = new TenantInfoVO(); dbResponse.setStatus("0"); dbResponse.setSubProductCode("P999"); ``` 2. **依赖行为模拟**: ```java // 模拟不同条件的数据库响应 when(iTenantInfoDao.getTenantInfo(any())) .thenReturn(disabledTenant) // 首次调用 .thenReturn(null); // 后续调用 // 模拟空集合返回值 when(iTenantUserConfigDao.getAllTenantUserInfo(any())) .thenReturn(Collections.emptyList()); ``` 3. **异步操作验证**: ```java // 验证方法调用顺序 InOrder inOrder = inOrder(iTenantInfoDao, iTenantUserConfigDao); inOrder.verify(iTenantInfoDao).updateTenant(any()); inOrder.verify(iTenantUserConfigDao).updateTenantUserSubProductCode(any()); ``` ### 相关问题 1. 如何验证私有方法中的复杂条件分支? 2. 当依赖静态方法(如 `RequestContext`)时,如何改造代码使其可测试? 3. 对于大型服务类,如何设计分层测试策略? 4. 如何测量单元测试对私有方法的实际覆盖率? 5. 使用 Mockito 测试 void 方法的最佳实践是什么?[^1][^3] [^1]: Java单元测试篇之Mockito+PowerMock [^3]: 使用Junit5+PowerMock+Mockito来编写单元测试的小技巧汇总
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值