ckdk's blog

单元测试

2019/01/14

什么是单元测试?

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

为什么要写单元测试?

写单元测试似乎是一件很枯燥很乏味的工作,又似乎需要花费一些时间。那我们为什么要写单元测试那?

1
2
3
4
5
1. 通过编写测试代码更能明白你所做应用的目的和意图,没有测试,这些将会变得很模糊。
2. 通过单元测试能使你很容易的发现在引入新代码之后所带来的问题,没有单元测试,
你可能不会发现这些问题,或者在遇到问题后你要花几个小时去寻找问题出在哪。
3. 有测试的代码更受人欢迎。
4. 测试有助于团队合作,测试能够减少同事无意间破坏你的代码的机会。

如何编写测试?

  • 单元测试的根本目的是验证代码行为是否符合预期,所以通常我们需要设计用例,验证在给定输入的情况下是否能得到我们想要的输出

在python中,通常使用自带的unittest或者第三方包nose进行单元测试。这里我们选择使用nose
进行测试。这里选择使用nose的原因是因为nose的使用更简单、快速、灵活,无需编写类。

nose使用:

  • 安装
1
pip install nose
  • 使用
1
2
3
4
nosetests [options] [test files or directories]

如果不加目录,默认执行当前目录下的所有符合nose条件的用例
如果加目录,则运行指定目录里面符合nose条件的用例

这里符合nose条件指的是当前目录及子目录下名字包含test开头的文件和目录中名字以test开头的方法

  • 示例
1
2
3
4
5
6
7
8
def tearDown():
print "teardown"
def test1():
print 1
def test2():
print 2
def setUp():
print "this setup"
1
2
3
4
5
6
7
8
9
10
11
12
13
nosetests -s -v  

setup
testnose.test1 ... 1
ok
testnose.test2 ... 2
ok
teardown
=
------------------------------------------------
Ran 2tests in 0.010s

OK

nose优先执行setUp,最后执行tearDown,和函数的位置没关系。在setUp中我们可以创建数据库及一些用的测试数据等准备工作,执行完测试用例后再在tearDown中删除测试数据并关闭连接。

  • nose中的函数

在nose中 我们可以使用一些工具函数来辅助我们验证结果 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# coding: utf8
from nose import tools

# 验证两个值是否相等
def test_1():
tools.assert_equal(True, False)


# 验证一个元素是否存在一个容器内
def test_2():
tools.assert_in(3, range(5))


# 通过正则匹配验证,django视图和爬虫比较适合
def test_3():
tools.assert_regexp_matches('<html><div>123</div><html>', r'<div>(.*?)</div>')


def test_4():
tools.assert_regexp_matches('<html><div>123</div><html>', r'<span>(.*?)</span>')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ nosetests -s -v test_sample.py 


test_sample.test_1 ... FAIL
test_sample.test_2 ... ok
test_sample.test_3 ... ok
test_sample.test_4 ... FAIL

======================================================================
FAIL: test_sample.test_1
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
self.test(*self.arg)
File "/home/deploy/test_sample.py", line 6, in test_1
tools.assert_equal(True, False)
AssertionError: True != False

======================================================================
FAIL: test_sample.test_4
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
self.test(*self.arg)
File "/home/deploy/test_sample.py", line 20, in test_4
tools.assert_regexp_matches('<html><div>123</div><html>', r'<span>(.*?)</span>')
AssertionError: Regexp didn't match: '<span>(.*?)</span>' not found in '<html><div>123</div><html>'

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=2)

从结果中可以看到两条不符合预期结果的测试用例没有通过。另外tools下还有还多其他方法供使用,很多从名字上就能看出是如何使用 大家自行参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
assert_almost_equal
assert_almost_equals
assert_dict_contains_subset
assert_dict_equal
assert_equal
assert_equals
assert_false
assert_greater
assert_greater_equal
assert_in
assert_is
assert_is_instance
assert_is_none
assert_is_not
assert_is_not_none
assert_items_equal
assert_less
assert_less_equal
assert_list_equal
assert_multi_line_equal
assert_not_almost_equal
assert_not_almost_equals
assert_not_equal
assert_not_equals
assert_not_in
assert_not_is_instance
assert_not_regexp_matches
assert_raises
assert_raises_regexp
assert_regexp_matches
assert_sequence_equal
assert_set_equal
assert_true
assert_tuple_equal
eq_
istest
make_decorator
nontrivial
nontrivial_all
nottest
ok_
raises
set_trace
timed
trivial
trivial_all
with_setup

在django里进行单元测试

使用django自带的单元测试框架

具体的用法就不赘述了,这里推荐在使用django时,可以为debug和测试环境下单独创建一个settings文件,这样在测试时可以指定--settings参数用特定的配置文件进行测试

-k参数可以保留数据库状态

1
python manage.py test app_name --settings project_name.settings-dev -k

使用nose进行单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# coding: utf8
import os
import json
import sys

import django
from django.test import Client
from nose import tools

client = Client()


def setUp():
try:
print "\n---------test start------------"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "prj_name.settings")
django.setup()
except:
sys.exit()


def test_nose():
res = client.get('/accounts/test/')
tools.assert_equal(res, True)


def tearDown():
print "\n---------test accounts end------------"
  • 我们引入Client类, 使用Clinet类可以方便的用来访问django里的链接地址(使用requests也是一个不错的选择)。
  • 在setUp中我们设置下django的配置文件,并启动django,用于Client访问链接

执行测试:

1
nosetests -s -v

另外, 对于其他的一些返回html的请求或者对于抓取网页内容的爬虫进行测试,个人建议可以验证返回的http状态码即可 或者通过正则匹配的方式验证是否包含我们想要的内容, 如

1
2
3
4
5
6
7
res = client.get('/xxx/xxx/')

# 验证返回的状态码
tools.assert_equal(res.status_code, 200)

# 正则验证
tools.assert_regexp_matches(res.content, r'想要验证的regx')
CATALOG
  1. 1. 什么是单元测试?
  2. 2. 为什么要写单元测试?
  3. 3. 如何编写测试?
    1. 3.1. nose使用:
    2. 3.2. 在django里进行单元测试
      1. 3.2.1. 使用django自带的单元测试框架
      2. 3.2.2. 使用nose进行单元测试