阅读 87

单元测试——JUnit和Mockito

Ref

JUnit

Maven依赖

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
复制代码

基本用法

下面通过一个具体实例展示 JUnit 的基本用法。

  1. 创建 MessageDemo 类,作为被测试对象
public class MessageDemo {
    private String message;
    public  MessageDemo (String message){
        this.message = message;
    }
    public  String printMessage(){
        System.out.println("message is " + this.message);
        return this.message;
    }
}
复制代码
  1. 编写测试类 TestMessageDemo
import com.lbs.spring.spbapp.entity.MessageDemo;

import static org.junit.Assert;  //静态导入
import org.junit.Test;

public class TestMessageDemo {
    private String message = "Hello World!";
    private MessageDemo messageDemo = new MessageDemo(this.message);
    
    @Test
    public void testPrintMessage() throws Throwable {
        assertEquals(message,messageDemo.printMessage());
    }
}
复制代码

需要补充说明的是,上述代码中使用了 import static 进行静态导入,这样后续在调用 assertEquals 方法时,可以不通过 类名.方法名 的方式调用,而是直接通过 方法名 方式调用。

也可以采用普通 import 方式导入,后续使用 Assert.assertEquals语法进行断言,如下代码所示。


//import static org.junit.Assert;  //静态导入

import org.junit.Assert; 

... 

    @Test
    public void testPrintMessage() throws Throwable {
        assertEquals(message,messageDemo.printMessage());
    }

...

复制代码
  1. 创建一个类用于运行测试文件,如下代码所示。使用 org.junit.runner.Result 用于接受测试结果,并使用 result.wasSuccessful() 判断测试结果是否全部正确。对于未通过测试用例的 case,使用 org.junit.runner.notification.Failure 对象打印出错误结果。
package com.lbs.spring.spbapp;

import org.junit.jupiter.api.Test;

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpbAppApplicationTests {

    public static void main(String[] args){
        Result result = JUnitCore.runClasses(TestMessageDemo.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("Test Result:" + result.wasSuccessful());
    }
}
复制代码

上述代码中使用了 JUnitCore.runClasses(Class[]) 来执行测试代码。

  1. 执行 main 方法,会看到如下输出
message is Hello World!
Test Result:true
复制代码
  1. 修改测试用例,查看测试不通过的场景
    @Test
    public void testPrintMessage() throws Throwable {
        String testStr = "Robert";
        assertEquals(testStr,messageDemo.printMessage());
    }
复制代码

执行测试方法,会看到如下输出

message is Robert
testPrintMessage: expected:<[Hello World!]> but was:<[Robert]>
Test Result:false
复制代码

JUnit 断言

上述 Demo 中已经展示了 JUnit 断言的基本使用。JUnit 所有的断言都包含在 Assert 类中,常用方法如下。

  • void assertEquals(boolean expected, boolean actual)
  • void assertTrue(boolean expected, boolean actual)
  • void assertFalse(boolean condition)
  • `void assertNotNull(Object object)``
  • void assertNull(Object object)
  • void assertSame(boolean condition):assertSame()
  • void assertNotSame(boolean condition):assertNotSame()
  • void assertArrayEquals(expectedArray, resultArray)

JUnit 注解

  • @Test: 指定一个测试案例
  • @Before: 指定该该方法需要在 @Test 方法前运行,该方法会针对每一个测试用例执行,且在测试方法前执行。
  • @After: 指定该该方法需要在 @Test 方法后运行,该方法会针对每一个测试用例执行,且在测试方法后执行。
  • @BeforeClass: 指定该方法需要在类中所有方法前运行,对应的方法会首先执行,且执行一次。
  • @AfterClass: 指定该方法需要在类中所有方法后运行,对应的方法会最后执行,且执行一次。
  • @Ignore: 指定不需要执行测试的方法
public class TestMessageDemo {
    private String message = "Hello World!";
    private MessageDemo messageDemo = new MessageDemo(this.message);
 


    @Test
    public void testPrintMessage() throws Throwable {
        System.out.println("@Test--testPrintMessage");
        //Assert.assertEquals(message, messageDemo.printMessage());
    }

    @Test
    public void test2() {
        System.out.println("@Test--test2");
    }

    @BeforeClass
    public static void beforeClass() {
        System.out.println("BeforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("AfterClass");
    }

    @Before
    public void before() {
        System.out.println("Before");
    }

    @After
    public void after() {
        System.out.println("After");
    }
}
复制代码

此处结合一个实例,理解 JUnit 注解的执行顺序。当执行上述测试场景时,会输入如下内容

BeforeClass
Before
@Test--test2
After
Before
@Test--testPrintMessage
After
AfterClass
复制代码

如果使用 @Ignore 注释 test2 方法,即

    @Test
    @Ignore
    public void test2() {
        System.out.println("@Test--test2");
    }
复制代码

此时执行上述测试场景时,会输入如下内容

BeforeClass
Before
@Test--testPrintMessage
After
AfterClass
复制代码

JUnit 套件测试

测试套件意味着捆绑几个单元测试用例并且一起执行他们。在 JUnit 中,@RunWith@Suite 注解用来运行套件测试。

  1. 在上述代码的基础上,创建一个测试类 TestMessageDemo2,代码如下
public class TestMessageDemo2 {
    private String message = "Robert";
    private MessageDemo messageDemo = new MessageDemo(this.message);

    @Test
    public void testPrintMessage() {
        System.out.println("@Test--testPrintMessage2");
        Assert.assertEquals("TestMessageDemo2", messageDemo.printMessage());
      
    }
}
复制代码
  1. 使用 @RunWith@Suite 注解创建一个套件测试类 TestSuite
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({
        TestMessageDemo.class,
        TestMessageDemo2.class
})
public class TestSuite {
}

复制代码
  1. 最后修改启动测试类文件如下
    public static void main(String[] args){
        Result result = JUnitCore.runClasses(TestSuite.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("Test Result:" + result.wasSuccessful());
    }
复制代码
  1. 启动测试,会看到如下结果,两个测试类的方法一起被执行
message is Hello World!
message is Robert
testPrintMessage(com.lbs.spring.spbapp.TestMessageDemo): expected:<[test1]> but was:<[Hello World!]>
testPrintMessage(com.lbs.spring.spbapp.TestMessageDemo2): expected:<[test2]> but was:<[Robert]>
Test Result:false
复制代码

JUnit 时间测试

如果一个测试用例比起指定的毫秒数花费了更多的时间,那么 Junit 将自动将它标记为失败。@Test 注解中附加 timeout 参数,用于指定允许的执行时间。

    @Test(timeout = 2000)
    public void testPrintMessage() throws Throwable {
        Thread.sleep(5000);
        Assert.assertEquals("test1", messageDemo.printMessage());
    }
复制代码

如上代码所示,指定测试方法标记超时时间为2s。执行上述测试方法,会看到如下输出

testPrintMessage(com.lbs.spring.spbapp.TestMessageDemo): test timed out after 2000 milliseconds
复制代码

JUnit 异常测试

@Test(expected = ArithmeticException.class)
复制代码

@Test 注解中附加 expected 参数并指定期望抛出的异常,可以对方法抛出的异常进行测试。

JUnit 参数化测试

Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。你将遵循 5 个步骤来创建参数化测试。

  1. @RunWith(Parameterized.class) 来注释 test 类。
  2. 创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
  3. 创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
  4. 为每一列测试数据创建一个实例变量。
  5. 用实例变量作为测试数据的来源来创建你的测试用例。

一旦每一行数据出现测试用例将被调用。

  1. 创建 PrimeNumberChecker 类,作为被测试的对象
public class PrimeNumberChecker {
    public boolean validate(final int primeNumber){
        for(int i=2;i<(primeNumber/2);i++){
            if(primeNumber%i == 0){
                return false;
            }
        }
        return  true;
    }
}
复制代码
  1. 创建测试类 TestPrimeNumberChecker,使用 @Parameterized.Parameters 指定测试参数,并使用 @RunWith(Parameterized.class) 将测试参数绑定到测试方法中。
package com.lbs.spring.spbapp;

import com.lbs.spring.spbapp.entity.PrimeNumberChecker;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.Arrays;
import java.util.Collection;


@RunWith(Parameterized.class)
public class TestPrimeNumberChecker {
    private int inputNumber;
    private boolean expectedResult;
    private PrimeNumberChecker primeNumberChecker;

    @Before
    public void initialize() {
        primeNumberChecker = new PrimeNumberChecker();
    }

    public TestPrimeNumberChecker(int inputNumber, boolean expectedResult) {
        this.inputNumber = inputNumber;
        this.expectedResult = expectedResult;
    }

    @Parameterized.Parameters
    public static Collection primeNumbers() {
        return Arrays.asList(new Object[][]{
                {2, true},
                {6, false},
                {19, true},
                {22, false}
        });
    }

    @Test
    public void  testPrimeNumberCheck(){
        System.out.println("Parameterized Number is " + inputNumber);
        Assert.assertEquals(expectedResult,primeNumberChecker.validate(inputNumber));
    }
}
复制代码
  1. 最后修改启动测试类文件如下
    public static void main(String[] args){
        Result result = JUnitCore.runClasses(TestPrimeNumberChecker.class);
        for(Failure failure : result.getFailures()){
            System.out.println(failure.toString());
        }
        System.out.println("Test Result:" + result.wasSuccessful());
    }
复制代码
  1. 执行测试方法,会看到如下输出结果
Parameterized Number is 2
Parameterized Number is 6
Parameterized Number is 19
Parameterized Number is 22
Test Result:true
复制代码

Mockito

Mock测试

考虑如上场景,使用JUnit进行单元测试时,若要对 Class A 进行测试,要先把 A 的整个依赖全部构建出来,也就是B,C,D,E的实例。

一种替代方案就是使用 Mock,Mock 出 B 和 C 的实例,如下图所示。

从图中可以看出

  • mock 对象就是在调试期间用来作为真实对象的替代品
  • mock 测试就是在测试过程中,对那些不容易构建的对象,用一个虚拟对象来代替测试的方法

Mock依赖引入

  • Maven
<dependency> 
  <groupId>org.mockito</groupId>  
  <artifactId>mockito-core</artifactId>  
  <version>3.3.3</version>  
  <scope>test</scope>
</dependency>
复制代码
  • Gradle
//gradle
testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'
复制代码

基本用法

如下Demo,静态导入 Mockito 依赖,并使用 mock 方法创建一个 mock 对象,使用 when().thenReturn() 设置方法预期的返回值,并使用 verify() 方法验证方法的调用。


import org.junit.Assert;
import org.junit.Test;

import java.util.List;

import static org.mockito.Mockito.*; //静态导入

public class MockitoSimpleTest {

    @Test
    public void simpleTest(){

        //创建mock对象,参数可以是类,也可以是接口
        List<String> list = mock(List.class);

        //设置方法的预期返回值
        when(list.get(0)).thenReturn("hello");

        String result = list.get(0);

        //验证方法调用(是否调用了get(0))
        verify(list).get(0);

        //junit测试
        Assert.assertEquals("hello2",result);
    }
}
复制代码

创建 mock 对象不能对 finalanonymousprimitive 类进行 mock。

可对方法设定返回异常,如下代码所示。


when(mockedList.get(0)).thenReturn("first");

when(list.get(1)).thenThrow(new RuntimeException("test excpetion"));  
复制代码
  • 当获取第0个元素时,返回 first;
  • 当获取第1个元素时,抛出 RuntimeException 异常

模拟接口实现类

  1. 建立一个 Statement 接口
public interface Statement {

    String query();
}
复制代码
  1. 再建立一个 ResultSetHandler 类,其方法 handlerResultSets 需要一个实现 Statement 接口的类
public class ResultSetHandler {

    public void handlerResultSets(Statement statement) {
        String queryStr = statement.query ();
        System.out.println (queryStr);
    }
}
复制代码
  1. 建立测试类:使用 Mockito 创建一个 Statement 接口的模拟实现类,传入需要调用的方法,可以很方便的对 ResultSetHandler 进行测试,解除了对 Statement 实现类的依赖
public class MockTest {

    public static void main(String[] args) {

        // 使用mock模拟Statement接口行为,这里假设执行查询,返回的结果为test
        Statement imock = Mockito.mock (Statement.class);
        Mockito.when (imock.query ()).thenReturn ("test");

        // 使用spy创建一个真实对象
        ResultSetHandler useMock = Mockito.spy (ResultSetHandler.class);
        useMock.handlerResultSets (imock);
    }
}
复制代码