Junit+mockito+powermock单元测试初探

2020-12-03
halley.fang

Mockito 是一个流行的 mock 框架,可以与 JUnitTestNG 一起使用。Mockito 允许您创建和配置模拟对象。使用 Mockito 可以极大地简化具有外部依赖项的类的测试开发。

单元测试

白盒测试定义:
  白盒测试又称结构测试,透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试是一种测试用例设计方法,白盒指的是程序的内部结构和运作机制是可见的。

白盒测试的目的:
  通过检查软件内部的逻辑结构,对软件中的逻辑路径进行覆盖测试;在程序不同地方设置检查点,检查程序的状态,以确定实际运行状态与预期状态是否一致。

白盒测试的方法:大致分为静态方法和动态方法两大类。

  1. 静态分析:
      是一种不执行程序而进行测试的技术。静态分析的主要目的是检查软件的表示和描述是否一致,没有冲突或者没有歧义。

  2. 动态分析:
      当软件系统在模拟或真实的环境中执行前、过程中和执行后,对其行为分析。它显示了一个系统在检查状态下是否正确。在动态分析技术中,最重要的技术是路径和分支测试。

说明:本文中不对白盒测试用例设计做详细阐述,后续会在另外的文档中进行详解。

单元测试属于白盒测试范畴,应该尽量消除与其他类或系统的耦合性,即解耦。我们可以用模拟的测试依赖来替换真实依赖,测试依赖主要有以下几类:

  • 一个虚拟对象被传递但从未被使用,即它的方法永远不会被调用。例如,可以使用这样的对象来填充方法的参数列表。

  • 伪对象具有可工作的实现,但通常是简化的。例如,内存数据库用于测试,而不是基于 SQL 的数据库。

  • 桩(stub)类是接口或类的部分实现,目的是在测试期间使用这个桩类的实例。桩通常不会对测试程序之外的任何东西做出响应。桩还可以记录关于调用的信息。

  • 模拟(mock)对象是接口或类的虚拟实现,在其中定义某些方法调用的输出。模拟对象被配置为在测试期间执行特定的行为。他们通常记录与系统的交互,测试可以验证这一点。

测试依赖可以传递给其他被测试的对象。您的测试可以验证类在测试期间的正确反应。例如,您可以验证是否调用了模拟对象上的某些方法。这有助于确保您只在运行测试时测试类,并且您的测试不会受到任何副作用的影响。

模拟(mock)对象通常需要较少的代码进行配置,因此应该首选模拟对象

mock 对象生成

您可以手动(通过代码)创建模拟对象,或者使用模拟框架来模拟这些类。Mock 框架允许您在运行时创建模拟对象并定义它们的行为。模拟对象的经典示例是数据提供程序。在生产中,使用连接到实际数据源的实现。但是对于测试来说,模拟对象模拟数据源并确保测试条件总是相同的。可以将这些模拟对象提供给要测试的类。因此,要测试的类应该避免对外部数据的任何硬依赖。mock 或 mock 框架允许测试预期的与 mock 对象的交互。例如,您可以验证只在模拟对象上调用了某些方法。

使用 Mockito 来模拟对象

Mockito 是一个流行的 mock 框架,可以与 JUnitTestNG 一起使用。Mockito 允许您创建和配置模拟对象。使用 Mockito 可以极大地简化具有外部依赖项的类的测试开发。使用 Mockito 一般过程如下:

  1. 对测试类的依赖进行 Mock

  2. 针对测试类编写测试代码

  3. 对测试内容进行验证

Java 项目使用 Mockito

添加 Maven 依赖

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.3.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>3.3.3</version>
            <scope>test</scope>
        </dependency>

示例参考: 本文中示例git库地址

创建 mock 对象

Mockito提供了几种方法来创建模拟对象:

  1. 使用 static mock()
    示例 Demo1Test.java

    //静态导入
    import static org.mockito.Mockito.*;
    
    public class Demo1Test {
    
        @Test
        public void testMyModel(){
            String name = RandomString.random();
            MyModel myModel = mock(MyModel.class);//直接调用mock
            myModel.setName(name);//执行mock类的一些代码
            verify(myModel).setName(name);//验证方法是在myModel mock上调用的
        }
    }
  2. 使用 @Mock 注解
    示例 Demo2Test.java

    import static org.junit.Assert.assertTrue;
    import static org.mockito.Mockito.*;
    //@RunWith(MockitoJUnitRunner.class)//初始化mock,继承父类MockitoBaseCase
    public class Demo2Test extends MockitoBaseCase {
    
        @Mock
        MyModel myModel;
    
        @InjectMocks
        MyCase1 myCase1;
    
        @Test
        public void testCase1(){
            MyModel result = myCase1.case1();
            assertTrue(!result.getIsBoy());
            verify(myModel).setIsBoy(true);
        }
    }

Static imports
通过添加 org.mockito.Mockito.*; 静态导入,您可以在测试中直接使用诸如 mock() 之类的方法。静态导入允许您调用静态成员,即类的方法和字段,而不指定类。
使用静态导入还可以极大地提高测试代码的可读性。

Junit集成Mokito注解有以下种初始化方式:

  1. @RunWith(MockitoJUnitRunner.class)

  2. MockitoAnnotations.initMocks(this)

  3. @Rule

上面例子中写了 1 初始化mock的方式,方式 2 使用如下:

示例 Demo3Test.java

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;

public class Demo3Test {

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);//其中this就是单元测试所在的类,在initMocks函数中Mockito会根据类中不同的注解(如@Mock, @Spy等)创建不同的mock对象,即初始化工作
    }

    @Mock
    MyModel myModel;

    @InjectMocks
    MyCase1 myCase1;

    @Test
    public void testCase1(){
        MyModel result = myCase1.case1();
        assertTrue(!result.getIsBoy());
        verify(myModel).setIsBoy(true);
    }

}

方式3使用如下:

示例 Demo4Test.java

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;

public class Demo4Test {

    @Mock
    MyModel myModel;

    @InjectMocks
    MyCase1 myCase1;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testCase1(){
        MyModel result = myCase1.case1();
        assertTrue(!result.getIsBoy());
        verify(myModel).setIsBoy(true);
    }

}

配置测试桩

Mockito允许通过API配置它的mock的返回值。未指定的方法调用返回“空”值:

  • null for objects

  • 0 for numbers

  • false for boolean

  • empty collections for collections

when thenReturn 和 when thenThrow

mock可以根据传入方法的参数返回不同的值,when(…​.).thenReturn(…​.) 方法链用于为具有预定义参数的方法调用指定返回值。您还可以使用像 anyStringanyInt 这样的方法来定义依赖于输入类型的返回值。

示例 Demo5Test.java

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
 * when thenReturn 示例
 */
public class Demo5Test extends MockitoBaseCase {

    @Mock
    MyModel myModel;

    @Test
    public void testMyModel(){
        String name = RandomString.random();
        when(myModel.getName()).thenReturn(name);
        assertEquals(myModel.getName(),name);
        verify(myModel).getName();
    }

    /**
     * 测试多个返回
     */
    @Test
    public void testMoreThanOneReturnValue(){
        String name1 = RandomString.random();
        String name2 = RandomString.random();
        when(myModel.getName()).thenReturn(name1).thenReturn(name2);
        assertEquals(myModel.getName(),name1);//第一次调用返回name1
        assertEquals(myModel.getName(),name2);//第二次调用返回name2
        assertEquals(myModel.getName(),name2);//超过定义数则调用返回最后一次的赋值name2
        verify(myModel,times(3)).getName();
    }

    /**
         * 测试抛出异常
         */
        @Test
        public void testThrow(){
            when(myModel.getName()).thenThrow(new RuntimeException());
            try {
                myModel.getName();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                verify(myModel,times(1)).getName();
            }
        }
}

doReturn when 和 doThrow when

doReturn(…).when(…)methodCall 效果类似于 when(…).then return(…),主要使用与以下场景:

  • 对void方法进行打桩

  • 对spy对象进行打桩

  • 对同一个方法多次进行打桩从而在测试过程中改变mock行为

示例 Demo6Test.java

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
 * doReturn when 示例
 */
public class Demo6Test extends MockitoBaseCase {

    @Mock
    MyModel myModel;

    @Spy
    @InjectMocks
    MyCase1 myCase1;

    @Test
    public void testMyCase1(){
        doReturn(myModel).when(myCase1).case1();
        assertEquals(myCase1.case1().getAge(),0);
        verify(myCase1).case1();
    }

    /**
     * 测试抛出异常
     */
    @Test
    public void testThrow(){
        doThrow(new RuntimeException()).when(myCase1).case1();
        try {
            myCase1.case1();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            verify(myCase1,times(1)).case1();
        }
    }
}

用 Spy 包装 Java 对象

@Spyspy() 方法可以用来包装一个真实的对象。除非另外指定,否则每个调用都被委托给对象。

代码参照 [示例Demo6Test.java]

spy 拓展

spy和mock的异同:

  1. 得到的对象同样可以进行“监管”,即验证和打桩。

  2. 如果不对spy对象的methodA打桩,那么调用spy对象的methodA时,会调用真实方法。

  3. 如果不对mock对象的methodA打桩,将doNothing,且返回默认值(null,0,false)。

场景1
public class TestSubject{
   public void methodA(){
      throw new RuntimeException();
   }

   public void methodB(){
      System.out.println("methodB begin");
      methodA();
      methodC()
      System.out.println("methodB end");
   }

   public void methodC(){
      System.out.println("methodC");
   }
}

public class Test{
   //此用例中使用spy的原因是我要测试的是TestSubject的methodB方法,所以调用methodB时必须执行其
   //真实的方法体,methodB会调用methodA,methodA会抛异常,所以要绕过methodA
   @Test
   public void testMethodB(){
      TestSubject t = new TestSubject();
      TestSubject spyT  = Mockito.spy(t);
      //避免调用mehtodB时抛运行时异常。
      doNothing().when(spyT).methodA();
      sptT.methodB();
   }
}
场景2
public class TestSubject{
   public int methodA(){
      //根据某成员变量的值去计算得出一个value,这个过程包含了复杂的逻辑和层层方法嵌套调用
      return value;
   }

   public void methodB(){
      int key = methodA();
      switch(key){
         case 0:
            //do something
         case 1:
            //do something
         case 2:
            //do something
      }
   }

}

public class Test{
   //此用例中使用spy的原因是我要测试的是TestSubject的methodB方法,所以需要
   //调用真实对象的methodB,methodB的输入来自methodA的返回值。但是methodA的计算十分复杂,
   //那么想要methodA返回你想要的值就不那么容易,别人看起来也不直观,不确定methodA否是真的
   //返回0,1,2。那么就可以对methodA打桩,对真实对象打桩,就要用到spy.
   @Test
   public void testMethodB(){
      TestSubject t = new TestSubject();
      TestSubject spyT  = Mockito.spy(t);
      //第一次,第二次,第三次调用methodA时,分别返回0,1,2
      when(spyT.methodA()).thenReturn(0,1,2);
      for(int i=0; i<=2; i++){
         spyT.methodB();
      }
      //assert && verify
   }
}
场景3
public class TestSubject{
   public void methodA(){
      System.out.println("methodA");
   }

   public void methodB(int i){
      int key;
      //根据参数i进行复杂运算,得出结果赋值给key
      switch(key){
         case 0:
            methodA();
         case 1:
            methodC();
         case 2:
            methodD();
      }
   }
   public void methodC(){
      System.out.println("methodC");
   }

   public void methodD(){
      System.out.println("methodD");
   }

}

public class Test{
   //此用例中使用spy的原因是我要测试的是TestSubject的methodB方法,所以需要
   //调用真实对象的methodB,此例中需要verify输入特定的i,是否能分别走进case 0,1,2,
   //methodA,C,D方法体内的东西都没法获取并证明methodA,C,D被调用过。那么就只能verify了,
   //verify只能针对mock对象,其实spy对象,也可以使用verify
   @Test
   public void testMethodB(){
      TestSubject t = new TestSubject();
      TestSubject spyT  = Mockito.spy(t);
      //假定输入1,能让key==0
      spyT.methodB(1);
      //assert && verify
      verify(spyT).methodA();
   }
}
场景4
public class TestSubject{

   public void methodB(TestObject obj, i){
      int key;
      //这方法执行的内容非常必要,所以obj需要真实对象。
      obj.doImportantThing();
      //根据参数i进行复杂运算,得出结果赋值给key
      switch(key){
         case 0:
            LayoutInflater inflater = obj.getLayoutInfalter();
            ViewGroup v = inflater.inflate(R.layout.complex_layout,null,false);
            v.setVisibility(View.GONE);
            //do something can be verify
         case 1:
            methodC();
         case 2:
            methodD();
      }
   }
   public void methodC(){
      System.out.println("methodC");
   }

   public void methodD(){
      System.out.println("methodD");
   }

}

public class TestObject{
   public void doImportantThing(){
      //do something nessisary for TestSubject
   }
   //一个layout文件经常无法inflate出一个ViewGroup,所以很可能你需要该方法返回一个
   //mock对象,然后你可以随心所欲指定inflate出来的ViewGroup对象
   public LayoutInflater getLayoutInfalter(){
      //obtain LayoutInflater
   }
}

public class Test{
   //此用例中使用spy的原因是我要测试的是TestSubject的methodB方法,所以需要
   //调用真实对象的methodB,此例中需要verify输入1后,是否进入case 0;因为
   //TestObject#doImportantThing()中的内容是必须执行的,所以TestObject需要传入的
   //真实对象,但是R.layout.complex_layout太复杂了,里面都是厂商定制的控件,无法加载,
   //进入case 0后,将无法正常跑下去,那么可以通过spy TestObject,然后对getLayoutInfalter
   //打桩,使得返回一个mock LayoutInfalter,然后再对mock LayoutInfalter的inflate方法打桩,
   //使得不去真正加载R.layout.complex_layout,而是返回一个自己创建好ViewGroup,使得代码
   //能继续跑下去
   @Test
   public void testMethodB(){
      TestSubject t = new TestSubject();
      TestSubject spyT  = Mockito.spy(t);
      TestObject obj = new TestObject();
      TestObject spyObj = Mockito.spy(obj);
      LayoutInflater mockInflater = mock(LayoutInflater.class);
      ViewGroup mockViewGroup = mock(ViewGroup.class);
      when(mockInflater).inflate(anyInt(), isNull(ViewGroup.class), anyBoolean()).thenReturn(mockViewGroup);
      doReturn(mockInflater).when(spyObj).getLayoutInfalter();
      //假定输入1,能让key==0
      spyT.methodB(1);
      //assert && verify
      verify(mockViewGroup).setVisibility(View.GONE);
      //verify other
   }
}

总而言之,如果你想对一个真实对象的某个方法打桩( doReturn().when().method() ),verify真实对象的public方法( verify().method() ),绕过真实对象的某个public方法( doNothing().when().method() ),你可以使用spy后的对象,如:

TestSubject t = new TestSubject();
TestSubject spyT = Mockito.spy(t);

特别需要注意的是,t和spyT是两个不同的对象,后面的代码必须要使用spyT,打桩才有效,才能verify TestSubject的方法。如果你只是spy(t),而后面的代码仍然调用t.methodB()的话,则打桩无效,无法verify。而要是保证调用的是spyT.methodB()。

对于@Spy,如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化。所以下面两种写法是等价的:如果没有无参构造函数,必须使用写法2。

// 写法1
@Spy
private ExampleService spyExampleService;

// 写法2
@Spy
private ExampleService spyExampleService = new ExampleService();

Verify 验证模拟对象上的调用

Mockito跟踪所有的方法调用及其对mock对象的参数。可以在模拟对象上使用 verify() 方法来验证是否满足指定的条件。例如,您可以验证是否使用某些参数调用了某个方法。这种测试有时被称为行为测试。行为测试不检查方法调用的结果,但它检查使用正确的参数调用方法。如果您不关心值,可以使用 anyX,例如 anyIntanyString()any(YourClass.class) 方法。

示例 Demo7Test.java

import static org.mockito.Mockito.*;

/**
 * verify 示例
 */
public class Demo7Test extends MockitoBaseCase {

    @Mock
    MyModel myModel;

    @Test
    public void testVerify(){
        myModel.getName();
        myModel.getAge();
        myModel.getAge();
        myModel.getAge();

        // verify记录着这个模拟对象调用了什么方法,调用了多少次,never() 没有被调用,相当于 times(0),atLeast(N) 至少被调用 N 次,atLeastOnce() 相当于 atLeast(1),atMost(N) 最多被调用 N 次
        // 参数匹配也可以为:verify(mock).someMethod(anyInt(), anyString());
        verify(myModel).getName();
        verify(myModel,times(1)).getName();
        verify(myModel, never()).getIsBoy();
        verify(myModel, atLeastOnce()).getName();
        verify(myModel, atLeast(2)).getAge();
        verify(myModel, atMost(3)).getAge();
        // This let's you check that no other methods where called on this object.
        // You call it after you have verified the expected method calls.
        verifyNoMoreInteractions(myModel);
    }

}

使用 @InjectMocks 进行依赖注入

@InjectMocks 注释,它尝试根据类型进行构造函数、方法或字段依赖注入,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。例如,假设您有以下类。

public class MyCase2 {

    private MyModel myModel;
    private MyCase1 myCase1;

    public MyCase2(MyModel myModel, MyCase1 myCase1) {
        super();
        this.myModel = myModel;
        this.myCase1 = myCase1;
    }

    public void initialize() {
        myCase1.case1();
    }
}

这个类可以通过Mockito来构造,它的依赖关系可以通过模仿对象来实现,如下面的代码片段所示。

示例 Demo8Test.java

import static org.mockito.Mockito.*;

/**
 * InjectMocks 示例
 */
public class Demo8Test extends MockitoBaseCase {

    @Mock
    MyModel myModel;

    @Spy
    @InjectMocks
    MyCase1 myCase1;

    @InjectMocks
    MyCase2 myCase2;

    @Test
    public void testVerify(){
        doReturn(myModel).when(myCase1).case1();
        myCase2.initialize();
        verify(myCase1).case1();
    }

}

Argument matchers 参数匹配器

参数匹配器可以让打桩和验证变得更加灵活和方便,匹配器例如:anyString(),any()等。注意:当传参中有有一个参数使用了参数匹配器,则其他的传参也必须是参数匹配器。

示例 Demo9Test.java

public class Demo9Test extends MockitoBaseCase {

    @Spy
    MyCase1 case1;

    @Test
    public void testMatchers(){
        String str = RandomString.random();
        doReturn(str).when(case1).matchers(anyString(),ArgumentMatchers.<MyModel>any());
        String result = case1.matchers(anyString(),ArgumentMatchers.<MyModel>any());
        assertEquals(result,str);
    }
}

ArgumentCaptor 参数获取

ArgumentCaptor 类允许在验证期间访问方法调用的参数。这允许捕获方法调用的这些参数,并在测试中使用它们。

示例 Demo10Test.java

import static org.mockito.Mockito.*;

/**
 * captor.capture() 示例
 */
public class Demo10Test extends MockitoBaseCase {

    @Captor
    private ArgumentCaptor<Integer> captor;

    @Mock
    MyModel myModel;

    @Test
    public void testArgumentCaptor(){
        myModel.setAge(10);
        verify(myModel).setAge(captor.capture());
    }

}

Answers

当遇到一些比较复杂的结果时可以定义一个 Answers 对象。当 thenReturn 每次返回一个预定义的值时,通过 answers 您可以根据提供给桩方法的参数计算响应。如果您的桩方法要对其中一个参数调用一个函数,或者如果您的方法要返回第一个参数以允许方法链接,那么这将非常有用。对于后者,存在一个静态方法。还请注意,有不同的方式配置一个 answers:

import static org.mockito.AdditionalAnswers.returnsFirstArg;

@Test
public final void answerTest() {
    // with doAnswer():
    doAnswer(returnsFirstArg()).when(list).add(anyString());
    // with thenAnswer():
    when(list.add(anyString())).thenAnswer(returnsFirstArg());
    // with then() alias:
    when(list.add(anyString())).then(returnsFirstArg());
}

或者如果你需要回调你的参数:

@Test
public final void callbackTest() {
    ApiService service = mock(ApiService.class);
    when(service.login(any(Callback.class))).thenAnswer(i -> {
        Callback callback = i.getArgument(0);
        callback.notify("Success");
        return null;
    });
}

甚至可以模仿 DAO 这样的持久性服务,但是如果您的答案变得过于复杂,您应该考虑创建一个伪类而不是模仿类

List<User> userMap = new ArrayList<>();
UserDao dao = mock(UserDao.class);
when(dao.save(any(User.class))).thenAnswer(i -> {
    User user = i.getArgument(0);
    userMap.add(user.getId(), user);
    return null;
});
when(dao.find(any(Integer.class))).thenAnswer(i -> {
    int id = i.getArgument(0);
    return userMap.get(id);
});

Mocking final classes

Mockito Mocking final classes 实际上是使用的 PowerMock 进行的代理,所以建议直接使用 PowerMock

这个功能默认是隐藏关闭的,要开启则需要在 src/test/resources/mockito-extensions/ 或者 src/mockito-extensions/ 目录下创建 org.mockito.plugins.MockMaker 文件,在文件中加入以下内容:

mock-maker-inline

配置完成后就可以mock final class了

final class FinalClass {
    public final String finalMethod() { return "something"; }
}

@Test
public final void mockFinalClassTest() {
     FinalClass instance = new FinalClass();

     FinalClass mock = mock(FinalClass.class);
     when(mock.finalMethod()).thenReturn("that other thing");

     assertNotEquals(mock.finalMethod(), instance.finalMethod());
}

Java 项目使用 PowerMock

Mockito不能模拟静态方法。为此,您可以使用 Powermock。PowerMock提供了一个名为 PowerMockito 的类,用于创建模拟/对象/类并初始化验证和期望,您还可以使用Mockito设置和验证期望(例如 times()anyInt())。

maven配置

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.7</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-junit4-rule-agent</artifactId>
    <version>2.0.7</version>
    <scope>test</scope>
</dependency>
// PowerMock有两个重要的注解:
      –@RunWith(PowerMockRunner.class)
      –@PrepareForTest( { YourClassWithEgStaticMethod.class })
     // 如果你的测试用例里没有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class),反之亦然。当你需要使用PowerMock强大功能(Mock静态、final、私有方法等)的时候,就需要加注解@PrepareForTest。

编写powermock用例步骤

  • 类上面先写这两个注解@RunWith(PowerMockRunner.class)、@PrepareForTest(StudentService.class)

  • 先模拟一个假对象即studentdao方法中的局部变量

  • 用无参的方式new对象

  • 再模拟这个对象被调用时,是否有返回,有返回值给出默认值,没有用doNothing()

  • 验证有返回值使用assertEquals即可,无返回值使用Mockito.verify验证

有以下待测试类:

public class MyCase4 {

    @Autowired
    MyCase3 myCase3;

    public String caseStatic(){
       return MyCase3.caseStatic();
    }

    public Boolean caseFinal() {
        return myCase3.caseFinal();
    }

    public Boolean casePrivate() {
        return myCase3.casePrivate();
    }
}

依赖类:

public class MyCase3 {

    public static String caseStatic(){
        return "";
    }

    public final Boolean caseFinal(){
        return true;
    }

    Boolean casePrivate(){
        return false;
    }
}

以下测试示例代码参见示例 Demo11Test.java

Mock静态方法

import static org.junit.Assert.assertEquals;
import static org.powermock.api.mockito.PowerMockito.*;

@PrepareForTest(MyCase3.class)
public class Demo11Test extends PowerMockBaseCase {

  @Test
  public void testStatic() {
      mockStatic(MyCase3.class);
      String str = RandomString.random();
      when(MyCase3.caseStatic()).thenReturn(str);
      String result = case4.caseStatic();
      assertEquals(result, str);
  }
}

Mock Final方法

@Test
    public void testFinal() {
        when(case3.caseFinal()).thenReturn(true);
        Boolean result = case4.caseFinal();
        assertTrue(result);
    }

Mock 方法内部new出来的对象(模拟构造函数)

@Test
    public void testVoid() throws Exception {
        whenNew(MyCase3.class).withNoArguments().thenReturn(case3);
        MyCase3 testCase = new MyCase3();
        when(testCase.caseFinal()).thenReturn(false);
        Boolean result = testCase.caseFinal();
        assertTrue(!result);
    }

Mock Private方法

@Test
    public void testPrivate() throws Exception {
        when(case3, "casePrivate").thenCallRealMethod();
        Boolean result = case4.casePrivate();
        assertTrue(!result);
    }

单元测试运行

Junit test

IntelliJ IDEA 自带了 jupiter-api 实现,连 jnit-jupiter-engine 都可以不要。

mvn test

若要用 mvn test 在控制台下运行测试用例,还要为 maven-surefire-plugin 加上一个内部依赖,在 pom.xml 文件中

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.21.0</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.2.0</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

然后执行控制台命令

mvn test

集成Jacoco和Sonar

maven配置

<dependencies>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>2.0.7</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-junit4-rule-agent</artifactId>
        <version>2.0.7</version>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.1</version>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>org.jacoco</groupId>
        <artifactId>org.jacoco.agent</artifactId>
        <version>0.8.6</version>
        <classifier>runtime</classifier>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>org.codehaus.sonar-plugins.java</groupId>
        <artifactId>sonar-jacoco-plugin</artifactId>
        <version>2.3</version>
      </dependency>
    </dependencies>

    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <systemPropertyVariables>
            <jacoco-agent.destfile>target/jacoco.exec</jacoco-agent.destfile>
          </systemPropertyVariables>
          <!--暂时跳过测试代码的编译和运行-->
          <skip>false</skip>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.8.6</version>
        <executions>
          <execution>
            <id>default-instrument</id>
            <goals>
              <goal>instrument</goal>
            </goals>
          </execution>
          <execution>
            <id>default-restore-instrumented-classes</id>
            <goals>
              <goal>restore-instrumented-classes</goal>
            </goals>
          </execution>
          <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>report</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.sonarsource.scanner.maven</groupId>
        <artifactId>sonar-maven-plugin</artifactId>
        <version>3.7.0.1746</version>
      </plugin>
      ...

执行命令:

mvn clean verify sonar:sonar

单元测试结果如图:

u1
Figure 1. 单元测试结果

覆盖率查看如图:

c1
Figure 2. 覆盖率视图
c2
Figure 3. 覆盖率详情

sonar配置以及覆盖率分析详细在其他blog中进行详述。

GitLab CI 持续集成

GitLab CI 持续集成后续在其他文档中进行详细描述。

附录

参考链接

UserController.java 测试代码示例

  1. UserController.java 测试代码示例

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.core.env.Environment;
import org.springframework.web.context.request.async.DeferredResult;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.mockStatic;

@RunWith(PowerMockRunner.class)
@PrepareForTest({UserController.class,SecurityUtil.class, MetricsUtil.class})
public class UserControllerPowermockTest{

    @Mock
    private HttpServletRequest request;

    @Mock
    private HttpServletResponse response;

    @Mock
    private TenantUser tenantUser;

    @Mock
    private MenuService menuService;

    @Mock
    private PreferenceService preferenceService;

    @Mock
    private WireProperties WireProperties;

    @Mock
    private TenantManager tenantManager;

    @Mock
    Environment env;

    @Mock
    private TenantProjectService projectService;

    @Spy
    @InjectMocks
    private UserController spyUserController = new UserController();

    @Before
    public void init() throws Exception {
        mockStatic(SecurityUtil.class);
        when(SecurityUtil.currentUser()).thenReturn(tenantUser);
        when(tenantUser.getUsername()).thenReturn("testname");
        when(tenantUser.getDomain()).thenReturn("testname");
        when(tenantUser.getFullName()).thenReturn("testname");
        when(tenantUser.getCommonName()).thenReturn("testname");
        when(tenantUser.isAdmin()).thenReturn(true);
        when(tenantUser.getRole()).thenReturn("testname");
        List<String> list = new ArrayList<>();
        list.add("test");
        list.add("admin");
        when(tenantUser.getRoles()).thenReturn(list);
        doReturn("20MB").when(env).getProperty("spring.servlet.multipart.max-file-size");
        doReturn("100").when(env).getProperty("spring.servlet.multipart.max-request-size");
//        PowerMockito.doReturn(null).when(spyUserController,"buildAppConfigData",WireProperties);
//        PowerMockito.doReturn(null).when(spyUserController,"buildMultipartConfigData",env);
        Map<String, Object> ext = new HashMap<>();
        ext.put("ext","ext test");
        when(tenantUser.getExt()).thenReturn(ext);
    }

    /**
     * 测试 UserController user方法
     * @throws Exception
     * @authour Halley.Fang
     */
    @Test
    public void testUser() throws Exception {
        Map<String, Object> preferences = new HashMap<>();
        preferences.put("client.test","v test");
        doReturn(preferences).when(preferenceService).getValuesByKeyPrefix(Mockito.anyString(),Mockito.anyString(),Mockito.anyString());
        mockStatic(MetricsUtil.class);
        final ResponseData[] rsp = new ResponseData[1];
        when(MetricsUtil.timerRecord(Mockito.anyString(),Mockito.anyObject(),Mockito.any(Supplier.class))).thenAnswer(
                i -> {
                    Supplier s = (Supplier) i.getArguments()[2];
                    if(null != s){
                        rsp[0] = (ResponseData) s.get();
                    }
                    return rsp[0];
                } );
        doAnswer(i -> {
            HttpService service = (HttpService) i.getArguments()[2];
            if(null != service) {
                service.service(request, response);
            }
            DeferredResult deferredResult = new DeferredResult();
            deferredResult.setResult("user test");
            return deferredResult;
        }).when(spyUserController).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
        //调用测试
        DeferredResult result = spyUserController.user(request, response);
        //断言
        assertEquals("user test", result.getResult().toString());
        String rsp_data = "{commonName=testname, role=testname, appConfig={wire.jod-converter.enabled=false, wire.production-mode=false}, domain=testname, roles=[test, admin], preference={client.test=v test, client.version=[]}, multipartConfig={spring.servlet.multipart.max-request-size=100, spring.servlet.multipart.max-file-size=20480KB}, name=testname, admin=true, userExt={ext=ext test}, username=testname}";
        assertEquals(rsp_data,rsp[0].getData().toString());
        //验证
        verify(spyUserController,times(1)).user(request,response);
        verify(spyUserController,times(1)).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
    }

    /**
     * 测试 UserController user方法 if分支测试
     * @throws Exception
     * @authour Halley.Fang
     */
    @Test
    public void testUser2() throws Exception {
        Map<String, Object> preferences = new HashMap<>();
        preferences.put("client.version","v test");
        doReturn(preferences).when(preferenceService).getValuesByKeyPrefix(Mockito.anyString(),Mockito.anyString(),Mockito.anyString());
        mockStatic(MetricsUtil.class);
        final ResponseData[] rsp = new ResponseData[1];
        when(MetricsUtil.timerRecord(Mockito.anyString(),Mockito.anyObject(),Mockito.any(Supplier.class))).thenAnswer(
                i -> {
                    Supplier s = (Supplier) i.getArguments()[2];
                    if(null != s){
                        rsp[0] = (ResponseData) s.get();
                    }
                    return rsp[0];
                } );
        doAnswer(i -> {
            HttpService service = (HttpService) i.getArguments()[2];
            if(null != service) {
                service.service(request, response);
            }
            DeferredResult deferredResult = new DeferredResult();
            deferredResult.setResult("user test");
            return deferredResult;
        }).when(spyUserController).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
        //调用测试
        DeferredResult result = spyUserController.user(request, response);
        //断言
        assertEquals("user test", result.getResult().toString());
        String rsp_data = "{commonName=testname, role=testname, appConfig={wire.jod-converter.enabled=false, wire.production-mode=false}, domain=testname, roles=[test, admin], preference={client.version=v test}, multipartConfig={spring.servlet.multipart.max-request-size=100, spring.servlet.multipart.max-file-size=20480KB}, name=testname, admin=true, userExt={ext=ext test}, username=testname}";
        assertEquals(rsp_data,rsp[0].getData().toString());
        //验证
        verify(spyUserController,times(1)).user(request,response);
        verify(spyUserController,times(1)).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
    }

    /**
     * 测试 UserController menu方法
     * @throws Exception
     * @authour Halley.Fang
     */
    @Test
    public void testMenu() throws Exception {
        when(menuService.getMenu(Mockito.anyObject(),Mockito.anyList())).thenReturn(null);
        mockStatic(MetricsUtil.class);
        final ResponseData[] rsp = new ResponseData[1];
        when(MetricsUtil.timerRecord(Mockito.anyString(),Mockito.anyObject(),Mockito.any(Supplier.class))).thenAnswer(
                i -> {
                    Supplier s = (Supplier) i.getArguments()[2];
                    if(null != s){
                        rsp[0] = (ResponseData) s.get();
                    }
                    return rsp[0];
                } );
        doAnswer(i -> {
            HttpService service = (HttpService) i.getArguments()[2];
            if(null != service) {
                service.service(request, response);
            }
            DeferredResult deferredResult = new DeferredResult();
            deferredResult.setResult("menu test");
            return deferredResult;
        }).when(spyUserController).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
        //调用测试
        DeferredResult result = spyUserController.menu(request, response);
        //断言
        assertEquals("menu test", result.getResult().toString());
        String rsp_data = "{admin=true, menu=null, favorite_menus=[]}";
        assertEquals(rsp_data,rsp[0].getData().toString());
        //验证
        verify(spyUserController,times(1)).menu(request,response);
        verify(spyUserController,times(1)).runWithDefer(Mockito.anyObject(),Mockito.anyObject(),Mockito.any(HttpService.class));
    }
    }

Similar Posts

上一篇 白盒测试方法

Comments

Table of contents