junit4整合PowerMockito进行单元测试

2023-10-18 16:33:57 浏览数 (1)

junit4整合PowerMockito进行单元测试

一、介绍

在单元测试中,代码里面往往有一些需要连接数据库、调用第三方远程的代码。

由于没有环境,这些代码的存在,会给单元测试造成影响。

所以我们在单测中,往往会使用mock的方式对这些代码做一个数据的模拟,从而达到对代码进行测试的一个目的。

所以单测需要满足以下几点

  • 可复用:单测代码可以重复执行
  • 无环境:不要依赖数据库,第三方接口等外部的环境依赖
  • 方法级细粒度:单测代码应该针对具体一个方法的测试,
  • 高覆盖率:如果代码中复杂度过高,单测要覆盖到方法中的每一行代码
  • 自动断言:每一段单测代码都应该有自己的断言方法,而不是通过打印再人工查看正确性

所以我们就有了Mockito,它可以模拟对象,模拟对象方法的返回值,来完成mock

本文使用的是PowerMockito,它是由Mockito的基础上开发而来,语法规则基本一致,同时也有一些自己的增强,可以对静态方法,局部变量进行mock

二、初步入门

假设我们有下面这两段代码PowerMockitoServiceImpl.javaPowerMockitoMapper.java

代码语言:javascript复制
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import com.banmoon.test.service.PowerMockitoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    @Autowired
    private PowerMockitoMapper powerMockitoMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insert(PowerMockitoEntity entity) {
        Boolean status = Optional.ofNullable(entity.getValue()).map(a -> Boolean.TRUE).orElse(Boolean.FALSE);
        entity.setStatus(status);
        powerMockitoMapper.insert(entity);
    }

}
代码语言:javascript复制
package com.banmoon.test.mapper;

import com.banmoon.test.entity.PowerMockitoEntity;

public interface PowerMockitoMapper {

    int insert(PowerMockitoEntity entity);
}

还有一段PowerMockitoEntity.java

代码语言:javascript复制
package com.banmoon.test.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName("power_mockito")
public class PowerMockitoEntity {

    @TableId
    private Integer id;

    private String value;

    private Boolean status;

}

上面代码所做的功能就是,插入一个实体至数据库。

在插入前,我们根据entity.value是否有值,给予entity.status的值


故此,上面的代码需要连接数据库,我们在单测时,直接对PowerMockitoMapper进行mock即可

首先,先导入依赖,根据自己的需要进行删减使用

代码语言:javascript复制
<!-- powermock -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4-rule</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-classloading-xstream</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

代码如下

代码语言:javascript复制
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
public class PowerMockitoServiceImplTest {

    @Mock
    private PowerMockitoMapper mockPowerMockitoMapper;

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    /**
     * 有值测试
     */
    @Test
    public void testInsert1() {
        // 设置参数
        final PowerMockitoEntity entity = new PowerMockitoEntity();
        entity.setId(1);
        entity.setValue("有值测试");
        when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

        // 执行测试
        powerMockitoServiceImplUnderTest.insert(entity);

        // 校验结果
        Assert.assertTrue(entity.getStatus());
    }

    /**
     * 无值测试
     */
    @Test
    public void testInsert2() {
        // 设置参数
        final PowerMockitoEntity entity = new PowerMockitoEntity();
        entity.setId(1);
        entity.setValue(null);
        when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

        // 执行测试
        powerMockitoServiceImplUnderTest.insert(entity);

        // 校验结果
        Assert.assertFalse(entity.getStatus());
    }
}

执行结果如下,保证两个测试方法如预期通过即可

三、其他使用

1)如何对无返回值的方法进行断言

假设有一个无返回值的方法,我们要针对它进行测试。由于它没有返回值,就没有办法对其返回值进行断言校验。

那么针对这种情况,一个方法,就算是无返回值的情况。内部一定做了一些什么操作。所以我们一般有两种方式

  • 这个方法做了设置某个对象的属性,我们可以校验对象属性是否符合预期
    • 如第二章的初步使用就是如此
  • 如果这个方法执行了某段逻辑分支的代码,我们可以可以校验那段代码是否执行过

PowerMockitoServiceImpl.java上再加一个无返回值的方法

代码语言:javascript复制
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    @Autowired
    private PowerMockitoMapper powerMockitoMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrUpdate(PowerMockitoEntity entity) {
        if (entity.getId() == null) {
            powerMockitoMapper.insert(entity);
        } else {
            powerMockitoMapper.updateById(entity);
        }
    }
}

PowerMockitoMapper.java如下

代码语言:javascript复制
package com.banmoon.test.mapper;

import com.banmoon.test.entity.PowerMockitoEntity;

public interface PowerMockitoMapper {

    int insert(PowerMockitoEntity entity);

    int updateById(PowerMockitoEntity entity);
}

那么我们可以这样

代码语言:javascript复制
package com.banmoon.test.service.impl;

import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
public class NoneReturnTest {

    @Mock
    private PowerMockitoMapper mockPowerMockitoMapper;

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    /**
     * 有ID测试
     */
    @Test
    public void test1() {
        // 设置参数
        final PowerMockitoEntity entity = new PowerMockitoEntity();
        entity.setId(1);
        entity.setValue("测试");
        when(mockPowerMockitoMapper.updateById(entity)).thenReturn(1);

        // 执行测试
        powerMockitoServiceImplUnderTest.saveOrUpdate(entity);

        // 校验结果
        Mockito.verify(mockPowerMockitoMapper).updateById(entity);
    }

    /**
     * 没有ID测试
     */
    @Test
    public void test2() {
        // 设置参数
        final PowerMockitoEntity entity = new PowerMockitoEntity();
        entity.setId(null);
        entity.setValue("测试");
        when(mockPowerMockitoMapper.insert(entity)).thenReturn(1);

        // 执行测试
        powerMockitoServiceImplUnderTest.saveOrUpdate(entity);

        // 校验结果
        Mockito.verify(mockPowerMockitoMapper).insert(entity);
    }
}

2)对属局部对象进行mock并设置

如果一个方法中,有一个自己实例化的一个局部变量,那么我们该如何对其进行mock呢?

例如下面这个方法,有一个自己的局部变量tuple,并返回了这个局部变量的数量

代码语言:javascript复制
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;

import java.io.File;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    @Override
    public long localVariable() {
        File file = new File("E://abc.txt");
        return file.length();
    }

}

我们只需要这样进行,即可以完成对局部变量的mock

代码语言:javascript复制
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.File;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class LocalVariableTest {

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    @Test
    public void localVariableTest() throws Exception {
        // 设置参数
        File file = PowerMockito.mock(File.class);

        // mock
        PowerMockito.whenNew(File.class)
                .withAnyArguments()
                .thenReturn(file);
        PowerMockito.when(file.length()).thenReturn(2L);

        // 执行测试
        long i = powerMockitoServiceImplUnderTest.localVariable();

        // 校验结果
        Assert.assertEquals(2L, i);
    }

}

3)对静态方法mock

如何对静态方法的返回值进行mock

先在PowerMockitoServiceImpl.java添加一个静态方法,其中发现HttpUtil.get()是一个静态方法

代码语言:javascript复制
@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    @Autowired
    private PowerMockitoMapper powerMockitoMapper;

    @Override
    public int syncPowerMockitoEntity() {
        String result = HttpUtil.get("url");
        if (CharSequenceUtil.isNotBlank(result)) {
            List<JSONObject> list = JSON.parseObject(result, List.class);
            int i = 0;
            for (JSONObject json : list) {
                PowerMockitoEntity entity = json.toJavaObject(PowerMockitoEntity.class);
                i  = powerMockitoMapper.insert(entity);
            }
            return i;
        } else {
            throw new BanmoonException(1001, "同步出现异常");
        }
    }

}

针对上面的方法,我们可以这样进行mock,注意@PrepareForTest注解,一定要写上,改变了其中的字节码

代码语言:javascript复制
package com.banmoon.test.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.banmoon.test.entity.PowerMockitoEntity;
import com.banmoon.test.mapper.PowerMockitoMapper;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest(HttpUtil.class)
public class StaticMethodTest {

    @Mock
    private PowerMockitoMapper mockPowerMockitoMapper;

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    /**
     * 静态方法mock
     */
    @Test
    public void test() {
        // 设置参数
        final PowerMockitoEntity entity1 = new PowerMockitoEntity();
        entity1.setId(1);
        entity1.setValue("测试1");
        final PowerMockitoEntity entity2 = new PowerMockitoEntity();
        entity2.setId(2);
        entity2.setValue("测试2");
        List<PowerMockitoEntity> list = CollUtil.newArrayList(entity1, entity2);

        // mock
        PowerMockito.mockStatic(HttpUtil.class);
        when(HttpUtil.get(any())).thenReturn(JSON.toJSONString(list));
        when(mockPowerMockitoMapper.insert(any())).thenReturn(1);

        // 执行测试
        int i = powerMockitoServiceImplUnderTest.syncPowerMockitoEntity();

        // 校验结果
        Assert.assertEquals(2, i);
    }
}

4)mock final修饰的类和方法

首先我们先写一个工具类,这个工具类是final修饰的,里面的方法也是final

代码语言:javascript复制
package com.banmoon.util;

import cn.hutool.core.util.RandomUtil;

public final class PowerMockitoUtil {

    public final String finalMethod(int length) {
        return RandomUtil.randomString(length);
    }

}

单测这个方法

代码语言:javascript复制
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import com.banmoon.util.PowerMockitoUtil;
import org.springframework.stereotype.Service;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    private final PowerMockitoUtil powerMockitoUtil = new PowerMockitoUtil(10);

    @Override
    public int finalMethod() {
        String method = powerMockitoUtil.finalMethod();
        return method.length();
    }

}

测试类

代码语言:javascript复制
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import com.banmoon.util.PowerMockitoUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.lang.reflect.Field;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class, PowerMockitoUtil.class})
public class FinalMethodTest {

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    @Test
    public void finalMethodTest() throws Exception {
        // 设置参数
        PowerMockitoUtil util = PowerMockito.mock(PowerMockitoUtil.class);

        // mock
        Field field = PowerMockito.field(PowerMockitoServiceImpl.class, "powerMockitoUtil");
        field.set(powerMockitoServiceImplUnderTest, util);
        PowerMockito.when(util.finalMethod()).thenReturn("abcde");

        // 执行测试
        int i = powerMockitoServiceImplUnderTest.finalMethod();

        // 校验结果
        Assert.assertEquals(5, i);
    }

}

5)异常的情况

有些时候,代码是会发生异常的,那么在单测的环境下,我们需要判断这些异常是什么,是不是符合预期

如下这个方法,我们只需要传个null,就会发生NullPointException的异常

代码语言:javascript复制
package com.banmoon.service.impl;

import com.banmoon.service.PowerMockitoService;
import org.springframework.stereotype.Service;

@Service
public class PowerMockitoServiceImpl implements PowerMockitoService {

    @Override
    public int exceptionMethod(String name) {
        return name.length();
    }

}

测试用例

代码语言:javascript复制
package com.banmoon.test.powerMockitoTest;

import com.banmoon.service.impl.PowerMockitoServiceImpl;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockitoServiceImpl.class})
public class ExceptionMethodTest {

    @InjectMocks
    private PowerMockitoServiceImpl powerMockitoServiceImplUnderTest;

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void exceptionMethodTest() throws Exception {
        // 校验结果
        Assert.assertThrows(NullPointerException.class, () -> {
            // 执行测试
            int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
        });
    }

    @Test
    public void exceptionMethodTest2() throws Exception {
        // 校验结果
        thrown.expect(NullPointerException.class);

        // 执行测试
        int i = powerMockitoServiceImplUnderTest.exceptionMethod(null);
    }

}

四、最后

推荐一个很好用的IDEA插件,这个插件可以快速生成单元测试代码

squaretest

我是半月,你我一同共勉!!!

0 人点赞