单元测试与Mock

发布时间 2023-07-14 17:43:22作者: ~鲨鱼辣椒~

JUnit 4

以下是JUnit 4中一些常用的类和注解的相关API:

  1. 注解:
    • @Test:标记测试方法。
    • @Before:在每个测试方法之前执行的方法。
    • @After:在每个测试方法之后执行的方法。
    • @BeforeClass:在整个测试类之前执行的静态方法。
    • @AfterClass:在整个测试类之后执行的静态方法。
    • @RunWith:指定自定义的测试运行器(Runner)。
    • @Ignore:忽略某个测试方法或测试类。
  2. 断言(Assert):
    • Assert.assertEquals(expected, actual):判断两个值是否相等。
    • Assert.assertTrue(condition):判断给定条件是否为真。
    • Assert.assertFalse(condition):判断给定条件是否为假。
    • Assert.assertNull(actual):判断给定对象引用是否为空。
    • Assert.assertNotNull(actual):判断给定对象引用是否不为空。
  3. 测试运行器(Runner):
    • BlockJUnit4ClassRunner:默认的JUnit 4运行器,用于执行测试类。
    • Parameterized:用于执行参数化测试。
    • Categories:用于按照测试类或测试方法分类执行。

JUnit 5

JUnit 5JUnit 4是两个不同版本的Java单元测试框架,它们有一些区别和改进。

以下是一些主要的区别和特性:

  1. 注解方式: JUnit 4使用@Test注解来标记测试方法,而JUnit 5使用@org.junit.jupiter.api.Test注解。JUnit 5还引入了其他新的注解,如@BeforeEach@AfterEach@BeforeAll@AfterAll等,用于在测试方法执行前后进行设置和清理操作。
  2. 扩展模型: JUnit 4使用@RunWith注解来指定运行器(Runner),而JUnit 5引入了更灵活的扩展模型,并使用@ExtendWith注解来应用扩展(如Mockito、Spring等)。这使得JUnit 5更加可扩展和定制化。
  3. 参数化测试: JUnit 5引入了参数化测试的功能,可以使用@ParameterizedTest注解来定义参数化测试方法,并通过提供不同的测试参数执行多次测试。
  4. 断言方式: JUnit 4使用Assert类中的静态方法来进行断言,例如Assert.assertEquals()。JUnit 5将断言功能移到了org.junit.jupiter.api.Assertions类中,提供了更多的断言方法,例如assertAll()assertThrows()assertTimeout()等,以及优化的错误消息输出。
  5. 并发测试: JUnit 5提供了一些新的注解和工具类,用于编写并发测试。例如,@RepeatedTest注解可以重复执行测试,Assertions.assertTimeoutPreemptively()方法可以设置超时时间来进行并发测试。
  6. 动态测试: JUnit 5引入了动态测试的功能,允许在运行时生成和执行测试。这对于需要根据特定条件生成一组测试用例或在运行时确定测试参数的情况非常有用。

选择使用哪个版本取决于你的项目需求、团队偏好以及与其他框架的集成性等因素。但在JUnit 5中有很多新的功能和改进。如果你要开始一个新的项目或者对现有项目进行维护,推荐使用JUnit 5来获得更好的支持和功能。

Mockito

在Java项目中,一般的项目依赖比较多,单纯靠JUnit很难完成单元测试的编写,为了让单元测试的编写更加灵活高效,各种Mock框架应运而生,其中最为经典的非Mockito莫属。

Mockito是一个常用的Java模拟框架,用于在单元测试中创建和管理模拟对象(mocks)。下面是一些Mockito常用的API:

  1. 创建模拟对象:
    • mock(Class<T> classToMock):创建指定类或接口的模拟对象。
    • @Mock注解:在测试类中使用该注解标注字段,可以自动创建模拟对象。
  2. 设置模拟对象行为和返回值:
    • when(mock.method()).thenReturn(value):配置模拟对象方法调用时的返回值。
    • doReturn(value).when(mock).method():与上述方式功能相同,但使用不同的语法。
    • when(mock.method()).thenThrow(exception):配置模拟对象方法调用时抛出异常。
    • doThrow(exception).when(mock).method():与上述方式功能相同,但使用不同的语法。
  3. 验证模拟对象的方法调用:
    • verify(mock).method():验证模拟对象的方法是否被调用。
    • verify(mock, times(n)).method():验证模拟对象的方法被调用了指定次数。
    • verify(mock, never()).method():验证模拟对象的方法从未被调用。
    • verify(mock, atLeastOnce()).method():验证模拟对象的方法至少被调用一次。
    • verifyNoMoreInteractions(mock):验证模拟对象除了已验证的方法外没有其他交互。
  4. 模拟对象参数匹配:
    • any(Type class):匹配任意类型的参数。
    • eq(value):与指定值相等的参数。
    • anyInt()anyString()anyList()等:用于匹配特定类型的参数。
    • argThat(Matcher<T> matcher):使用自定义的参数匹配器。
  5. 重置模拟对象:
    • reset(mock):重置模拟对象的状态,清除之前的行为和交互记录。

以上只是Mockito提供的一些常用API示例,还有更多功能和方法可以根据具体测试需求进行探索和使用。在编写单元测试时,合理使用Mockito的API可以帮助你轻松创建和管理模拟对象,并进行验证和断言操作。

使用

在Maven项目中仅需要引入mockito-core的包,选择合适的版本

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

示例场景

在系统中一个获取所有sku信息的接口,该接口通过调用其他远程服务获取数据,如何编写单元测试验证业务代码的正确呢?

public interface MallItemApiService {

    /**
     * 获取全部的sku信息
     * @return sku信息
     */
    List<MallSkuResponse> getAllSku();
}
package com.meifute.mall.adapter.item;

import com.fasterxml.jackson.core.type.TypeReference;
import com.meifute.mall.adapter.MallSenderHelper;
import com.meifute.mall.adapter.common.ApiResult;
import com.meifute.mall.adapter.item.response.MallSkuResponse;
import com.meifute.mall.adapter.item.response.Sku;
import com.meifute.mall.adapter.util.JSONUtils;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

public class MallItemApiServiceImpl implements MallItemApiService {

    private final MallSenderHelper mallSenderHelper;

    public MallItemApiServiceImpl(MallSenderHelper mallSenderHelper) {
        this.mallSenderHelper = mallSenderHelper;
    }

    /**
     * 获取全部的sku信息
     * @return sku信息
     */
    @Override
    public List<MallSkuResponse> getAllSku() {

        //  所有商品接口
        String result = mallSenderHelper.doGet(ItemPathConstant.FIND_ALL_SKU);
        ApiResult<List<Sku>> apiResult = JSONUtils.read(result, new TypeReference<ApiResult<List<Sku>>>() {
        });
        apiResult.assertSuccess();

        return MallSkuResponse.of(apiResult.getData());
    }
}


@RunWith(MockitoJUnitRunner.class)
public class MallItemApiServiceTest {

    @Mock
    private  MallSenderHelper mallSenderHelper;

    @InjectMocks
    private MallItemApiServiceImpl mallItemApiService;

    @Before
    public void setUp() {
        mallItemApiService = new MallItemApiServiceImpl(mallSenderHelper);
    }

    @Test
    public void getAllSkuTest(){

        // 模拟接口返回的数据
        Unit unit = new Unit();
        unit.setId(1);
        unit.setName("单位");

        Sku sku = new Sku();
        sku.setCode("A0001");
        sku.setId(11L);
        sku.setPrice(BigDecimal.ZERO);
        sku.setStorageUnit(unit);
        sku.setState(GoodsState.ON);

        List<Sku> skus = Collections.singletonList(sku);
        ApiResult<List<Sku>> apiResult = new ApiResult<>();
        apiResult.setData(skus);

        // 模拟doGet方法返回的结果
        String result = JSONUtils.write(apiResult);
        when(mallSenderHelper.doGet(ItemPathConstant.FIND_ALL_SKU)).thenReturn(result);

        // 调用被测方法
        List<MallSkuResponse> response = mallItemApiService.getAllSku();

        // 验证断言
        assertEquals(1, response.size());
        assertEquals("A0001", response.get(0).getSkuCode());

        // 验证调用次数和参数
        verify(mallSenderHelper, times(1)).doGet(ItemPathConstant.FIND_ALL_SKU);
    }

}

通过上面的例子可以看出,在运行单元测试时,并没有远程的调用接口,而是Mock模拟了一个对象的行为,并进行断言验证。这里也符合单元测试仅仅验证业务代码是否正确的目的,并不需要将所有的依赖全部引入。

@mock注解

从上面的例子可以看出,被@mock修饰的变量将会产生一个mock对象,对这个生成的mock对象可以模拟它的行为。

@InjectMocks注解

使用@InjectMocks注解可以方便地自动创建被测类的实例,并自动将模拟对象注入到被测类中所需的依赖属性上。

spy

在Mockito中,spy是一种用于部分模拟(partial mocking)的功能。通过使用spy,可以创建一个真实的对象,并对其部分行为进行模拟。与mock不同,spy保留了对象的原始状态和行为,只对显式指定的方法进行模拟,而其他方法将继续执行真实的逻辑。

// 创建一个真实的对象
List<String> list = new ArrayList<>();

// 将对象转换为spy
List<String> spyList = spy(list);

// 对指定方法进行模拟
when(spyList.size()).thenReturn(10);

// 调用被测对象的方法
int size = spyList.size();  // 返回模拟的值,即10
String element = spyList.get(0);  // 执行真实的方法,返回列表中的第一个元素

// 验证模拟方法的调用次数
verify(spyList).size();

静态方法

在实际工作中,我们经常还会遇到需要对静态方法进行mock的情况,在Mockito 3.4.0前需要借助PowerMock才能实现,3.4.0后开始了对静态方法mock的支持,主要是通过mockito-inie包进行实现。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>3.7.7</version>
    <scope>test</scope>
</dependency>

比如想要对下面这个IpUtil的工具类进行单元测试

import java.net.InetAddress;
import java.net.UnknownHostException;

public class IpUtils {
    
    public static String getIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            return null;
        }
    }
}
@RunWith(MockitoJUnitRunner.class)
public class IpUtilsTest {

    @Test
    public void getIpTest() {

        String target = "192.168.2.1";

        try (MockedStatic<IpUtils> mockStatic = Mockito.mockStatic(IpUtils.class);) {
            mockStatic.when(IpUtils::getIp).thenReturn(target);

            Assert.assertEquals(IpUtils.getIp(), target);
        }
    }
}

PowerMockito

PowerMockito是一个基于Mockito的拓展框架,他提供了更多的操作,包括对私有方法、静态方法和构造函数的模拟和验证。

虽然PowerMockito提供了更多强大的功能来进行单元测试,但也要慎重使用,避免滥用。优先考虑通过公共接口进行测试,只在必要时才使用PowerMockito来处理特定情况下的静态方法、私有方法或构造函数。

以下是一些常用的PowerMockito示例:

  1. Mock静态方法:
    • PowerMockito.mockStatic(ClassToMock.class):模拟静态类。
    • PowerMockito.when(ClassToMock.staticMethod()).thenReturn(value):配置模拟静态方法调用时的返回值。
  2. Mock私有方法:
    • ClassToMock spyObject = PowerMockito.spy(new ClassToMock()):创建目标对象的模拟实例。
    • PowerMockito.doReturn(value).when(spyObject, "privateMethod", arg1, arg2):配置模拟私有方法调用时的返回值。
  3. Mock构造函数:
    • PowerMockito.whenNew(ClassToMock.class).withArguments(args).thenReturn(mockObject):模拟构造函数的调用,并返回模拟对象。
  4. 禁用构造函数:
    • PowerMockito.suppress(PowerMockito.constructor(ClassToSuppress.class)):禁用指定类的构造函数。
  5. 验证静态方法调用:
    • PowerMockito.verifyStatic(ClassToMock.class):验证静态方法被调用。
    • PowerMockito.verifyStatic(times(n)):验证静态方法被调用n次。
  6. 验证私有方法调用:
    • PowerMockito.verifyPrivate(spyObject).invoke("privateMethodName", arg1, arg2):验证私有方法被调用。
  7. 重放所有模拟和验证:
    • PowerMockito.replayAll():重放所有模拟对象。
    • PowerMockito.verifyAll():验证所有模拟对象的方法调用。

使用

需要注意的是PowerMockito有可能会和Mockito产生冲突,需要选择合适的版本

<properties>
    <java.version>1.8</java.version>
    <powermock-version>2.0.2</powermock-version>   
    <mockito-version>2.23.4</mockito-version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <mockito-version>${mockito-version}</mockito-version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>${powermock-version}</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>${powermock-version}</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-core</artifactId>
        <version>${powermock-version}</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4-rule</artifactId>
        <version>${powermock-version}</version>
        <scope>test</scope>
    </dependency> 
</dependencies>

示例场景

如果想要使用PowerMockitoUserManager进行单元测试,代码如下所示:

public final class UserManager {

    private final UserService service;

    public UserManager(UserServiceImpl service) {
        this.service = service;
    }

    public User saveUser(User user) {
        User save = service.save(user);
        return save;
    }

    public String returnName(){
       return getStaticName("ljw1") + " aaa";
    }

    public String httpReturnName(HttpServletRequest request){
        return httpGetStaticName("ljw1", request) + " aaa";
    }

    public static String getStaticName(String name) {
        return "A_" + name;
    }

    public static String httpGetStaticName(String name, HttpServletRequest request) {
        String aaa = request.getParameter("aaa");
        return aaa + "A_" + name;
    } 

    public String getPrivateName(String name) {

        if (publicCheck()){
            return "public 被mock 了";
        }
        if (check(name)){
            return "private 被mock 了";
        }
        return "A_" + name;
    }

    public boolean publicCheck() {
        return false;
    }

    private boolean check(String name) {
        return false;
    }

    private String say(String content) {
        return "ljw say " + content;
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(UserManager.class)
public class UserManagerTest {
    @Mock
    private UserServiceImpl serviceImpl;

    @InjectMocks
    private UserManager userManager;

    /**
     * mock service的保存方法
     */
    @Test
    public void mockSave() {
        User user1 = new User();
        User user2 = new User();
        user1.setId("1");
        user2.setId("2");
        Mockito.when(serviceImpl.save(user1)).thenReturn(user2); //当调用service的save()时,mock让他返回user2
        User saveUser = userManager.saveUser(user1); //调用
        Mockito.verify(serviceImpl,Mockito.times(1)).save(user1);//verify验证mock次数
        assertEquals(user2, saveUser);//断言是否mock返回的是user2
    }

    /**
     * mock spy public Check方法
     * @throws Exception xx
     */
    @Test
    public void getPrivateNameOfPublicCheckTest() throws Exception {
        UseruserManager spy = PowerMockito.spy(userManager); //监视userManager的publicCheck方法,让他返回true
        Mockito.when(spy.publicCheck()).thenReturn(true);
        String name = spy.getPrivateName("ljw");//执行该方法
        assertEquals("public 被mock 了", name);//验证
    }

    /**
     * mock私有 check 方法
     * @throws Exception xx
     */
    @Test
    public void getPrivateNameOfPrivateCheckTest() throws Exception {
        UseruserManager spy = PowerMockito.spy(userManager);
        PowerMockito.when(spy, "check", any()).thenReturn(true);//私有方法mockito不行了,需要用PowerMock监视spy
        String name = spy.getPrivateName("ljw");
        assertEquals("private 被mock 了", name);
    }

    /**
     * mock 静态方法
     */
    @Test
    public void mockStaticMethod() {
        PowerMockito.mockStatic(UseruserManager.class);//mock静态方法
        when(UseruserManager.getStaticName(any())).thenReturn("hi");
        String staticName = UseruserManager.getStaticName("ljw");//执行
        assertEquals("hi", staticName);//验证
    }

    @Test
    public void mockStaticMethod_2() {
        PowerMockito.mockStatic(UseruserManager.class);
        when(UseruserManager.getStaticName(any())).thenReturn("hi");
        String staticName = userManager.returnName();//通过returnName()调用,看能否被mock
        assertEquals("hi", staticName);
    }

    /**
     * 静态方法传入一个HttpServerletRequest参数 + 普通方法
     * A(T.class)检查参数T的实例instance,表示它为非null。
     * same(obj)检查参数是否与obj相同,从而arg == obj为true。
     * eq(obj)根据其equals方法检查参数是否等于obj。
     * 如果在不使用匹配器的情况下传递实数值,这也是行为。
     */
    @Test
    public void mockStaticMethod_3() {
        PowerMockito.mockStatic(UseruserManager.class);
        PowerMockito.when(UseruserManager.httpGetStaticName(any(),any())).thenReturn("hi");
        String staticName = userManager.httpReturnName(eq(any()));    //通过returnName()调用,看能否被mock
        assertEquals("hi aaa", staticName);
    }

    /**
     * 测试私有方法一
     * @throws InvocationTargetException xx
     * @throws IllegalAccessException xx
     */
    @Test
    public void testPrivateMethod() throws InvocationTargetException, IllegalAccessException {
        Method method = PowerMockito.method(UseruserManager.class, "say", String.class);
        Object say = method.invoke(userManager, "hi");
        assertEquals("ljw say hi", say);
    }

    /**
     * 测试私有方法二
     * @throws Exception xx
     */
    @Test
    public void testPrivateMethod_2() throws Exception {
        Object say = Whitebox.invokeMethod(userManager, "say", "hi");
        assertEquals("ljw say hi", say);
    }

}

@PrepareForTest

在使用PowerMockito时,为了能够正确地模拟和验证目标类的静态方法、私有方法或构造函数,你需要在测试类上使用@PrepareForTest注解,并指定需要进行操作的类。@PrepareForTest注解告诉PowerMockito需要对指定类进行字节码操作,以便能够模拟和验证该类的静态方法、私有方法或构造函数。

SpringBoot

在SpringBoot项目中,只需要引入spring-boot-starter-test包,其会带入junitmockito-corespring-test包,如果需要对私有方法进行mock,需要额外引入PowerMock

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

spring-boot-test包中对Mock框架进行了一些封装,方便在单元测试和集成测试中使用,总的来说@MockBean@SpyBean主要用在集成测试阶段,但有的时候。

@MockBean

@MockBean@Mock是两个不同的注解,用于在测试中创建模拟对象(mock)。

  1. 作用范围:
    • @MockBean是Spring Boot Test框架提供的注解,用于集成测试中创建模拟对象并将其注入到Spring容器中。
    • @Mock是Mockito框架提供的注解,主要用于独立的单元测试中创建模拟对象。
  2. 依赖关系:
    • @MockBean通常与Spring Boot Test一起使用,它基于Spring应用程序上下文创建和管理模拟对象。它可以与其他Spring功能(如自动装配)无缝集成。
    • @Mock则与Mockito框架一起使用,它是一个独立的Java模拟框架,专注于模拟和验证对象的行为。
  3. 目标对象:
    • @MockBean适用于模拟Spring Bean组件,例如模拟数据库访问、外部服务调用等。
    • @Mock可以用于模拟任何类或接口的实例,不限于Spring Bean组件。
  4. 测试环境:
    • @MockBean通常用于集成测试,需要启动整个Spring应用程序上下文以便正确创建和管理模拟对象。
    • @Mock主要用于独立的单元测试,没有Spring上下文的依赖,可以更轻量地创建和使用模拟对象。

@SpyBean

Mockito中的spy类似,@SpyBean会保留原始对象的状态和行为,并只对你显式指定的方法进行模拟。这使得你可以在集成测试中针对真实的Spring组件进行测试,并对其中的部分方法进行模拟和验证。