Java单元测试框架介绍

分享内容

  • 单元测试框架 junit4:Java的单元测试框架基础套件简单介绍
  • Mock框架 mockito: Java单元测试中的mock框架
  • Mockito的扩展PowerMock: 给mockito提供扩展功能,mock静态方法,final方法.绕过封装:读写私有成员
  • Xpath for Json : https://github.com/json-path/JsonPath, 简化测试中Json字符串解析取值的类库

分享目的

介绍Java单元测试框架JUnit4的功能
介绍mockito框架的基本mock用法

测试原则

你永远不知道这个方法会在什么时候被修改(特别是涉及业务逻辑的方法)
测试用例应该是独立的,可重复的,自检测的,测试结果不应该依赖外部环境配置
类方法逻辑应该单一,尽可能简短=>健壮,易读

涉及框架介绍

JUnit4

基本测试注解

@RunWith Junit的Runner机制,@RunWith(SpringJUnit4ClassRunner.class) 简化基于spring的测试
@BeforeClass 测试类启动时执行一次
@AfterClass 测试类销毁时执行一次
@Test 方法注解,测试用例
@Before 方法注解,测试方法执行前,例如测试对象的创建,配置
@After 方法注解,测试方法执行后,例如资源清理

Assertions(断言)

检测输出结果为预期值,框架提供了很多内建的断言匹配静态方法,例如:
基本类型等值,数组匹配,范围匹配,空值匹配,集合元素包含等
静态包导入

import static org.hamcrest.CoreMatchers.*
import static org.junit.Assert.*

示例见引用链接

Test Executor Order(测试执行顺序)

默认Junit中测试类的方法执行顺序是不确定的(依赖JDK的反射出来的method顺序),如果要指定顺序,junit4仅支持按照方法名的自然顺序.

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {

  @Test
  public void testA() {
      System.out.println("first");
  }
  @Test
  public void testB() {
      System.out.println("second");
  }
  @Test
  public void testC() {
      System.out.println("third");
  }
}

Exception Test(异常抛出测试)

有三种异常测试的方法

public class ExceptionTest {

    //第一种:使用@Test的expected属性

    @Test(expected = IndexOutOfBoundsException.class)
    public void empty() {
        new ArrayList<Object>().get(0);
    }

    //第二种:使用try/catch捕捉异常流程,断言失败

    @Test
    public void testExceptionMessage() {
        try {
            new ArrayList<Object>().get(0);
            fail("Expected an IndexOutOfBoundsException to be thrown");
        } catch (IndexOutOfBoundsException anIndexOutOfBoundsException) {
            assertThat(anIndexOutOfBoundsException.getMessage(), is("Index: 0, Size: 0"));
        }
    }

    //第三种:使用异常规则,这种方式可以深度断言异常的类型和异常Message

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

    @Test
    public void shouldTestExceptionMessage() throws IndexOutOfBoundsException {
        List<Object> list = new ArrayList<>();

        thrown.expect(IndexOutOfBoundsException.class);
        thrown.expectMessage("Index: 0, Size: 0");
        //使用匹配器断言Message内容
        thrown.expectMessage(CoreMatchers.containsString("Size: 0"));
        list.get(0); // execution will never get past this line
    }

}

Ignoring Tests(忽略测试)

如果希望暂时忽略某个测试方法

使用 @Ignore 注解 而不是 注释掉 @Test , 这样测试报告工具可以统计被暂时忽略的测试方法

@Ignore("因为某种原因而不想测试")  
@Test  
public void testSame() {  
 assertThat(1, is(1));}  
  

测试基础框架中的高级功能

分组聚合测试,依赖测试,参数化测试的高级功能使用 TestNG 会更方便处理.或者使用Junit5

Mockito*

mockito是一个对象mock框架,用来创建一个虚拟的对象,隔离测试依赖

静态import

import static org.mockito.Mockito.*;  
import static org.mockito.Matchers.*;  

mock object (模拟对象)

场景: 需要对A行为进行测试,但是A行为依赖B行为.
B行为可以预测,有限的结果
B行为是外部系统调用(HTTP , DUBBO etc.)

public class MockTestObject {

//    @Atuowire
    private String url;

    public String post(){
        return url;
    }

    public String des(){
        return "this is class";
    }

    public int mutiArgs(int x,int y,String name){
        return name.hashCode()*x*y;
    }

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

    public void addList(List<String> strings){
        strings.add("abc");
    }

    public int subMethodCall(){
        return call()*2;
    }
    public int call(){
        return 100;
    }

    private String thisPrivateMethod(){
        return "private";
    }

    private String thisPrivateMethod(int a){
        return "private "+a;
    }

    final private String thisFinalPrivateMethod(){
        return "final private";
    }

    public static String thisStaticMethod(){
        return "static method";
    }

}

    @Test
    public void mock_test() throws Exception {

        MockTestObject testObject = mock(MockTestObject.class);

        when(testObject.subMethodCall()).thenReturn(2);
        assertEquals(2,testObject.subMethodCall());


        doReturn(3).when(testObject).subMethodCall();
        assertEquals(3,testObject.subMethodCall());


        //使用doAnswer来返回你自定义的需要,谨慎使用,因为会引入"测试的测试"问题
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] args = invocation.getArguments();
                Object mock = invocation.getMock();
                System.out.println(Arrays.toString(args));
                return 100;
            }
        }).when(testObject).subMethodCall();
        assertEquals(100,testObject.subMethodCall());


        //对返回void的方法Mock

        //when(testObject.thisvoidmethod()).thenReturn(XXX);//compire error
        doNothing().when(testObject).thisvoidmethod();
        testObject.thisvoidmethod();


//        doThrow()可以mock方法抛出异常
//        doThrow(new RuntimeException()).when(testObject).addList(anyList());
//        testObject.addList(Arrays.asList(new String[]{}));


        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object[] args = invocation.getArguments();
                ((List)args[0]).add("OK");
                return null;
            }
        }).when(testObject).addList(anyList());
        List<String> vs = new ArrayList<>();
        testObject.addList(vs);
        assertEquals("OK",vs.get(0));

        //anyInt() eq()这些是Metchers的静态方法,用来匹配存根参数
        doReturn(1000).doReturn(1001).when(testObject).mutiArgs(anyInt(),eq(2),anyString());
        assertEquals(1000,testObject.mutiArgs(1,2,""));
        assertEquals(1001,testObject.mutiArgs(1222,2,"4"));
        assertEquals(1001,testObject.mutiArgs(333,2,"5"));
    }

验证行为

场景,A行为执行会执行B行为,通过verify验证A行为是否真的会执行B行为

// mock creation
List mockedList = mock(List.class);
// using mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one");
mockedList.clear();
// selective, explicit, highly readable verification
verify(mockedList).add("one");
verify(mockedList).clear();

还可以按照顺序验证行为

    @Test
    public void Verification_in_order() throws Exception {

        // A. 单个Mock对象的行为验证
        List singleMock = mock(List.class);

        //using a single mock
        singleMock.add("was added first");
        singleMock.add("was added second");

        //create an inOrder verifier for a single mock
        InOrder inOrder = inOrder(singleMock);

        //following will make sure that add is first called with "was added first, then with "was added second"
        inOrder.verify(singleMock).add("was added first");
        inOrder.verify(singleMock).add("was added second");

        // B. 多个Mock对象的行为验证
        List firstMock = mock(List.class);
        List secondMock = mock(List.class);

        //using mocks
        firstMock.add("was called first");
        secondMock.add("was called second");

        //create inOrder object passing any mocks that need to be verified in order
        InOrder inOrder2 = inOrder(firstMock, secondMock);

        //following will make sure that firstMock was called before secondMock
        inOrder2.verify(firstMock).add("was called first");
        inOrder2.verify(secondMock).add("was called second");

        // A,B两种用法可以混合使用

    }

PowerMock*

mock静态方法/final方法/JDK类的静态方法  
绕过封装
    直接调用私有成员方法/构造方法
    直接读写成员变量

绕过封装(工具类)对于独立测试 Spring Bean 非常有帮助



/**
 * @author zhangweicong 2018/9/14.
 */
@RunWith(PowerMockRunner.class)//1
@PrepareForTest({StaticClass.class})//2
public class UsedTest {


    /**
     * 白盒测试工具类
     * @throws Exception
     */
    @Test
    public void whiteBoxTest() throws Exception {

        MockTestObject testObject = new MockTestObject();
        assertEquals(null,testObject.post());

        Whitebox.setInternalState(testObject,"url","http://a.b.c");
        assertEquals("http://a.b.c",testObject.post());

        String res = Whitebox.invokeMethod(testObject,"thisPrivateMethod");
        assertEquals("private",res);

        res = Whitebox.invokeMethod(testObject,"thisPrivateMethod",1);
        assertEquals("private 1",res);

//        Whitebox.setInternalState(..)
//        Whitebox.getInternalState(..)
//        Whitebox.invokeMethod(..)
//        Whitebox.invokeConstructor(..)

    }

    /**
     * @RunWith(PowerMockRunner.class)//1.加测试类注解
     * @PrepareForTest({StaticClass.class})//2.加测试类注解
     * @throws Exception
     */
    @Test
    public void mock_static_method() throws Exception {

        PowerMockito.mockStatic(StaticClass.class);//3.mock静态类
        PowerMockito.when(StaticClass.ask()).thenReturn("abc");//4.存根
        assertEquals("abc",StaticClass.ask());

    }

    //有时使用PowerMock非出现类加载错误,通常在测试类上加 @PowerMockIgnore({pack.foo.bar.*}) 注解可以解决该问题

}


json-path

依赖(spring-boot-start-test包含了)

<dependency>
  <groupId>com.jayway.jsonpathgroupId>
  <artifactId>json-pathartifactId>
  <version>2.3.0version>
dependency>

部分特性的支持需要 json provider,可以使用gson/jackson:


  com.google.code.gson
  gson
  2.8.5

用法示例

example json


    private String JSON_DOCUMENT="{\n" +
            "    \"store\": {\n" +
            "        \"book\": [\n" +
            "            {\n" +
            "                \"category\": \"reference\",\n" +
            "                \"author\": \"Nigel Rees\",\n" +
            "                \"title\": \"Sayings of the Century\",\n" +
            "                \"price\": 8.95\n" +
            "            },\n" +
            "            {\n" +
            "                \"category\": \"fiction\",\n" +
            "                \"author\": \"Evelyn Waugh\",\n" +
            "                \"title\": \"Sword of Honour\",\n" +
            "                \"price\": 12.99\n" +
            "            }\n" +
            "        ],\n" +
            "        \"bicycle\": {\n" +
            "            \"color\": \"red\",\n" +
            "            \"price\": 19.95\n" +
            "        }\n" +
            "    },\n" +
            "    \"expensive\": 10\n" +
            "}";
    @Test
    public void used() throws Exception {

        //对象解析
        String json = "{\"date_as_long\" : 1411455611975}";
        Date date = JsonPath.parse(json).read("$['date_as_long']", Date.class);

        //内联谓词
        List<Map<String, Object>> books =  JsonPath.parse(JSON_DOCUMENT)
                .read("$.store.book[?(@.price < 10)]");

        System.out.println(Arrays.toString(books.toArray()));

        String author = JsonPath.parse(JSON_DOCUMENT).read("$.store.book[0].author");
        
    }

相关资料

  • https://junit.org/junit4/
  • https://site.mockito.org/
  • http://powermock.github.io/
  • https://github.com/json-path/JsonPath

你可能感兴趣的:(学习日志)