- 1、简介
- 2、常用注解
- 3、最佳实践
3.1、maven依赖- 3.2、Mock静态方法
- 3.4、被Mock的类没有无参构造函数
- 3.5、对private方法进行单测
- 3.6、仅在指定条件下mock,其他条件执行原逻辑
- 3.7、直接在测试方法的参数列表里注入mock对象
- 3.8、@Mocked和@Injectable的区别
- 3.9、模拟返回值类型为void的方法
- 3.10、模拟抛出Exception
- 3.11、当在spring环境中Autowired一个List变量时,该如何mock
- 3.12、mock代码块和静态代码块
- 3.13、使用Verifications进行验证
- 3.14 使用Delegate委托来mock
- 3.15、使用any参数
- 3.16、使用with方法
1、简介
单元测试(unit testing)
是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,Java里单元指一个类。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
模拟测试(mock testing)
就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
JUnit
是一个Java语言的单元测试框架。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。
JMockit
JMockit 是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。所以他能解决当测试的代码包含了一些静态方法,未实现方法,未实现接口的问题。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。
Jmockit中文网:http://jmockit.cn/
Jmockit官网:http://jmockit.github.io/
2、常用注解
@Mocked
@Mocked不仅能修饰一个类,也能修饰接口。@Mocked修饰的普通类/接口/抽象类,是告诉JMockit,帮我生成一个Mocked对象,这个对象的方法(包含静态方法)都会返回return type的默认值,比如return type是int,则返回0,return type 是String,则返回null。
使用@Mocked时需要注意,如果是定义为测试类的成员变量,则被mock类的所有实例都是mock生成的,如下:
public class HelloJMockitTest {
@Tested
HelloJMockit helloJMockit;
//mock成员变量,Something类的所有实例均由mock生成
@Mocked
Something something;
@Test
public void sayHelloTest(){
String msg = helloJMockit.sayHello();
}
}
如果是mocked方法参数,则只在该测试内的实例是mock生成的,如下:
public class HelloJMockitTest {
@Tested
HelloJMockit helloJMockit;
@Test
public void sayHelloTest(@Mocked Something something){//作为方法参数,仅在该方法内是由mock生成实例
something.doSomething();
String msg = helloJMockit.sayHello();
}
}
@Tested & @Injectable
@Injectable 也是告诉 JMockit生成一个Mocked对象,但@Injectable只是针对其修饰的实例,而@Mocked是针对其修饰类的所有实例。此外,@Injectable对类的静态方法,构造函数没有影响。因为它只影响某一个实例。
@Tested修饰的类,表示是我们要测试对象,如果该对象没有赋值,JMockit会去实例化它。该注解不能实例化接口。
@Tested & @Injectable通常搭配使用。若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。
除了构造函数的注入,JMockit还会通过属性查找的方式,把@Injectable对象注入到@Tested对象中。注入的匹配规则:先类型,再名称(构造函数参数名,类的属性名)。若找到多个可以注入的@Injectable,则选择最优先定义的@Injectable对象。当然,我们的测试程序要尽量避免这种情况出现。因为给哪个测试属性/测试参数加@Injectable,是人为控制的。
@Capturing
@Capturing主要用于子类/实现类的Mock, 我们只知道父类或接口时,但我们需要控制它所有子类的行为时,子类可能有多个实现(可能有人工写的,也可能是AOP代理自动生成时)。就用@Capturing。
MockUp & @Mock
MockUp & @Mock比较适合于一个项目中,用于对一些通用类的Mock,以减少大量重复的new Exceptations{{}}代码。
在实际Mock场景中,我们需要灵活运用JMockit其它的Mock API。让我们的Mock程序简单,高效。
一个类有多个实例,但只对其中某1个实例进行mock的场景是MockUp & @Mock做不到的,这种时候就需要上述的@Capturing注解了。
Expectations
Expectations的作用主要是用于录制。即录制类/对象的调用,返回值是什么。主要有两种使用方式:
a.通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制;
b.通过构建函数注入类/对象来录制.
Verifications
Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。
通常在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications() {{}}验证代码块。除非,你的测试程序关心类的某个方法有没有调用,调用多少次,你可以使用new Verifications() {{}}验证代码块。
3、最佳实践
3.1、maven依赖
<!-- 引入dependency -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.49</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 同时引入plugin -->
<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=coverage
</argLine>
<disableXmlReport>true</disableXmlReport>
</configuration>
</plugin>
3.2、Mock静态方法
//需要mock的静态方法
public class StaticTarget {
public static int m1() {
return 1;
}
public static String m2(String s) {
return 2;
}
}
public class RunState {
@Before
public void stepUp(){
//打桩需要被mock的静态方法
new MockUp<StaticTarget>() {
//被mock的静态方法,可以自己定义返回结果
@Mock
public String m2(String s) {
return "TEST";
}
@Mock
public int m1() {
return 100;
}
};
}
//单元测试中调用
@Test //import org.junit.jupiter.api.Test;
public void test() {
//调用静态方法
int i = StaticTarget.m1();
//判断返回值结果
Assert.assertEquals(100, i));
//调用静态方法
String s = StaticTarget.m2();
//判断返回值结果
Assert.assertEquals("ATY", s);
}
}
3.3、Mock普通方法和私有方法
//需要mock的普通方法
public class CommonTarget {
public int m3() {
return 1;
}
public void m4(String s) {
int i = 1 + 1;
}
}
public class RunState {
//打桩需要被mock的静态方法
@Before
public void stepUp(){
//打桩需要mock的方法所属的类
new MockUp<CommonTarget>() {
@Mock
public String m3(String s) {
return "TEST";
}
@Mock
public int m4() {
return 100;
}
};
}
//单元测试中调用
@Test
public void test() {
CommonTarget commonTarget = new CommonTarget();
Assert.assertEquals("ATY", commonTarget.m3());
Assert.assertEquals(100, commonTarget.m4());
}
}
tips:private方法不能被mock。在Jmockit的早期版本里,提供了反射的工具类来mock私有方法,但是在1.36版本之后,就移除了该功能,并且作者没有提供替代的方案。
3.4、被Mock的类没有无参构造函数
若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。
/**
● 待测试的类
*/
public class HelloJMockit {
private static Locale locale;
private int num=0;
//不含有无参构造函数
HelloJMockit(int num){
this.num = num;
}
public String sayHello() {
locale = Locale.getDefault();
if (locale.equals(Locale.CHINA)) {
// 在中国,就说中文
return "你好,JMockit!";
} else {
// 在其它国家,就说英文
return "Hello,JMockit!";
}
}
}
public class HelloJMockitTest {
@Tested
HelloJMockit helloJMockit;
@Test
public void sayHelloTest(@Injectable("123")int num){
String msg = helloJMockit.sayHello();
}
}
3.5、对private方法进行单测
使用JMockit时,private方法不能被mock,1.36之前的版本可以mock私有方法,但是在之后的版本里,作者移除了该功能,并且没有给出相应的替代方案。
当需要对某个类中的private方法进行单元测试时,无法直接通过普通方式进行调用,这里推荐使用反射进行调用测试。
public class PrivateMethodDemo {
private String saySomething(String something){
System.out.println(something);
return something+" is said!";
}
}
import mockit.Injectable;
import mockit.Tested;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class PrivateMethodDemoTest {
@Tested
PrivateMethodDemo privateMethodDemo;
@Test
public void saySomethingTest(@Injectable("hello")String something) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = PrivateMethodDemo.class.getDeclaredMethod("saySomething",String.class);
method.setAccessible(true);
Assert.assertEquals(something+" is said!",method.invoke(privateMethodDemo,something));
}
}
3.6、仅在指定条件下mock,其他条件执行原逻辑
在某些场景下,我们需要仅在某种条件下执行mock逻辑,其他条件执行原逻辑,这里以平台常用的SPI工具类FactoriesLoader举例。
在待测试类中,通过spi机制来获取一个数据库连接管理工厂的实例,如果不对其进行mock,会抛出空指针异常;
待模拟方法如下:
public class FactoriesLoader{
//...some code...
public static <T> T getDefaultFactory(Class<T> factoryClass) {
SPI spi = factoryClass.getAnnotation(SPI.class);
if (spi == null)
return null;
String id = spi.defaultId();
if (id != null) {
Map<String, T> factoryInstances = getFactories(factoryClass);
return factoryInstances.get(id);
}
return null;
}
//...some code...
}
先创建一个工厂类,然后通过mock生成该类的实例对象。
//interface也可以被@Mocked
public interface DBConnectionManagerFactory {
public DBConnectionManager getDBConnectionManager();
}
然后模拟SPI工具类的方法,由于该方法在执行过程中,还有其他地方也要调用,比如获取日志输出对象,所以在模拟的时候,要求只有当此处调用该方法时,才执行mock逻辑,否则应执行原逻辑。测试代码如下:
public class EdspTransactionManagerTest {
//待测试类
@Tested
EdspTransactionManager edspTransactionManager;
//模拟的工厂类
@Mocked
DBConnectionManagerFactory dbConnectionManagerFactory;
@Test
public void getDBConnectionEnableTest() throws NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
//模拟SPI工具类
new MockUp<FactoriesLoader>() {
@Mock
public <T> T getDefaultFactory(Invocation invocation, Class<T> factoryClass) {//factoryClass是原方法的参数,invocation是JMcokit的参数
//只有满足条件,才执行模拟逻辑
if (factoryClass == DBConnectionManagerFactory.class) {
return (T) dbConnectionManagerFactory;
}
//否则执行原逻辑
return invocation.proceed(factoryClass);
}
};
}
}
3.7、直接在测试方法的参数列表里注入mock对象
在测试方法的参数列表里加入参数,这在以往的印象中是不行的,但是Jmockit允许我们这样做,使用@Mocked和@Injectable注解即可实现。如下:
public class InjectableDemoTest {
@Tested
InjectableDemo injectableDemoTested;
@Test
public void doSomethingWhitParamTest(@Injectable User user, @Mocked JobEntity jobEntity){
new Expectations(){
{
user.getUsername();
result = "Davi";
}
{
jobEntity.getId();
result = 1;
}
};
String msg = injectableDemoTested.doSomethingWhitParam(user);
Assert.assertEquals("hello,Davi",msg);
Assert.assertEquals(1,jobEntity.getId());
}
}
此处有个需要注意的点,使用@Injectable时,如果不存在@Tested,那么Injectable的对象不会被mock,会为null。
3.8、@Mocked和@Injectable的区别
使用@Mocked注解修饰一个变量实例,则该实例所属的类的所有实例都会被mock,包括类的静态方法、构造函数都会被mock;
使用@Injectable注解修饰一个变量实例,只有该变量实例会被mock,该变量实例所属类的其他实例不受影响,类的静态方法、构造函数也不受影响。
@Mocked注解可以mock接口、抽象类、普通类;而@Injectable注解只能mock普通类
如下:
@Test
public void showDifferenceTest(@Injectable User user1, @Mocked JobEntity jobEntity1){
new Expectations(){
{
user1.getUsername();
result = "张三";
}
{
jobEntity1.getId();
result = 1;
}
};
User user2 = new User();//重新new的一个对象
user2.setUsername("李四");
System.out.println("user1:"+user1.getUsername());
System.out.println("user2:"+user2.getUsername());
JobEntity jobEntity2 = new JobEntity();//重新new的一个对象
jobEntity2.setId(2);
System.out.println("jobEntity1:"+jobEntity1.getId());
System.out.println("jobEntity2:"+jobEntity2.getId());
}
//控制台输出:
//user1:张三
//user2:李四
//jobEntity1:1
//jobEntity2:1
3.9、模拟返回值类型为void的方法
返回值类型为void的方法,不能使用Expectation来模拟,可以通过MockUp来模拟如下:
public class VoidMethodDemo {
//要被模拟的void类型方法
public void doSomething(){
System.out.println("do something with void return type");
}
}
public class VoidMethodDemoTest {
@Tested
VoidMethodDemo voidMethodDemo;
@Test
public void doSomethingTest(){
new MockUp<VoidMethodDemo>(){
@Mock
public void doSomething(){
System.out.println("had been mocked!");
}
};
voidMethodDemo.doSomething();
}
}
3.10、模拟抛出Exception
当需要测试一些异常场景时,需要模拟某个方法运行时抛出Exception。如下:
public class ExceptioneDemoTest {
@Tested
User user;
/**
* 使用result返回一个exception实例
*/
@Test
public void doSomethingTest2(){
new Expectations(user){
{
user.getUsername();
result = new RuntimeException();
}
};
Assert.assertThrows(RuntimeException.class,()->{user.getUsername();});
}
/**
* 使用MockUp&Mock直接mock原方法抛出异常
*/
@Test
public void exceptionTest(){
new MockUp<User>(){
@Mock
public int getId() {
throw new RuntimeException();
}
};
Assert.assertThrows(RuntimeException.class,()->{user.getId();});
}
}
3.11、当在spring环境中Autowired一个List变量时,该如何mock
在spring中,容器中的bean都是单例bean,而且并不会有List类型在容器中,但是当容器中有多个同类型的bean时,spring自动注入@Autowired就支持用list来接收。JMockit在这方面也对spring做了支持。代码如下:
public class ListFieldDemo {
//如果没有@AutoWired注解,则mock不生效
@Autowired
private List<User> userList;
public List<User> getUserList(){
return userList;
}
}
public class ListFieldDemoTest {
@Tested
ListFieldDemo listFieldDemo;
@Injectable
User user1;
@Injectable
User user2;
@Test
public void getUserListTest(){
List<User> userList = listFieldDemo.getUserList();
Assert.assertNotNull(userList);
System.out.println(userList.size());//size值是2,jmockit会像spring一样自动把user1和user2注入到userList中
}
}
3.12、mock代码块和静态代码块
public class StaticCodeBlockDemo {
{
System.out.println("代码块执行了");
}
static {
System.out.println("静态代码块执行了");
}
public void sayHello(){
System.out.println("hello");
}
}
public class StaticCodeBlockDemoTest {
@Tested
StaticCodeBlockDemo staticCodeBlockDemo;
@BeforeEach
public void mockStaticCodeBlock(){
new MockUp<StaticCodeBlockDemo>(){
//模拟代码块
@Mock
public void $init(){
System.out.println("代码块被mock了");
}
//模拟静态代码块
@Mock
public void $clinit(){
System.out.println("静态代码块被mock了");
}
};
}
@Test
public void getUserTest(){
staticCodeBlockDemo.sayHello();
}
}
3.13、使用Verifications进行验证
JMockit的Record-Replay-Verify中,使用Expectations进行模拟,使用Verifications进行验证。通过Verifications,可以验证模拟方法被调用的次数,通过VerificationInOrder可以验证模拟方法的调用顺序。代码如下:
public class VerificationsDemo {
public String method1(){
return "method1 ";
}
public String method2(){
return "method2";
}
public String method3(){
return "method3";
}
}
public class VerificationsDemoTest {
@Tested
VerificationsDemo verificationsDemo;
/**
* 验证调用次数
*/
@Test
public void verficationDemo(){
//Record
new Expectations(verificationsDemo){
{
verificationsDemo.method1();
result = "mocked";
}
};
//Replay
System.out.println(verificationsDemo.method1());
//System.out.println(verificationsDemo.method1());
//System.out.println(verificationsDemo.method1());
//Verify
new Verifications(){
{
verificationsDemo.method1();
times = 3;
}
//{
// verificationsDemo.method1();
// maxTimes = 2;
//}
//{
// verificationsDemo.method1();
// minTimes = 2;
//}
};
}
/**
* 验证调用顺序
*/
@Test
public void verificationInOrderTest(){
new Expectations(verificationsDemo){
{
verificationsDemo.method1();
result = "method1 mocked";
}
{
verificationsDemo.method2();
result = "method2 mocked";
}
{
verificationsDemo.method3();
result = "method3 mocked";
}
};
System.out.println(verificationsDemo.method1());
System.out.println(verificationsDemo.method2());
System.out.println(verificationsDemo.method3());
new VerificationsInOrder(){
//验证调用顺序
{
verificationsDemo.method1();
verificationsDemo.method2();
verificationsDemo.method3();
}
};
}
}
3.14 使用Delegate委托来mock
Delegate类可以起到一个委托的效果,将原方法的逻辑委托给一个自定义方法,从而达到mock的效果。
定义一个DelegateService类:
public class DelegateService {
//被委托mock的方法
public int intReturningMethod(String arg1, int arg2){
return 1;
}
}
定义一个测试类,在其中调用上面的委托方法:
public class DelegateDemo {
public int testService(){
DelegateService delegateService = new DelegateService();
return delegateService.intReturningMethod("hello",2);
}
}
测试代码如下:
public class DelegateDemoTest {
@Tested
DelegateDemo delegateDemo;
//使用@Mocked修饰的类的所有实例都会走mock逻辑
@Mocked
DelegateService delegateService;
@Test
public void testServiceTest(){
new Expectations() {{
delegateService.intReturningMethod(anyString,anyInt );
result = new Delegate() {
int aDelegateMethod(String s,int i ) {
System.out.println( "代理方法执行了,原入参:arg1:"+s+" arg2:"+i );
return 100;
}
};
}};
System.out.println( delegateDemo.testService() );
}
}
测试结果:
3.15、使用any参数
anyString、anyInt、anyLong等。。。。
“any”可以匹配Object类型的参数,也可以强转它,比如:”(User)any”可以得到一个User对象示例
3.16、使用with方法
@Test
void someTestMethod(@Mocked DependencyAbc abc) {
DataItem item = new DataItem(...);
new Expectations() {{
// 只要第二个参数不为null就可以匹配上
abc.voidMethod("str", (List<?>) withNotNull());
result = 1000;
// 第一个参数要和item是同一个对象实例
// 第二个参数要包含"xyz"字符串
// 同时满足以上两个条件才能匹配上
abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
result = 100;
}};
cut.doSomething(item);
new Verifications() {{
// withAny(T) :只要参数类型为T就会匹配上
abc.anotherVoidMethod(withAny(1L));
}};
}