django 集成个推_Django 项目单元测试的几个套路

单元测试,可以说是软件工程当中降低开发成本、提高软件质量的最常用手段之一。不过,相信很多同学在项目当中尝试或者推广单元测试的时候,都会遇到这样那样的实际问题,又无法得到解答。

下面是我遇到的几个常见问题和解决方案,在此整理一下。

mock http 请求

很多项目当中都需要做系统集成,或者说软件工程很大程度上就是个做集成的活儿。很多模块势必需要调用其他系统的接口,最常见的例子就是发送短信通知,比如下面这个例子:

import requests

def sendCode(phone, code):

data = {'phone': phone, 'code': code, 'apiKey': API_KEY}

res = requests.post('https://message.api.com/api/v1/send-code', data=data)

# error handling code...

saveCode(phone, code)

如果不考虑 http 请求的问题,单元测试一般会写成下面这个样子:

class SendCodeTestCase(AuditTestCase)

def test_send_code(self):

sendCode('18888888888', '123456')

code = self.getSavedCode('18888888888')

self.assertEqual(code, '123456')

但是,这个测试用例是有问题的,至少是有副作用的,每次执行都得发 http 请求。

解决方案其实很简单,可以使用 responses 模块来 mock http 请求。使用方式如下:

+import responses

class SendCodeTestCase(AuditTestCase)

+ @responses.activate def test_send_code(self):

+ responses.add(responses.GET, 'https://message.api.com/api/v1/send-code',+ json={'error': 'not found'}, status=404)

sendCode('18888888888', '123456')

code = self.getSavedCode('18888888888')

self.assertEqual(code, '123456')

仔细看,你还能发现,使用 responses 库 mock http 请求的另一大好处是,能够模拟 http 请求出错的情况。

fake time

有很多时候,我们需要测试和时间有关的逻辑。举一个常见的例子是检查 token 是否超时:

def validateToken(token):

now = timezone.now()

data = getDataByToken(token)

if now > data.expiredAt:

return 'token-expired'

# some other validation rules...

其中的 now = timezone.now() 语句每次执行测试的时候都会边,而且都是系统当前时间。解决方案是使用 freegun 这个模块:

from freezegun import freeze_time

class TokenTestCase(AuditTestCase)

def test_validate_token(self):

token = None

with freeze_time(lambda: datetime.datetime(2012, 1, 14)):

token = fakeToken(userId='jack')

with freeze_time(lambda: datetime.datetime(2012, 1, 22)):

error = validateToken(token)

self.assertEqual(error, 'token-expired')

假设 token 有效期是 7 天,使用 freegun 就可以模拟 token 生成后时间超过七天 token 失效的场景。

依赖注册/注入

有的时候,我们要对接的系统不是通过 http 接口来集成的,而是对方提供了 client 库,比如七牛,参考官方文档提供的例子:

import qiniu

def updateFile(path):

q = qiniu.Auth(ACCESS_KEY, SECRET_KEY)

key = 'hello'

data = 'hello qiniu!'

token = q.upload_token(bucket_name)

ret, info = qiniu.put_data(token, key, data)

if ret is not None:

print('All is OK')

else:

print(info) # error message in info

处理这类问题一般要是依赖注册/注入的方法来解决了,我推荐优先使用 依赖注册 的方法解决这个问题。具体方法是实现一个全局模块,用来注册像七牛这类的服务:

class ServiceRegistry()

def __init__(self):

self.services = {}

def register(self, id, service):

self.services[id] = service

def get(self, id):

return self.services[id]

serviceRegistry = ServiceRegistry()

上传文件的代码要做一些调整:

import qiniu

+from xxx import serviceRegistry

def updateFile(path):

q serviceRegistry.get('qiniu)

- q = qiniu.Auth(ACCESS_KEY, SECRET_KEY) key = 'hello'

data = 'hello qiniu!'

token = q.upload_token(bucket_name)

ret, info = qiniu.put_data(token, key, data)

if ret is not None:

print('All is OK')

else:

print(info) # error message in info

简单来说,就是使用 qiniu 接口的时候,不再自己创建,而是通过 serviceRegistry 对象来查询 qiniu 的接口实现。

在测试代码中调用 uploadFile 函数之前,先通过 serviceRegistry 注册一个 mock 的 qiniu 接口:

from xxx import serviceRegistry

class MockQiniu:

def __init__(self):

pass

def upload_token(self):

return 'foobar'

def put_token(self, key, data)

return None

class UploadTestCase(AuditTestCase)

def test_upload(self):

serviceRegistry.register('qiniu', MockQiniu())

uploadFile('/var/lib/avatar.png')

依赖注册 vs 依赖注入

之所以推荐优先使用依赖注册,主要的原因是 Django 项目一般都是应用层项目,代码逻辑不会很复杂,使用依赖注册很容易让人理解,对现有代码的破坏程度也不大。

在几乎都是各种搬砖逻辑的项目里面用依赖注册,就显得有点大材小用了,而且还没有看到有比较好的 Django 依赖注册框架。

mock 缓存、数据库

mock cache 和 database 在 Django 项目当中是非常容易的,因为启动 Django 服务是可以设置不同的配置文件。

可以先创建一个测试环境配置文件 test_settings.py:

import logging

from backend.settings import *

DATABASES = {

'default': {

'ENGINE': 'django.db.backends.sqlite3',

'NAME': os.path.join(BASE_DIR, 'db.sqlite3')

}

}

CACHES = {

'default': {

'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',

'LOCATION': 'unique-snowflake',

}

}

然后运行测试的时候,让 Django 读取这个配置文件即可。

$ manage.py test --settings=backend.test_settings

这样,执行 Django 单元测试的时候,就可以使用内存版本缓存以及本地临时创建的 sqlite3 数据库来测试了。

统计代码覆盖率

统计代码覆盖率就用 coverage 模块好了,还没找到其他合适的工具,使用方式如下:

$ coverage run \ --source='.' \ --omit='venv/*,core/migrations/*' \ manage.py test \ --no-logs \ --failfast \ --settings=backend.test_settings

$ coverage html

$ coverage report -m

执行完测试以后,可以打开 htmlcov/index.html 文件来看 html 版本的统计报告。

更多内容可以查看我的博客

你可能感兴趣的:(django,集成个推)