为什么我们要写Unit test? 因为随着代码量的增加, 开发人员会忘记, 因此需要Unit test帮助我们保证程序的可靠性. 尤其是我们的程序涉及到医疗信息, 关乎生命安全, 或关乎其他人的资金.
1. test文件准备
当我们使用manage.py startapp创建新的Django app时, django会为我们创建test.py. 我们需要做的第一步是删除该文件, 然后建立test_models.py, test_forms.py, test_views.py文件:
myapp/ __init__.py admin.py forms.py models.py test_forms.py test_models.py test_views.py views.py
如果项目中出现其他代码文件, 则建立相应的test文件. 这样做的原因是使测试文件扁平化, 方便我们更容易浏览和修改. 注意, 则是文件必须以"test_"开头, 否则django无法发现这些测试文件.
2. 如何写 Unit Tests
a. 每个method只测试一项
每个test method应当尽量减少其测试的范围, 不要尝试在一个method中测试多个views, models, forms.
当然, 这里也会出现难题, 因为通常一个view会涉及到models, forms, 其他methods和functions. 此时我们就最简化我们的环境:
# 测试 REST Api # test_api.py import json from django.core.urlresolvers import reverse from django.test import TestCase from myapp.models import Article class ArticleTests(TestCase): def setUp(self): Article.objects.get_or_create(title="A title", slug="a-slug") def test_list(self): url = reverse("article_object_api") response = self.client.get(url) self.assertEquals(response.status_code, 200) data = json.loads(response.content) self.assertEquals(len(data), 1)
以上代码中, 我们使用setUp method, 最简化了我们需要的model. 以下是一个更为完整的例子, 测试使用django_rest_framework构建的REST API:
# test_api.py import json from django.core.urlresolvers import reverse from django.test import TestCase from django.utils.http import urlencode from myapp.models import Article class ArticleTests(TestCase): def setUp(self): Article.objects.get_or_create(title="title1", slug="slug1") Article.objects.get_or_create(title="title2", slug="slug2") self.create_read_url = reverse("article_rest_api") self.read_update_delete_url = reverse("article_rest_api", kwargs={"slug": "slug1"}) def test_list(self): response = self.client.get(self.create_read_url) # content中, 两个title是否都有? self.assertContains(response, "title1") self.assertContains(response, "title2") def test_detail(self): response = self.client.get(self.read_update_delete_url) data = json.loads(response.content) content = {"id": 1, "title": "title1", "slug": "slug1"} self.assertEquals(data, content) def test_create(self): post = {"title": "title3", "slug": "slug3"} response = self.client.post(self.create_read_url, post) data = json.loads(response.content) self.assertEquals(response.status_code, 201) content = {"id": "3", "title": "title3", "slug": "slug3"} self.assertEquals(data, content) self.assertEquals(Article.objects.count(), 3) def test_delete(self): response = self.client.delete(self.read_update_delete_url) self.assertEquals(response.status_code, 204) self.assertEquals(Article.objects.count(), 1)
b. 使用Request Factory测试view
django.test.client.RequestFactory可以生成用于view的request. 这样为我们提供了很大的便利性来定义request, 并能与标准test client隔离. 但是需要注意的是, request factory不支持middleware, 包括session和authentication.
c. 保持简单
tests应当尽可能的保持简单. 如果一个tesst中的code过于复杂, 那么我们可能需要为这一test再写一个test, 可见在这会在debug时造成多大的痛苦.
d. "不重复"原则不适用于 tests
有时我们需要相似但不同的数据来运行每个test method, 我们可能会尝试将他们写到一起, 并通过修改几个参数来达到test的目的. 但请不要这么做, 最好的方式其实是copy/paste这些代码, 使其扁平化.
e. 不要依赖fixtures
开发过程中, 数据结构的改变导致fixtures难以维护, 之前保存的fixture可能已经不再适用于现在的test. 因此尽量使用django自带的ORM来取代fixture.
也有其他开发人员喜欢使用以下这些工具:
- factory boy: 用于生成model数据
- model mommy: 用于生成model 数据
- mock: 不只是用于django, 已被纳入Python 3.3 标准库
f. 测试覆盖范围
以下代码都应当被测试:
- Views: 数据的改变, 自定义的method
- Models: model的创建/更新/删除, model的method, model manager的method
- Forms: form method, clean() method, 自定义field
- Validators
- Signals
- Filters
- Template Tag
- 其他: context processors, middleware, email等
g. 文档
对于test class和method, 都应当说明测试目的.
使用coverage.py
coverage.py为我们提供了哪些部分被测试了, 哪些部分未被测试的详细信息.
原文链接: http://www.weiguda.com/blog/31/