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
extern void increment_value(int * const value);
extern void decrement_value(int * const value);
#include <assert.h>
#include "assert_module.h"
void increment_value(int * const value) {
assert(value);
(*value) ++;
}
void decrement_value(int * const value) {
if (value) {
(*value) --;
}
}
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "assert_module.h"
static void increment_value_fail(void **state) {
(void) state;
increment_value(NULL);
}
static void increment_value_assert(void **state) {
(void) state;
expect_assert_failure(increment_value(NULL));
}
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);
}
#include <stdio.h>
#include "assert_module.h"
int main(int argc, char **argv) {
int x = 5;
increment_value(&x);
printf(“5加1后:%d\n”, x);
int y = 9;
decrement_value(&y);
printf(“9减1后:%d\n”, y);
return 0;
}
#ifndef __TOOL_H__
#define __TOOL_H__
#include <stdbool.h>
extern void readline(char *value);
extern bool isValidNum(char *value);
#endif
#include <stdio.h>
#include <string.h>
#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;
}
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <cmocka.h>
#include "tool.h"
static void test_isValidNum_1(void **state) {
(void)state;
char value[] = "tst";
bool want = false;
bool target = isValidNum(value);
assert_int_equal(want, target);
}
static void test_isValidNum_2(void **state) {
(void)state;
char value[] = "123";
bool want = true;
bool target = isValidNum(value);
assert_int_equal(want, target);
}
static void test_isValidNum_3(void **state) {
(void)state;
char value[] = "+123";
bool want = true;
bool target = isValidNum(value);
assert_int_equal(want, target);
}
static void isValidNum_4(void **state) {
(void)state;
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);
}
#include <stdio.h>
#include "tool.h"
#include <stdio.h>
#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文件运行模式
{
"version": "0.2.0",
"configurations": [{
"name": "mytest",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"args": ["-v"]
}]
}
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
import unittest
from widget import Widget
class Demo(unittest.TestCase):
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__':
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。
from .main import main
main(module=None)
-
unittest.main()即TestProgram()是unittest框架执行的入口,TestLoader(默认加载器)识别和加载了测试用例。 -
将测试代码和源代码分开到不同文件很有必要。
pytest
-
第三方模块,需要安装 -
框架封装的更智能,用例发现更简单(test_ 前缀函数, Test 前缀类),用户只需要按约定命名并import pytest即可
{
"version": "0.2.0",
"configurations": [{
"name": "mytest",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"module": "pytest",
"cwd": "${fileDirname}"
}]
}
def inc(x):
return x + 1
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
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=[
('sqlite_memory', 'sqlite:///:memory:'),
('sqlite_file', 'sqlite:///{dbfile}'),
],
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
url = url.format(dbfile=str(tmpdir / "db.sqlite"))
print('request:', id, url)
print('tmpdir:', tmpdir)
_engine = create_engine(url)
yield _engine
_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__])
总结
- 单元测试框架上手快
- 多写多练多看
- 异步多线程场景,编写单元测试比较困难
|