一、分享前提问,一个复杂的功能怎么可以保证高效和质量?
A需求例如:
我们考虑出租车 (Taxi) 计价 (Calculate) 问题:
l 不大于 2 公里时只收起步价 6 元
l 超过 2 公里时每公里 0.8 元
l 超过 8 公里则每公里加收 50% 长途费
l 停车等待时加收每分钟 0.25 元
l 最后计价的时候司机 (Driver) 会四舍五入只收 (Charge) 到元
请写一个程序计算司机最终收费的数额。
B需求例如:每天监测各个机构的异常指标,假如异常就发送邮件及异常指标数据给对应机构的负责人。
- A机构,指标1:大于1个不同交费帐户退费至同一帐户数,指标2投保人变更后做保全退费的保单,指标N...
- B机构,指标1:投资尽调报告关键信息不完整的母基金项目,指标2:不存在立项建议书的直投项目,指标N...
- 机构N...
B需求优化1:增加发送短信到对应机构的负责人
B需求优化2:接收者可以配置指定接收人。
B需求优化3:把指标分成异常,正常,警告三种分别发送给负责人。
二、为什么需要单元测试
优势
- 提升代码质量
- 减少线上bug
- 提升项目上线成功率(因开发时已分析和测试大部分逻辑,后期进度可控)
- 增加代码可维护性(未来扩展,优化性能,找bug时)
- 最重要是减少加班时间
劣势
增加代码工作量,至少1比1~3的代码量
引用书的内容
三、使用junit编写单元测试
- 编写测试代码的步骤
l 准备测试的所需要的各种条件(创建所有必须的对象,分配必要的资源等等)
l 调用要测试的方法
l 验证被测试的方法的行为和期望值是否一致
l 完成后清理各种资源
- 认识junit
l TestCase:字面意思,测试用例。为一个或多个方法提供测试方法,一般是一个类对应一个case(case里面包含多个方法的测试)。
TestSuite:测试集合,即一组测试。一个test suite是把多个相关测试归入一组的快捷方式。如果自己没有定义,Junit会自动提供一个test suite ,包括TestCase中的所有测试。
TestRunner:测试运行器。执行test suite的程序。
l TestResult:集合了执行测试样例的所有结果
l 断言Assert:void assertEquals(boolean expected, boolean actual)
检查两个变量或者等式是否平衡;void assertFalse(boolean condition);void assertNotNull(Object object)
检查对象不是空的;也可以验证异常情况assertTrue(e instanceof NumberFormatException);
l 运行流程:@BeforeClass;@AfterClass;@Before;@After
public class JunitFlowTest {
@BeforeClass
public static void setUpBeforeClass() throws Exception {
System.out.println("beforeClass...");
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
System.out.println("afterClass...");
}
@Before
public void setUp() throws Exception {
System.out.println("before...");
}
@After
public void tearDown() throws Exception {
System.out.println("after");
}
@Test
public void test1() {
System.out.println("test1方法...");
}
@Test
public void test2(){
System.out.println("test2方法...");
}}
在上面的代码中,我们使用了两个测试方法,还有junit运行整个流程方法。我们可以运行一下,就会出现下面的运行结果:
beforeClass...
before...
test1方法...
afterbefore...
test2方法...
afterafterClass...
从上面的结果我们来画一张流程图就知道了:
- 案例
这里我们要测试的功能超级简单,就是加减乘除法的验证。
public class Calculate {
public int add(int a,int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a,int b) {
return a * b;
}
public int divide(int a ,int b) {
return a / b;
}}
然后我们看看如何使用junit去测试。
public class CalculateTest {
@Test
public void testAdd() {
assertEquals(2, new Calculate().add(1,1));
}
@Test
public void testSubtract() {
assertEquals(8, new Calculate().subtract(10,2));
}
@Test
public void testMultiply() {
assertEquals(6, new Calculate().multiply(3, 2));
}
@Test
public void testDivide() {
assertEquals(5, new Calculate().divide(10, 2));
}}
使用测试套件,把这些测试类嵌套在一起。
@RunWith(Suite.class)@Suite.SuiteClasses({CalculateTest.class,Test1.class,Test2.class等相关测试类})public class SuiteTest {
/*
* 写一个空类:不包含任何方法
* 更改测试运行器Suite.class
* 将测试类作为数组传入到Suite.SuiteClasses({})中
*/
}
四、好的单元测试所具有的品质
基本的单元测试要求Right-BICEP
l Right-结果是否正确?
l B-是否所有边界条件都是正确?(null,数字极值,正负数,非正常输入等)
l I-能查一下反向关联吗?(例如插入数据库,使用查询该记录)
l C-能用其他手段交叉检查结果?(算法,能否用其他算法验证)
l E-是否检查错误条件?(读取文件,请求其他系统的接口,有没有考虑异常)
l P-是否满足性能要求?
好的单元测试所具有的品质
l 自动化(调用测试的自动化,检查结果的自动化)
l 彻底化(测试覆盖率高,如代码的每个分支,可能抛出的异常,极端数据等)
l 可重复
l 独立的
l 专业的(使用设计模式,提高测试效率,例如公共的逻辑可以抽出抽象的类,让子类实现具体不同的业务,在多重逻辑判断中,使用过滤器设计模式,等)
l 代码可测试性高(当一个方法无法简单写出对应单元测试,即代码需要重构拆分逻辑,方法的逻辑尽量简单,一个方法只做一个事情)
l 测试与评审(让组员同事互相评审,或者交叉写单元测试)
五、在项目中进行单元测试
l Mock对象(当方法需要请求其他系统时,使用Mock模拟其他系统的接口的结果)
网络搜索Mockito的使用方法
六、面向测试的设计
对应上文的代码可测试性,与下文的TDD,其实这个实现起来牵涉内容很多,简单说,最好是易于编写单元测试,方法模块逻辑解耦等,通俗说就是一个方法只做一个功能,这样就能写出对应的单元测试。
例如:编写了一个周期执行任务的功能,那么可以或者执行时间的值,来判断是否正常,而不是等待任务执行。
例如:一个方法逻辑比较复杂,总行数越写越长,需要思考是否有逻辑重复,不同业务逻辑是否解耦。
举例:某系统的批量文件转换pdf任务。
原代码:
public String batchWord2Pdf(String orgCode) {
// 获取各个机构公司的文件信息及转换
if (orgCode.equals(Constant.ORG.orgA)) {
//获取A文件及转换
orgAWord2Pdf();
}
if (orgCode.equals(Constant.ORG.orgB)) {
//获取B文件及转换
orgBWord2Pdf();
}
if (orgCode.equals(Constant.ORG.orgC)) {
//获取C文件及转换
orgCWord2Pdf();
}
return "Y";
}
新增一个机构就需要增加较多的代码,见项目代码
com.xxx.service.impl.FileConvertServiceImpl(一个大类把所有功能都写在一起,已经600多行了,代码行数太多也是坏味道)
可见业务代码和转换逻辑代码耦合,且业务代码中存在重复逻辑的代码,这在单元测试和业务测试产生重复的工作量,如果继续新增机构N估计超过千行,而且假如逻辑有bug需要修复,修改代码行数N倍,增加需求代码也是N倍,测试也是N倍。
合理的代码设计,应该是把转换流程,记录转换结果标识到数据库的流程抽象,取各个机构的具体文件数据在子类。
处理过程抽象类
获取orgA文件类
获取orgB文件类
转换PDF类
同时降低单元测试的数量,再新增机构也不会成倍的增加测试工作量。
原代码
|
重构后 |
||
机构 |
测试内容 |
机构 |
测试内容 |
orgA |
查询文件,判断是否转换,转换,保存转换记录(或记录异常) |
orgA |
查询文件 |
orgB |
查询文件,判断是否转换,转换,保存转换记录(或记录异常) |
orgB |
查询文件 |
orgN |
查询文件,判断是否转换,转换,保存转换记录(或记录异常) |
orgN |
查询文件 |
|
|
转换流程(通用方法) |
判断是否转换,转换,保存转换记录(或记录异常) |
|
|
文件转换PDF(通用方法) |
请求FileConvert服务转换 |
举例:spring的filter设计
相信大家都可能用过spring的filter来实现登录拦截,流程如下
filter的责任链模式设计可以为一个Web应用组件部署多个过滤器,这些过滤器组成一个过滤器链,每个过滤器只执行某个特定的操作或者检查。
具体代码请看spring源码及设计模式的责任链模式。大家在项目实践中可以来处理某些数据是否满足哪些条件规则,这样在开发不同规则都是独立可测的,可以放心的新增或者修改规则。
案例:阿里巴巴的RocketMQ的储存消息默认实现DefaultMessageStore
类的方法
对应的单元测试
七、测试驱动开发TDD与结对编程
1先实现TDD测试驱动开发
大概流程:
1.1.编写一个失败的单元测试。
1.2.修改产品代码使之通过单元测试。
1.3.重构单元测试和产品代码。
(在重复这个过程中会发现代码是否方便测试,逻辑是否复杂,逻辑是否解耦,是否可测,及可以重构)
2 找一个同事伙伴基友,你写代码,ta写单元测试
参考资料
书籍
《单元测试之道-使用JUnit(Java版)].Andrew.Hunt》
《重构 改善既有代码的设计》
《设计模式》
视频资料:thoughtworks的员工分享
标签:...,转换,代码,单元测试,public,之道,测试,junit From: https://www.cnblogs.com/ihuotui/p/16793249.html