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
几乎所有的编程语言都有单元测试框架
先来感受下,一个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(“5加1后:%d\n”, x);
int y = 9;
decrement_value(&y);
printf(“9减1后:%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是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(默认加载器)识别和加载了测试用例。
将测试代码和源代码分开到不同文件很有必要。
第三方模块,需要安装
框架封装的更智能,用例发现更简单(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 ========================================================
'''
默认fE
f -失败
E -误差
s 跳过
x -失败
X -XPASS
p 通过
P -通过输出
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__])