如何构筑测试体系 - 一点儿小的启发

757 阅读5分钟

前言

从昨天开始大部分同学都开始在家办公了,我也在14day home quarantine中,希望大家都健健康康,迎接春天的道来。

进入正题,《重构_改善既有代码的设计》一书中,曾提到,重构的第1步为建立一组可靠的测试环境,可见重构的目的不仅仅为了代码结构上的优化。

而且,在日常工作中,仅依赖于QA同学的测试用例,难免不完全,毕竟作为代码编写者的你,更加理解会有什么样的边界情况。

JUnit测试框架

日常工作中,也会用JUnit编写单元测试,但总觉得不太规整,而且有些测试用例略显盲目,不完全,故借书系统学习下。

  • 四大特性:

1、测试工具 2、测试套件 3、测试运行器 4、测试分类

测试工具

  • 测试工具的目的是为了确保测试能够在共享且固定的环境中运行。

1、在所有测试调用指令发起前的 setUp() 方法。 2、在测试方法运行后的 tearDown() 方法。

代码示例

public class JavaTest extends TestCase {


    private FileReader fileReader = null;

    /**
     * 初始化运行环境、使用资源
     *
     * @date 11:56 2020-02-11
     **/
    @Override
    protected void setUp() throws Exception {
        String fileName = "/Users/jigangpark/Work/JavaProjects/jvmtest/src/main/java/junit/data.txt";
        fileReader = new FileReader(fileName);
    }

    public void testPrint() throws Exception {

        char[] a = new char[50];
        // 读取数组中的内容
        fileReader.read(a);
        for (char c : a) {
            // 一个一个打印字符
            System.out.print(c);
        }
    }

    /**
     * 对于资源进行回收,关闭运行环境
     *
     * @date 12:01 2020-02-11
     **/
    @Override
    protected void tearDown() throws Exception {
        fileReader.close();
    }
}
  • 可以进入调试模式看下,当测试用例跑完,会走到fileReader.close()

测试套件

  • 捆绑几个测试案例并且同时运行,通常使用@RunWith@Suite注解来表示。

代码示例

/**
 * TestSuite.java
 * @date 2020-02-11 14:19
 **/
@RunWith(Suite.class)
@Suite.SuiteClasses({
        TestJunit1.class, TestJunit2.class
})
public class TestSuite {
}

/**
 * TestJunit1.java
 **/
public class TestJunit1 {

    String message = "Robert";
    MessageUtil messageUtil = new MessageUtil(message);

    @Test
    public void testPrintMessage() {
        System.out.println("Inside testPrintMessage()");
        Assert.assertEquals(message, messageUtil.printMessage());
    }
}

/**
 * TestJunit2.java
 **/
public class TestJunit2 {

    String message = "Robert";
    MessageUtil messageUtil = new MessageUtil(message);

    @Test
    public void testSalutationMessage() {
        System.out.println("Inside testSalutationMessage()");
        message = "Hi!" + "Robert";
        Assert.assertEquals(message, messageUtil.salutationMessage());
    }
}

/**
 * MessageUtil.java
 **/
public class MessageUtil {
    private final String message;

    public MessageUtil(String message) {

        this.message = message;
    }

    public String printMessage() {
        return message;
    }

    public String salutationMessage() {
        return "Hi!" + message;
    }
}
  • 运行TestSuite,会发现会运行TestJunit1TestJunit2两个测试类,运行结果如下:
// Tests passed:2 of 2 tests
Inside testPrintMessage()
Inside testSalutationMessage()

Process finished with exit code 0

测试运行器

  • 测试运行器用于执行测试用例,可以使用测试运行器

代码示例

public class TestRunner {
    public static void main(String[] args) {
        Result result = JUnitCore.runClasses(TestSuite.class);
        for (Failure failure : result.getFailures()) {
            System.out.println(failure.toString());
        }
        System.out.println(result.wasSuccessful());
    }
}
  • 运行结果如下,会运行测试套件TestSuite,同样会运行测试套件里的两个测试用例,这里我们故意使其中一个测试用例运行结果不正确。
Inside testPrintMessage()
Inside testSalutationMessage()
// 未通过的测试用例错误信息
testSalutationMessage(junit.TestJunit2): expected:<Hi[!]Robert> but was:<Hi[]Robert>
// 测试用例未全部通过
false

测试分类

测试分类是在编写和测试 JUnit 的重要分类。几种重要的分类如下:

1、包含一套断言方法的测试断言 2、包含规定运行多重测试工具的测试用例 3、包含收集执行测试用例结果的方法的测试结果

单元测试和功能测试

  • 笔者通常的做法是,针对一个接口(interface)使用插件生成Junit测试类,这样每个方法都会有对应的测试方法(@Test),但是显得毫无章法。

  • 其实上述情况都属于单元测试的范畴,而功能测试是用来保证软件能够正常运作,QA团队也是从客户的角度保障质量。一般而言,功能测试尽可能把整个系统当作一个黑盒,通过app、网页等GUI来操作整个系统,不会关心具体实现。

  • 针对功能测试暴露出来的问题,笔者也通常只是改改代码,无问题了即可。但书中给出了一个给笔者启发的原则。

每当你收到bug报告,请先写一个单元测试来暴露bug。

  • 笔者受到的启发:这样针对功能测试的bug,也能用Junit来表示,有时这些测试用例会比单纯插件生成的@Test方法有用。

结语

笔者从自己实际开发工作中,无法有效地组织单元测试结构的痛点出发,从《重构_改善既有代码的设计》中,得到了如下启发,笔者以后在实际开发中也会尽量采用这种思路来进行编码,希望对大家有些帮助。

1、使用一个测试套件来表示一个业务场景(可以是一个接口,一个功能涉及到的一些接口) 2、单元测试需要尽量包含边界情况(这些边界往往作为作者的你最清楚),QA团队会帮你捕捉大多数错误,但是一些细节错误还是要靠自己! 3、QA提出的功能bug要在测试套件中添加测试用例,可以认为使用Junit测试用例表示一个功能测试。 4、对功能进行修改后,运行一次测试套件,验证其准确性,往往会遇到改了代码,另一个地方出问题。

  • 路漫漫其修远兮,吾将上下而求索
  • 继续摸鱼去了!希望春天尽早来临!

参考文献