单元测试之道junit

一、分享前提问,一个复杂的功能怎么可以保证高效和质量?

 

A需求例如:

我们考虑出租车 (Taxi) 计价 (Calculate) 问题:

不大于 2 公里时只收起步价 6 元

超过 2 公里时每公里 0.8 元

超过 8 公里则每公里加收 50% 长途费

停车等待时加收每分钟 0.25 元

最后计价的时候司机 (Driver) 会四舍五入只收 (Charge) 到元

请写一个程序计算司机最终收费的数额。

 

 

B需求例如:每天监测各个机构的异常指标,假如异常就发送邮件及异常指标数据给对应机构的负责人。

  1. A机构,指标1:大于1个不同交费帐户退费至同一帐户数,指标2投保人变更后做保全退费的保单,指标N...
  2. B机构,指标1:投资尽调报告关键信息不完整的母基金项目,指标2:不存在立项建议书的直投项目,指标N...
  3. 机构N...

 

     B需求优化1:增加发送短信到对应机构的负责人

   B需求优化2:接收者可以配置指定接收人。

B需求优化3:把指标分成异常,正常,警告三种分别发送给负责人。

 


二、为什么需要单元测试

优势

  1. 提升代码质量
  2. 减少线上bug
  3. 提升项目上线成功率(因开发时已分析和测试大部分逻辑,后期进度可控)
  4. 增加代码可维护性(未来扩展,优化性能,找bug时)
  5. 最重要是减少加班时间

劣势

增加代码工作量,至少1比1~3的代码量

 

 

引用书的内容

 

 

 

 

 

 

三、使用junit编写单元测试

  1. 编写测试代码的步骤

l 准备测试的所需要的各种条件(创建所有必须的对象,分配必要的资源等等)

l 调用要测试的方法

l 验证被测试的方法的行为和期望值是否一致

l 完成后清理各种资源

  1. 认识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...

从上面的结果我们来画一张流程图就知道了:

 

 

 

  1. 案例

这里我们要测试的功能超级简单,就是加减乘除法的验证。

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的员工分享

TDD理论基础_哔哩哔哩_bilibili

TDD实战@计算斐波那契数_哔哩哔哩_bilibili

posted @ 2022-10-14 22:40  ihuotui  阅读(73)  评论(0编辑  收藏  举报