1.前言
最近在做一个项目的开发双周一迭代发版这些都还算正常但这里有个比较特别的项目质量要求后台代码的单元测试覆盖率要达到80%并且这个指标作为上线的硬性要求。当时觉得这个要求挺逗的互联网开发还需要写单测私底下请教了一起合作的xx钉事业部小伙伴才知道他们这里推行这样的研发规范有一段时间了而且确实提升了研发质量保障后台的代码与服务质量减少因质量问题导致交付上线的延误其实我是极其认同的。恰好借助这次机会整体梳理下单元测试相关的知识内容。
2.单元测试的相关知识
2.1 单元测试
单元测试是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义 在Java里单元指一个实现类图形化的软件中可以指一个窗口或一个菜单等。
单元测试是在软件开发过程中要进行的最低级别的测试活动软件的独立单元将在与程序的其他部分相隔离的情况下进行测试经常与代码review静动态分析等联系起来。
- 静态分析: 通过研读项目代码去查找错误或收集一些度量数据通常会使用CheckStyle/FindBugs等工具。
- 动态分析: 通过观察软件运行时的动作来提供执行跟踪时间分析以及覆盖率等方面的信息。
当今都是以快制胜的年代更快地完成项目就意味着能抢占市场先机所以很多小伙伴都会感到疑惑功能都写不完还写啥单元测试其实编写单元测试还是利大于弊的:
- 提高开发速度以自动化方式执行测试提升了测试代码的执行效率;
- 提高软件代码质量它使用小版本发布至集成便于实现人员除错。同时引入重构概念让代码更干净和富有弹性;
- 提升系统的可信赖度可在一定程度上确保代码的正确性。
从项目开发迭代周期来讲Bug越是在后期修复成本会越高所以单元测试会将测试过程左移了让开发人员更早地进行设计与实现的质量把控减少修复成本。
从软件系统来看业务复杂度会越来越高各种关联与依赖层层叠加增加了质量不可控因素所以让系统组成的部分与关联部分都处于稳定状态可大大减少风险因子的叠加从而增强整个系统的稳定与质量。当你明白了这些之后写单元测试就会变成开发的一种习惯如同写功能代码一样自然。
2.2 代码覆盖率
**代码覆盖率是一种通过计算测试过程中被执行的源代码占全部源代码的比例进而间接度量软件质量的方法。**它在保证测试质量的时候潜在保证实际产品的质量可以基于此在程序中寻找没有被测试用例测试过的地方进一步创建新的测试用例来增加覆盖率。按性质它属于白盒测试的范畴即主要依据源代码的内部结构来设计测试用例通过设计不同的输入来测试软件的不同部分。常见的编程语言都有相应的代码覆盖率测试工具。基于代码覆盖率的分析可以有以下作用:
- 分析未覆盖部分的代码反推在前期测试设计是否充分需求/设计是否清晰测试设计的理解是否有误等等之后进行补充测试用例设计。
- 检测出程序中的废代码可以逆向反推在代码设计中思维混乱点提醒设计/开发人员理清代码逻辑关系提升代码质量。
在不少开发工具软件如的IDEA都内置了统计覆盖率的工具。
3.单元测试框架
3.1 Java单元测试框架
大部分的单元测试框架都包括了以下几大组件:
TestRuner
: 负责驱动单元测试用例的执行汇报测试执行的结果;TestFixture
: 以测试套件的形式提供setUp
()和tearDown()
方法保证两个测试用例之间的执行是相互独立互不影响的;TestResult
: 这个组件用于收集每个TestCase的执行结果;Test
: 作为TestSuite
和TestCase
的父类暴露run()方法为TestRunner调用;TestCase
: 暴露给用户的类用户通过继承TestCase编写自己的测试用例逻辑;TestSuite
: 提供suite功能管理testCase.
Java
单元测试框架在业界非常多大部分是基于Junit框架的设计思路去实现的其中主要常见单元测试框架以Junit4/Junit5/TestNG
为多目前这些框架都支持注释与参数化测试在运行期指定不同的测试数值来运行单元测试同时与Maven/Gradle等构建工具搭配。
3.2 一个简单的Junit例子
public class JunitDemoTest {
@Rule
public Timeout timeout = new Timeout(1000);
@Before
public void init() {
//...
}
@After
public void destroy() {
//....
}
@Test
public void testAssertArrayEquals() throws InterruptedException {
byte[] str1 = "test1".getBytes();
byte[] str2 = "test2".getBytes();
byte[] str3 = "test".getBytes();
assertTrue("true", String.valueOf(str1).equals(String.valueOf(str3)));
assertFalse("false", String.valueOf(str1).equals(String.valueOf(str2)));
assertArrayEquals("false - byte arrays not same", str1, str2);
Thread.sleep(50000);
}
}
复制代码
3.2 Junit主要实现原理
Junit框架启动时会调用到JunitCore#run(Runner runner)
的方法。
其中Runner
是一个抽象类针对于不同的平台或版本都做了不同的实现其中有Junit4ClassRunner
类继承了ParentRunner
的BlockJUnit4ClassRunner
类/SpringRunner
类/Suit
类 其中会调用各自实现的run
方法。
在组装Statement
中会去初始化运行前后与匹配规则。
在调用childrenInvoker
后会使用任务线程池开启单测任务。在getFilteredChildren
方法中会拿到单元测试类中所有需要测试的方法循环调用runChild
方法将测试方法并放入到任务线程池中执行。
在runChild
中会再去methodBlock
为每个测试方法生成Statement
对象
methodBlock
中会先去生成测试类的对象然后生成前置/后置/规则判断/真正目标方法执行等组成方法执行链条。
RunBefores/RunAfters
等类都继承了Statement
类使用链条的方式组成执行链条
使用Java
的反射机制调用测试方法。
在runLeaf
中会去调用生成好的statement对象的evaluate这是个方法执行链条的开始。并将调用结果通过EachTestNotifier
对象发送出去这里主要是用监听者模式来实现比如运行结果失败或异常信息都会发送出去。
其他几个框架也都是基于Junit
这个基本流程去扩展的 给日常开发中编写单元测试提供了很大的便利但在大型项目开发中也会遇到一些如协同开发的问题如有些依赖的接口或者底层模块并没有完成开发这样写出来的单元测试是无效的还需要“伪造”一些数据/结果来补全这些基础依赖为了解决这些问题Mock
技术应运而生。
4.Mock框架
4.1 Mock介绍
Mock
通常是指在测试一个对象S时我们构造一些假的对象来模拟与S之间的交互而这些Mock
对象的行为是我们事先设定且符合预期。通过这些Mock
对象来测试S在正常逻辑异常逻辑或压力情况下工作是否正常。引入Mock
最大的优势在于Mock
的行为固定它确保当你访问该Mock
的某个方法时总是能够获得一个没有任何逻辑的直接就返回的预期结果。 通常会带来以下一些好处
- 隔绝其他模块出错引起本模块的测试错误。
- 隔绝其他模块的开发状态只要定义好接口不用管他们开发有没有完成。
- 一些速度较慢的操作可以用
Mock Object
代替快速返回。
对于分布式系统的测试使用Mock Object
会有另外两项很重要的收益 - 通过
Mock Object
可以将一些分布式测试转化为本地的测试 - 将
Mock
用于压力测试可以解决测试集群无法模拟线上集群大规模下的压力目前常用的
Mock框架有EasyMock/JMock/Mokito/PowerMokito当前项目使用了PowerMokito。
4.2 一个简单的Mock例子
一次完整的Mock
包括了设定目标->设置消费条件->预期返回结果->消费并检验返回结果四大步骤我们用Mockito
工具来实现一个最简单的的例子 Mockito
主要就是通过Stub
打桩通过方法名加参数来准确的定位测试桩然后返回预期的值。
@Data
public void TestDao {
public User getUser(String uid) { return new User("testUser"); }
@Data
public void TestService {
private TestDao testDao;
public User getUser(String uid) { return testDao.getUser(uid); }}
@RunWith(PowerMokitoRunner.class)
public void MockDemoTest {
@InjectMock
private TestService testService
@Mock
private TestDao testDao;
@Before
public void init() {
Mockito.when(testDao.getUser(any())).thenReturn(new User("test2"));
}
@Test
public void test1() {
//调用方法时会调用到代理对象返回刚才的Mock数据
User userInfo = testService.getUser("uid");
}
}
复制代码
主要对应下面的四大步骤
-
设定目标 > User user
-
设置消费条件 -> testDao.getUser(any())
-
预期返回结果 -> thenReturn(…)
-
消费并检验返回结果 -> testDao.getUser(“uid”)
4.3 PowerMockito主要实现原理
PowerMockito
框架核心原理是基于代理模式使用CGLIGB字节码工具去实现生成目标对象或方法的代理当使用when调用初始化时会去匹配然后根据return去返回。
PowerMockitoRunner
继承Junit#Runner
类启动的时候会去调用runwith
方法Junit框架就会去加载PowerMokito#PowerMockJUnitRunnerDelegateImpl
类。
在Whitebox#findSingleFieldUsingStrategy
会去把带有@Mock/@InjectMocks
注解的参数找出来并进行初始化。
最后在Whitebox#newInstance
会生成Mock
对象。
一般都会在单元测试初始化的时候进行“打桩”比如when...thenReturn....
生成Stubbing
在返回时会生成后就会更新打桩进度并生成好相应的代理。
performStubbing
会去调用createMock
生成代理对象。
在做代理时会调用MockMaker
类生成Mock
对象。
PowerMockMaker/CglibMockMaker
都实现了MockMaker
接口 这里PowerMockito支持多种字节码增强改写方式。目前使用的CGLIB方式。
ClassImposterizer#createProxyClass
使用CGLIB
字节码工具的Enhance
类对单元测试目标类进行代理并插入方法拦截类对象。
定义的方法拦截类。
这样以后就新建了一个代理类对象当匹配到某个方法执行时就会将预期的结果返回因为整个PowerMockito框架实现还是较多的这里还搜到了一个比较简单的例子方便理解:
public class PowerMockito {
private static Map<Invocation, Object> results =
new HashMap<Invocation, Object>();
private static Invocation lastInvocation;
public static <T> T mock(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(new MockInterceptor());
return (T)enhancer.create();
}
private static class MockInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
Invocation invocation = new Invocation(proxy, method, args, proxy);
lastInvocation = invocation;
if (results.containsKey(invocation)) {
return results.get(invocation);
}
return null;
}
}
public static <T> When<T> when(T o) {
return new When<T>();
}
public static class When<T> {
public void thenReturn(T retObj) {
results.put(lastInvocation, retObj);
}
}
}
复制代码
5. 单元测试编写建议
以下是一些编写单元测试的建议:
- 心态要摆正写单测不是浪费时间而是为了更好地提升设计与实现的质量
- 后台开发都是分层次架构需要将测试用例分包管理分层编写单测
- 要尽可能提高代码的覆盖率想清楚测试数据集如边界/异常/失败等;
- 不要滥用Mock该调用的就调用不能为了达到方法覆盖率而忽略写单测最初的目的;
- 多使用单测生成工具提升编写效率为此笔者还封装了一个生成组件.
6.总结
本文是基于最近在项目开发中使用到单测的知识整理主要讲述了单测的定义与作用现在使用到的Java后台开发的Junit与Mock框架的基本使用与实现原理最后是讲述了笔者自己的编写单元测试一些心得与建议。
参考文献
- blog.csdn.net/unifirst/ar… 单元测试框架对比
- blog.csdn.net/qq_26295547… junit框架详细介绍
- tech.youzan.com/code-covera… 浅谈代码覆盖率
- zhuanlan.zhihu.com/p/144826192 代码覆盖率
- blog.csdn.net/weixin_4236…
- blog.csdn.net/weixin_4433… 如何写好单元测试
作者代码的色彩
链接https://juejin.cn/post/6954645626961264653
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。
共同学习,写下你的评论
评论加载中...
作者其他优质文章