关于单元测试的一点思考

前言

Hello,我是Storm。今天呢很想就单元测试这个点整理一下。在平时的coding当中,其实单元测试应该是用的比较多的,但是不知道大家有没有跟我有一样的感受。在测试自己的工具类或者比较简单的模块时,用junit等这样的测试框架就很方便的解决了问题。但是一旦测试的代码依赖于太多第三方的模块,或者自己写的其他模块代码,测试就变得非常麻烦,因为你需要启动这些三方服务或者模块。特别是分布式环境下,自己开发机器又不是性能优越的情况下那简直就是噩梦。

思考

那有没有方便的测试思想或者方法甚至工具可以帮我们摆脱这种困扰呢,答案是肯定的。但是还是想自己来想一想,这个背后到底是怎么个原理。针对我们遇到的问题,无非就是“解耦”,如何在不影响现有测试流程的情况下,把测试需要的环境给准备好。

例如需要某个接口服务,这个接口服务我们不关心它是分布式的还是单例的,网络状况如何,我只关注它给我返回的数据。在java中我们就可以借助依赖注入的方式来解决,自己实现一个接口,或者直接new一个服务,覆盖其方法,返回值直接设置成我们需要的数据,以此来模拟实例化一个服务。甚至我们可以自定义一个流程,在调用某些方法的前后来定制这个入参和返回值,以此达到测试的目的。

同时我也发现一个问题,对于服务、接口我们可以依赖注入的方式来规避,但是静态方法呢就不太好处理了。简单的方法还好,对于复杂的静态方法引入了过多外部资源其实很影响代码的可测试性,因此提醒自己在写码的时候要注意规避这种问题,也算是测试驱动开发(TDD)的一种应用吧。

mock

mock即模拟,在测试领域指的是模拟与测试相关的数据和环境,达到解耦第三方依赖的目的而只关注测试目标本体的思想方法。在java生态中比较常用的mock工具有EasyMock(早起流行)、Mockito(改进了EasyMock,主流)、PowerMock(主流)、Jmockit(轻量)等等。这里我简单介绍一下Mockito的用法。

Mockito可以让你轻松模拟任何Java类行为和数据,并且可以跟踪执行流程,自定义测试节点的参数和方法返回值,从而解耦第三方依赖,简化单元测试。
PUZZLE1min 1.png

Mockito基本使用

1
   @Test
2
   public void createMockObject() {
3
       // 使用 mock 静态方法创建 Mock 对象.
4
       List mockedList = mock(List.class);
5
       Assert.assertTrue(mockedList instanceof List);
6
7
       // mock 方法不仅可以 Mock 接口类, 还可以 Mock 具体的类型.
8
       ArrayList mockedArrayList = mock(ArrayList.class);
9
       Assert.assertTrue(mockedArrayList instanceof List);
10
       Assert.assertTrue(mockedArrayList instanceof ArrayList);
11
   }
12
13
   @Test
14
   public void configMockObject() {
15
//定制类行为
16
       List mockedList = mock(List.class);
17
18
       // 我们定制了当调用 mockedList.add("one") 时, 返回 true
19
       when(mockedList.add("one")).thenReturn(true);
20
       // 当调用 mockedList.size() 时, 返回 1
21
       when(mockedList.size()).thenReturn(1);
22
23
       Assert.assertTrue(mockedList.add("one"));
24
       // 因为我们没有定制 add("two"), 因此返回默认值, 即 false.
25
       Assert.assertFalse(mockedList.add("two"));
26
       Assert.assertEquals(mockedList.size(), 1);
27
28
       Iterator i = mock(Iterator.class);
29
       when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");
30
       String result = i.next() + " " + i.next();
31
       //assert
32
       Assert.assertEquals("Hello, Mockito!", result);
33
   }
34
35
   @Test(expected = NoSuchElementException.class)
36
   public void testForIOException() throws Exception {
37
//模拟异常
38
       Iterator i = mock(Iterator.class);
39
       when(i.next()).thenReturn("Hello,").thenReturn("Mockito!"); // 1
40
       String result = i.next() + " " + i.next(); // 2
41
       Assert.assertEquals("Hello, Mockito!", result);
42
43
       doThrow(new NoSuchElementException()).when(i).next(); // 3
44
       i.next(); // 4
45
   }
46
47
   @Test
48
   public void testVerify() {
49
//验证方法调用情况
50
       List mockedList = mock(List.class);
51
       mockedList.add("one");
52
       mockedList.add("two");
53
       mockedList.add("three times");
54
       mockedList.add("three times");
55
       mockedList.add("three times");
56
       when(mockedList.size()).thenReturn(5);
57
       Assert.assertEquals(mockedList.size(), 5);
58
59
       verify(mockedList, atLeastOnce()).add("one");
60
       verify(mockedList, times(1)).add("two");
61
       verify(mockedList, times(3)).add("three times");
62
       verify(mockedList, never()).isEmpty();
63
   }
64
65
   @Test
66
   public void testSpy() {
67
       //Mockito 提供的 spy 方法可以包装一个真实的 Java 对象, 并返回一个包装后的新对象. 
68
//若没有特别配置的话, 对这个新对象的所有方法调用, 都会委派给实际的 Java 对象.
69
       List list = new LinkedList();
70
       List spy = spy(list);
71
72
       // 对 spy.size() 进行定制.
73
       when(spy.size()).thenReturn(100);
74
75
       spy.add("one");
76
       spy.add("two");
77
78
       // 因为我们没有对 get(0), get(1) 方法进行定制,
79
       // 因此这些调用其实是调用的真实对象的方法.
80
       Assert.assertEquals(spy.get(0), "one");
81
       Assert.assertEquals(spy.get(1), "two");
82
83
       Assert.assertEquals(spy.size(), 100);
84
   }
85
86
   @Test
87
   public void testCaptureArgument() {
88
//参数模拟
89
       List<String> list = Arrays.asList("1", "2");
90
       List mockedList = mock(List.class);
91
       ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
92
       mockedList.addAll(list);
93
       verify(mockedList).addAll(argument.capture());
94
95
       Assert.assertEquals(2, argument.getValue().size());
96
       Assert.assertEquals(list, argument.getValue());
97
   }

总结

利用现代化的测试工具和框架能够大大简化日常开发当中涉及到的一些测试工作量,但是也要保持一颗求真的心,知道其背后的思想,才能沉淀成自己的东西。也会加深自己对于框架和工具的理解,更好的使用它。