Java 单元测试(3)mock进阶 - 静态、final、私有方法mock

前言

上一章讲了Spring-boot的starter test使用mock的方式mockito。但是mockito由于实现方式的原因(动态代理)不能支持静态、final、私有方法的mock。其实还有一种叫native方法,只是一般自己写native方法的地方不多,可能Android系统在这方面使用较多,比如游戏。查询了一些资料与笔者的以往经历,主要使用的有powerMock与jMockit。

1. powerMock

1.1. powerMock官方文档

powerMock在以前使用较多,最近反而使用少了,根本原因是不支持Junit5。官方最新版只支持Junit4,见官方文档

描述的很清楚,支持junit4版本,或者testNG,笔者在maven仓库看到junit5的支持jar,但是没人使用,更恶心的是有个版本号居然不能显示,看起来不是官方推送的。

所以笔者使用testNG来测试

1.2. powerMock demo模拟

pom文件如下,笔者依赖testNG与AssertJ。

		<dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.1.0</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>2.0.7</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-testng</artifactId>
            <version>2.0.7</version>
            <scope>test</scope>
        </dependency>

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

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>

随意写一个方法,包含静态、私有、final方法。如果是final类,方式同final方法。

package com.feng.demo;

public class User {

    private String name;

    public String getResult(String test){
        return test + "\tjunit";
    }

    private String getPrivateName(String test){
        return "123";
    }

    public final String getFinalName(String str) {
        return "1235" + str;
    }

    public static String getStaticName(String string) {
        return "12356" + string;
    }

}

构建test方法,testNG需要继承PowerMockTestCase。

package com.feng.demo;


import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.testng.PowerMockTestCase;
import org.powermock.reflect.Whitebox;
import org.testng.annotations.Test;

import java.lang.reflect.Method;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;

@PrepareForTest(User.class)//final方法准备,final类同理
public class UserTest extends PowerMockTestCase {

    @Mock
    private User user;

    @Test
    public void testTestGetResult() {
    	//由于次案例使用mockito,为powermock扩展,普通mock等同于mockito
        PowerMockito.when(user.getResult(anyString())).thenReturn("powerMock");
        String result = user.getResult("tom");
        assertThat(result).isEqualTo("powerMock");
    }

    @Test
    public void testGetFinalName() {
    	//final方法mock;这里同时mock了参数,跟final无关
        PowerMockito.when(user.getFinalName(argThat(s -> true))).thenReturn("powerMock");
        String result = user.getFinalName("tom");
        assertThat(result).isEqualTo("powerMock");
    }

    @Test
    public void testGetStaticName() {
    	//静态方法mock,先要设置需要mock的静态类
        PowerMockito.mockStatic(User.class);
        PowerMockito.when(User.getStaticName(argThat(s -> true))).thenReturn("powerMock");
        String result = User.getStaticName("tom");
        assertThat(result).isEqualTo("powerMock");
    }

    @Test
    public void testGetPrivateName() throws Exception {
    	//mock私有方法
        PowerMockito.when(user, "getPrivateName", anyString()).thenReturn("powerMock");
        //私有方法实现单元测试,本质是反射调用
        Method method = PowerMockito.method(User.class, "getPrivateName", String.class);
        Object result = method.invoke(user, "12");
        assertThat(result).isEqualTo("powerMock");

        Object say = Whitebox.invokeMethod(user, "getPrivateName", "12");
        assertThat(result).isEqualTo("powerMock");
    }
}

2. JMockit

重点介绍jmockit,功能十分强悍,只是测试代码有点不美观。不知道Spring-Boot推荐mockito而不是jmockit的原因是否是这个。jmockit查资料说支持mock私有方法,但笔者通过1.49版本测试是不支持的。

jmockit的本质Record-Replay-Verification

  1. Record: 录制类/对象的方法调用,在mockito里即为打桩。
  2. Replay: 重放方法,即调用方法。
  3. Verification: 验证。比如验证某个方法有没有被调用,调用多少次。

jmockit更新不是很频繁,最近更新是2019-12的1.49版本,支持junit5,这是jmockit1版本,一直在更新。

而且作者有jmockit2项目不知为啥2017年后不维护了。

2.1. jmockit demo

pom依赖

		<!-- Jmockit -->
        <dependency>
            <groupId>org.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>1.49</version>
            <scope>test</scope>
        </dependency>

        <!-- junit5 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>

继续使用上文的User作为需要测试的类。
这里要注意,仅仅依赖jar在maven的test是不管用的,笔者调试发现JMockit在执行单元测试时没有初始化,甚至笔者@ExtendWith(JMockitExtension.class)注入类都直接报错了。
查询官方文档:jmockit doc

笔者加入插件立马正常了,笔者查询很多博客,都没有这个,但根据笔者实践与官方文档是需要的。

<build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <argLine>
                        -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
                    </argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>

而且官方还给出了单元测试代码覆盖率输出的配置:Activating coverage in a Maven project
其实现在而言用处不大,一般使用jacoco也可以在sonarqube配置。

2.2. @Mocked

@Mocked修饰类或者接口,类或者接口被修饰后,是全局的,以后这个类或者对象的方法调用就会走mocked的实例,不会再执行原来的方法。@Mocked非常霸道,自己new一个对象也不会生效了,全部被Mocked的实例接管。
返回结果jmockit也会处理:

  1. 原始类型(short,int,float,double,long)返回0
  2. String返回null
  3. 其它引用类型,返回这个引用类型的Mocked对象,即将返回的其他对象也Mocked了。

测试一下,jmockit有两种注入方式

模拟单元测试验证正确,

Mocked注解要慎用,一旦Mocked,所有对象全部被Mocked,建议在方法的参数上使用注解,就像笔者上面的示例一样,这样只会接管当前方法有效,同理因为这个原因,使用Mocked可以支持静态方法mock。

2.3. @Injectable

Mocked注解会对所有范围内的类或者实例全部Mocked,如果只想Mocked当前实例,就需要@Injectable注解,由于仅仅Mocked当前实例,所有静态方法失效,new的对象失效。

	@Test
    void mocked(@Injectable User user){
        assertThat(user.getFinalName("sss")).isNull();
        assertThat(user.getResult("sss")).isNull();
        assertThat(User.getStaticName("sss")).isNotNull();
        assertThat(new User().getFinalName("sss")).isNotNull();
    }

2.4. @Test

表示被测试的对象,如果我们没有实例化,JMockit会帮我们实例化。如果带有Injectable注解的属性,JMockit会使用Injectable构造函数实例化,如果没有这种构造函数,这会通过默认构造函数实例化,通过属性注入Injectable注解的属性。etg

public class SendMailService {
    
    public String sendmail(){
        return "OK";
    }
}

@Test类

public class User {

    private SendMailService sendMailService;

    public String send(String str){
        return str + sendMailService.sendmail();
    }
}

开始测试

2.5. @Capturing

@Capturing同样是mock对象,但与@Mocked或者@Injectable是有很大区别的。
@Capturing可以mock接口或者实现类的子类的行为。平时基本上不用,但是在AOP切面的过程可以直接mock接口或者类的子类行为。也就是说除了有@Mocked的能力,还会对动态代理或者自己写的子类mock。etg

public interface AopService {
    String doAop(String param);
}

假设这是一个动态代理的接口:实现类N多,做了一个切面;或者这个接口没有实现,类似mybatis的mapper。这个时候@Capturing就派上用场了。可以模拟子类或者实现类的行为。

class AopServiceTest {

    @Capturing
    AopService aopService;

    @Test
    void doAop() {
        new Expectations(){{
            aopService.doAop(anyString);
            result = "aopMock";
        }};

        assertThat(aopService.doAop("sss")).isEqualTo("aopMock");
    }
}

2.6. Expectations录制,功能类似mockito的打桩

expectations的录制其实有2种方式,可录制静态方法,final方法,但私有方法与native方法不可录制。

  1. @Injectabe,@Mocked,@Capturing配套使用,主要方式,基本都使用这种方式。
  2. 通过构造函数录制

    对象录入正常;但是当笔者将User.class传入构造函数都报错了,推荐使用MockUp替代。

    当笔者使用ArrayList的时候居然不行,估计是需要配合

    笔者查看源码发现,部分mocking并不是所有类或者对象都可以。原来有特殊验证

2.7. MockUp & @Mock

Invalid Class argument for partial mocking (use a MockUp instead)
笔者上一章报了这个错,推荐我们使用MockUp,静态方法,native,final方法,但私有方法不可录制,而且要写很多代码。
测试私有方法mock时,难道1.49版本不允许mock私有方法了?
java.lang.IllegalArgumentException: Unsupported fake for private method
看见有人回复想办法mock私有方法,源码分析是被作者禁了,至于是否有新的方式实现也没看见说明,官方API文档也没发现
在这里插入图片描述

非private方法是可以正常MockUp的。

	@Test
    void doAopMockUp() {
        User user = new User();
        new MockUp<User>(User.class){
            @Mock
            public String getResult(String test){
                return "mock";
            }
        };
        assertThat(user.getResult("111")).isEqualTo("mock");
    }

这种方式很灵活,可以实现自定义部分mock,估计对公共类使用是很好的场景。
但是缺点很明显:

  1. 代码写的很多
  2. 接口有多个实现,只能mock一个实现
  3. 动态实现无法mock,比如mybatis的mapper

2.8. Verifications

Verifications用于验证录制是否执行了,对比上一章的mockito,发现mock的3要素都是不变的。mockito是打桩-执行-验证,jmockit是录制-回放-验证。设计原理差不多,Verifications方面感觉jmockit就比较弱了。

	@Test
    void doAopMockUp() {
        User user = new User();
        new MockUp<User>(User.class){
            @Mock
            public String getResult(String test){
                return "mock";
            }
        };
        assertThat(user.getResult("111")).isEqualTo("mock");

        new Verifications(){
            {
                user.getResult("111");
                times = 1;
            }
        };

2.9. Spring 集成

以Spring Boot为例,pom依赖如下

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.4.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>1.49</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <argLine>
                        -javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
                    </argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>

可以使用@Tested与@Injectable配合Spring的注解同时使用,使用上一章的示例

@SpringBootTest
class DemoServiceImplTest {

    @Tested
    @Autowired
    private DemoService demoService;//这里就可以使用接口类型了,这点jmockit强大

    @Test
    void getMapperData(@Injectable DemoMapper demoMapper) {
        new Expectations(){
            {
                demoMapper.getName(anyString);
                result = "JMockit";
            }
        };

        assertThat(demoService.getMapperData("sss")).isNotBlank().isEqualTo("JMockit");
    }
}

在这里插入图片描述

总结

从功能上讲jmockit是很强大的,但是笔者测试1.49版本无法mock私有方法,powerMock却是可以,但是powerMock不支持junit5。估计Spring Boot官方集成mockito而不是jmockit主要是:

  1. mock静态,final,私有方法不是特别需求
  2. 代码风格,jmockit需要写大量的内部类,内部类代码块
  • 1
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值