单元测试框架

https://en.wikipedia.org/wiki/Unit_testing

https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks

https://docs.python-guide.org/writing/tests/

https://docs.python.org/zh-cn/3/library/unittest.html#module-unittest

https://docs.python.org/zh-cn/3/using/cmdline.html#cmdoption-m

https://docs.python.org/3/library/main.html?highlight=main#module-main

https://docs.pytest.org/en/latest/

https://github.com/scrapy/scrapy/tree/master/tests

单元测试定义

  • 在不运行最终应用程序前,先为最小的可测试单元编写测试,然后为它们之间的复合行为编写测试,以确保某个部分符合其设计并按预期运行,可以为复杂的应用程序构建全面的测试。
  • 最小的可测试单元可以是一个的函数,一个类,一个接口,一个模块等。

单元测试意义

  • 编写单元测试,需要一定的‘代价’,因为质量不高的最小单元,是写不出好的单元测试的。
  • 付出‘代价’的同时,带来的优势(相互影响包含)。
    • 提高质量,强迫向大师看齐
      • 逼迫遵守单一职责原则,保证单一功能
      • 逼迫遵守低耦合原则,毕竟职责单一
      • 逼迫遵守输入输出无状态,即函数式编程理念,不要滥用全局变量等特技
      • 逼迫遵守编码规范,框架规范(不想自己造轮子,就要使用开源测试框架,就要按套路组织文件,组织命名)
    • 提高可维护性,保证重构质量
      • 新老交接,单元测试是活的文档,有助于新人熟悉功能和业务
      • 重构过程,保证之前的测试用例可以通过
    • 提高自动化程度
      • 单元测试注重的粒度更小,可以和接口测试,UI测试,集成测试有机结合
      • 可以渗透到自动化,持续集成,持续构建,持续交付,静态分析中(如Jenkins代码测试覆盖率,通过率)
    • 传承开源文化(分享,互助,团队)
      • 单元测试和代码评审(code review),已成为软件工程标准化流程
      • 讨论交流中取长补短
      • 大佬呼吁,没有代码评审和单元测试文化的公司,请离开

单元测试框架

  • 几乎所有的编程语言都有单元测试框架

  • 先来感受下,一个c语言的单元测试框架Cmocka

    /**
     * https://github.com/clibs/cmocka/blob/master/example/assert_module.h
     * 头文件
     */
    //将value指向的值加一
    extern void increment_value(int * const value);
    //将value指向的值减一
    extern void decrement_value(int * const value);
    
    
    /**
     * https://github.com/clibs/cmocka/blob/master/example/assert_module.c
     * 源文件
     */
    #include 
    #include "assert_module.h"
    
    void increment_value(int * const value) {
        assert(value);
        (*value) ++;
    }
    
    void decrement_value(int * const value) {
        if (value) {
          (*value) --;
        }
    }
    
    
    /**
     * https://github.com/clibs/cmocka/blob/master/example/assert_module_test.c
     * 测试入口文件
     */
    #include 
    #include 
    #include 
    #include 
    #include "assert_module.h"
    
    /**
     *increment_value执行过程,assert(value)为false,抛出异常
     */
    static void increment_value_fail(void **state) {
        (void) state;
    
        increment_value(NULL);
    }
    
    /**
     *increment_value执行过程,assert(value)为false,抛出异常
     *expect_assert_failure期望异常出现,用例通过
     */
    /* This test case succeeds since increment_value() asserts on the NULL
     * pointer. */
    static void increment_value_assert(void **state) {
        (void) state;
    
        expect_assert_failure(increment_value(NULL));
    }
    
    /**
     *decrement_value执行过程,if (value)为false,没有运行(*value) --,也没有异常
     *expect_assert_failure期望异常未出现,用例失败
     */
    static void decrement_value_fail(void **state) {
        (void) state;
    
        expect_assert_failure(decrement_value(NULL));
    }
    
    int main(void) {
        const struct CMUnitTest tests[] = {
            cmocka_unit_test(increment_value_fail),
            cmocka_unit_test(increment_value_assert),
            cmocka_unit_test(decrement_value_fail),
        };
        return cmocka_run_group_tests(tests, NULL, NULL);
    }
    
    /**
     * main.c
     * 应用入口文件
     */
    #include 
    #include "assert_module.h"
    
    int main(int argc, char **argv) {
        int x = 5;
        increment_value(&x);
        printf(51后:%d\n”, x);
        
        int y = 9;
        decrement_value(&y);
        printf(91后:%d\n”, y);
        
        return 0;
    }
    
    /*
     * 头文件tool.h
     */
    #ifndef __TOOL_H__
    #define __TOOL_H__
    
    #include 
    
    extern void readline(char *value);
    extern bool isValidNum(char *value);
    
    #endif
    
    /*
     * 源文件tool.c
     */
    #include 
    #include 
    #include "tool.h"
    
    void readline(char *value) {
      int position = 0;
      char c;
      while ((c = getchar()) != '\n') {
        value[position++] = c;
      }
      value[position] = '\0';
    }
    
    bool isValidNum(char *value) {
      if (!value) {
        return false;
      }
    
      //正负标志在第一位跳过
      if (*value == '-' || *value == '+') {
        value += 1;
      }
    
      int position = 0;
      while (*value) {
        char c = *value;
        //数字
        if (c < '0' || c > '9') {
          return false;
        }
        position++;
        value += 1;
      }
    
      return position > 0;
    }
    
    /*
     * 单元测试入口test.c
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "tool.h"
    
    static void test_isValidNum_1(void **state) {
      (void)state; /* unused */
      char value[] = "tst";
      bool want = false;
      bool target = isValidNum(value);
      assert_int_equal(want, target);
    }
    
    static void test_isValidNum_2(void **state) {
      (void)state; /* unused */
      char value[] = "123";
      bool want = true;
      bool target = isValidNum(value);
      assert_int_equal(want, target);
    }
    
    static void test_isValidNum_3(void **state) {
      (void)state; /* unused */
      char value[] = "+123";
      bool want = true;
      bool target = isValidNum(value);
      assert_int_equal(want, target);
    }
    
    static void isValidNum_4(void **state) {
      (void)state; /* unused */
      char value[] = "1-23";
      bool want = true;
      bool target = isValidNum(value);
      assert_int_equal(want, target);
    }
    
    int main(int argc, char **argv) {
      const struct CMUnitTest tests[] = {
          cmocka_unit_test(test_isValidNum_1), cmocka_unit_test(test_isValidNum_2),
          cmocka_unit_test(test_isValidNum_3), cmocka_unit_test(isValidNum_4)};
    
      return cmocka_run_group_tests(tests, NULL, NULL);
    }
    
    /*
     * 应用入口main.c
     */
    #include 
    #include "tool.h"
    
    #include 
    #include "tool.h"
    
    int main(int argc, char **argv) {
      char tmp[255];
    
      while (true) {
        readline(tmp);
        if (isValidNum(tmp)) {
          printf("%s is number!\n", tmp);
        } else {
          printf("%s is not number!\n", tmp);
        }
      }
    
      return 0;
    }
    
  • 用例通过:期望值==运行值;用例失败:期望值!=运行值。

  • 测试assert_module_test.c和assert_module.h assert_module.c是隔离的,互不影响。

  • 编写一个main.c文件,作为正式的程序入口;assert_module_test.c作为单元测试的入口。

  • 正式编译指定main.c,单元测试指定assert_module_test.c。

  • 结合编程语言本身特点,测试框架可以隐藏入口文件,通过@Test,文件Test_等魔法分离发布代码和测试代码,前提条件,遵守约定(类似Servlet规范等);指定运行入口(如pytest)。

unittest

  • unittest是python标准模块,风格类似JUnit。

  • demo文件运行模式

    #launch.json
    {
      "version": "0.2.0",
      "configurations": [{
        "name": "mytest",
        "type": "python",
        "request": "launch",
        "program": "${file}",
        "console": "integratedTerminal",
        "args": ["-v"]
      }]
    }
    
    #widget.py
    class Widget():
    
      def __init__(self, name):
        self.name = name
        self.width = 50
        self.height = 50
    
      def size(self):
        return (self.width, self.height)
    
      def resize(self, width, height):
        self.width = width
        self.height = height
    
    #demo.py
    import unittest
    from widget import Widget
    
    
    # unittest.TestCase提供了测试样例运行的基本框架
    class Demo(unittest.TestCase):
    
      # 每一个测试用例必须以test开头,assert开头方法比较预期值和运行值
      def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')
    
      def testadd(self):
        self.assertTrue(2 + 3 == 6)
    
      def tstadd(self):
        self.assertTrue(2 + 3 == 5)
    
    
    class Tst(unittest.TestCase):
    
      def testin(self):
        self.assertNotIn('n', ['h', 'e', 'l', 'l', 'o'])
    
    
    class WidgetTest(unittest.TestCase):
    
      #前置操作,创建资源等
      def setUp(self):
        self.widget = Widget('The widget')
    
      def test_default_widget_size(self):
        value = self.widget.size()
        self.assertEqual(value, (50, 50), 'incorrect default size')
    
      def test_widget_resize(self):
        self.widget.resize(100, 150)
        value = self.widget.size()
        self.assertEqual(value, (100, 150), 'wrong size after resize')
    
      #后置操作,清理资源等
      def tearDown(self):
        pass
    
    
    if __name__ == '__main__':
      #launch配置-v显示更详细信息
      unittest.main()
        
        
    #运行结果
    '''
    test_upper (__main__.Demo) ... ok
    testadd (__main__.Demo) ... FAIL
    testin (__main__.Tst) ... ok
    test_default_widget_size (__main__.WidgetTest) ... ok
    test_widget_resize (__main__.WidgetTest) ... ok
    
    ======================================================================
    FAIL: testadd (__main__.Demo)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "d:\testdemo\demo.py", line 13, in testadd
        self.assertTrue(2 + 3 == 6)
    AssertionError: False is not true
    
    ----------------------------------------------------------------------
    Ran 5 tests in 0.006s
    
    FAILED (failures=1)
    '''
    
  • demo命令行运行模式,让框架自己找到测试单元,或指定测试单元

    python -m unittest -v demo.py            #指定demo.py文件
    python -m unittest -v                    #将demo.py改名为test*.py
    python -m unittest -v demo.Demo          #指定测试类
    python -m unittest -v demo.Demo.tstadd   #指定测试函数
    
  • python -m参数:在 sys.path 中搜索指定名称的模块并将其内容作为 __main__ 模块来执行,运行的是该模块下__mian__.py。

    #unittest模块下__main__.py
    from .main import main
    
    main(module=None) #unittest.main()
    
  • unittest.main()即TestProgram()是unittest框架执行的入口,TestLoader(默认加载器)识别和加载了测试用例。

  • 将测试代码和源代码分开到不同文件很有必要。

pytest

  • 第三方模块,需要安装

  • 框架封装的更智能,用例发现更简单(test_ 前缀函数, Test 前缀类),用户只需要按约定命名并import pytest即可

    #luanch.json
    {
      "version": "0.2.0",
      "configurations": [{
        "name": "mytest",
        "type": "python",
        "request": "launch",
        "program": "${file}",
        "console": "integratedTerminal",
        "module": "pytest",
        "cwd": "${fileDirname}"
      }]
    }
    
    #demo.py
    def inc(x):
      return x + 1
    
    #test_demo.py
    import pytest
    from demo import inc
    
    
    def test_answer():
      assert inc(3) == 5
    
    
    class TestDemo():
    
      def testinc(self):
        assert inc(8) == 9
     
    
    '''
    =========================================================== test session starts ============================================================
    platform win32 -- Python 3.7.5, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
    rootdir: D:\testdemo
    collected 2 items                                                                                                                            
    
    test_demo.py F.                                                                                                                       [100%] 
    
    ================================================================= FAILURES ================================================================= 
    _______________________________________________________________ test_answer ________________________________________________________________ 
    
        def test_answer():
    >     assert inc(3) == 5
    E     assert 4 == 5
    E      +  where 4 = inc(3)
    
    test_demo.py:6: AssertionError
    ======================================================= 1 failed, 1 passed in 0.14s ========================================================
    '''
    
指定测试用例
  • 在模块中运行测试 pytest test_mod.py
  • 在目录中运行测试 pytest testing/
  • 按节点ID运行测试 pytest test_mod.py::test_func或者pytest test_mod.py::TestClass::test_method
  • 从包运行测试 pytest --pyargs pkg.testing
跟踪回溯打印
  • –showlocals # show local variables in tracebacks
  • -l # show local variables (shortcut)
  • –tb=auto # (default) ‘long’ tracebacks for the first and last # entry, but ‘short’ style for the other entries
  • –tb=long # exhaustive, informative traceback formatting
  • –tb=short # shorter traceback format
  • –tb=line # only one line per failure
  • –tb=native # Python standard library formatting
  • –tb=no # no traceback at all
详细总结报告-r

默认fE

  • f -失败

  • E -误差

  • s 跳过

  • x -失败

  • X -XPASS

  • p 通过

  • P -通过输出

保存结果
  • JUnitXML格式文件 pytest --junitxml=path
  • 结果日志格式文件 pytest --resultlog=path
代码调用pytest
  • pytest.main()
fixtures
  • 初始化测试功能,它们提供了一个固定的基线,以便测试可靠地执行并产生一致的、可重复的结果。初始化可以设置服务、状态或其他操作环境。在fixture函数中,每个函数的参数通常在test之后被命名为fixture
  • pytest fixtures相对于传统的xUnit风格的setup/teardown函数提供了显著的改进
  • conftest.py共享初始化
  • @pytest.fixtures(params=),params可循环序列,request.param . 无需更改测试功能代码。让我们再跑一次
import pytest
from sqlalchemy import create_engine, exc, inspect, text


@pytest.fixture(
    params=[
        # request: (sql_url_id, sql_url_template)
        ('sqlite_memory', 'sqlite:///:memory:'),
        ('sqlite_file', 'sqlite:///{dbfile}'),
        # ('psql', 'postgresql://records:records@localhost/records')
    ],
    ids=lambda r: r[0]
)
def db(request, tmpdir):
    """Instance of `records.Database(dburl)`

    Ensure, it gets closed after being used in a test or fixture.

    Parametrized with (sql_url_id, sql_url_template) tuple.
    If `sql_url_template` contains `{dbfile}` it is replaced with path to a
    temporary file.

    Feel free to parametrize for other databases and experiment with them.
    """
    id, url = request.param
    # replace {dbfile} in url with temporary db file path
    url = url.format(dbfile=str(tmpdir / "db.sqlite"))
    print('request:', id, url)
    print('tmpdir:', tmpdir)
    _engine = create_engine(url)
    yield _engine  # providing fixture value for a test case
    # tear_down
    _engine.dispose()


@pytest.fixture
def foo_table(db):
    """Database with table `foo` created

    tear_down drops the table.

    Typically applied by `@pytest.mark.usefixtures('foo_table')`
    """
    db.connect().execute(text('CREATE TABLE foo (a integer)'))
    yield
    db.connect().execute(text('DROP TABLE foo'))


def test_aaa(db):
    db.connect().execute(text("CREATE table users (id text)"))
    db.connect().execute(text("SELECT * FROM users WHERE id = :user"), user="Te'ArnaLambert")


if __name__ == "__main__":
    pytest.main([__file__])

总结

  • 单元测试框架上手快
  • 多写多练多看
  • 异步多线程场景,编写单元测试比较困难

你可能感兴趣的:(python库学习,单元测试)