测试
测试
介绍
Masonite 测试非常简单。您只需使用Masonite单元测试类扩展您的类,即可轻松测试代码中非常复杂的部分。
Masonite的测试集合是基于unittest框架的,尽管它使用pytest来运行测试用例。所以您可以使用unittest语法进行编写用例,使用pytest运行测试用例。所有语法遵循camelCase而不是PEP 8 under_score。为保持与unittest标准一致, 运行中调用的所有TestCase方法遵循camelCase形式。
正常测试应该使用下划线连接并以test_开头, 如下所示:
def test_user_can_login(self):
pass
配置
首先,在测试目录下创建一个新的测试。您可以通过以下命名创建测试:
$ craft test User
这将为我们创建一个可以运行的用户测试,您可以将测试拖到您喜欢的任何目录中。
以下为上述命令创建的基本用例:
tests/test\user.py
"""Example Testcase."""
from masonite.testing import TestCase
class TestUser(TestCase):
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
pass
用例准备就绪,您可以开始进行测试了。继续阅读以了解如何开始构建测试用例。
环境
大多数时候,您想在不同的数据库上进行开发和测试。也许您是在本地MySQL数据库上开发的,但是您的测试要在SQLlite数据库中运行。
您可以创建一个.env.testing
文件,并将所有数据库配置放入其中。当Pytest运行时,它将另外加载并覆盖任何其他环境变量。
你 .env.testing
配置文件看起来类似如下:
DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_DATABASE=masonite.db
DB_LOG=False
STRIPE_CLIENT=test_sk-9uxaxixjsxjsin
STRIPE_SECRET=test_sk-suhxs87cen88h7
你可以加载任何测试环境变量。默认它们并不会被版本控制提交。
断言方法
以下是可以用来断言的方法。
所有的方法均以assert
开发,可以进行链式调用来多次断言。所有其他方法都将返回布尔值或者值,以便你可以用来自行验证。
assertContains(value) | assertHasAmount(amount) | assertHeaderIs(key, value) | |
---|---|---|---|
assertNotFound() | assertNotHasAmount(amount) | assertPathIs(value) | |
assertHasJson(key, value) | assertParameterIs(parameter, value) | isNamed(name) | |
assertJsonContains(key, value) | assertIsStatus(code) | hasMiddleware(*middleware) | |
assertCount(number) | assertHasHeader(name) | assertNotHasHeader(name) | hasController(name) |
contains(value) | ok() | canView() | |
hasJson(key, value) | count(number) | amount(number) | |
isGet() | isPost() | isPut() | |
isPatch() | isDelete() | hasSession(key, value) | |
parameterIs() | headerIs() |
执行路由
存有几个方式用来测试路由。
测试路由是否存在
要检查路由是否存在,我们可以见到使用get或者post方法:
tests/test_unit.py
def test_route_exists(self):
self.assertTrue(self.get('/testing'))
self.assertTrue(self.post('/testing'))
获取Request或者Response对象
通过访问请求路由的request
或者response
属性来访问请求和响应对象。response
属性呈现为字符串内容:
def test_request_and_response(self):
request = self.get('/testing').request # <masonite.request.Request>
response = self.get('/testing').response # HTML code
请求方法
你可以选择任意请求方法:
def test_route_exists(self):
self.get('/testing')
self.post('/testing')
self.put('/testing')
self.patch('/testing')
self.delete('/testing')
JSON 请求
你可以使用标准的JSON请求,通过json()
方法实现:
def test_route_exists(self):
self.json('POST', '/testing', {'user': 'Joe'})
测试路由名字
tests/test_unit.py
def test_route_has_the_correct_name(self):
self.assertTrue(self.get('/testing').isNamed('testing.route'))
测试路由是否包含中间件
tests/test_unit.py
def test_route_has_route_middleware(self):
assert self.get('/testing').hasMiddleware('auth', 'owner')
测试路由内容是否包含字符串
它可以用来检查模板是否返回了指定的内容。
tests/test_unit.py
def test_view_contains(self):
assert self.get('/login').contains('Login Here')
也可以使用:
self.get('/login').assertContains('Login Here')
检查是否返回200状态码
你可以通过ok
方法来检查相应是否正常:
tests/test_unit.py
def test_view_is_ok(self):
assert self.get('/testing').ok()
CSRF 保护
默认,所有的路由调用都会进行CSRF保护。测试代码会绕过该保护。
这很有必要,这样你就不用操心每次请求设置CSRF tokens了,如果你需要打开该特性的话,通过在测试中用withCsrf()
方法来实现。
def test_csrf(self):
self.withCsrf()
self.post('/unit/test/json', {'test': 'testing'})
这将会在指定测试方法中打开,如果你想在所有测试方法打开。你可以通过在setUp()
方法调用该方法实现:
def setUp(self):
super().setUp()
self.withCsrf()
def test_csrf(self):
self.post('/unit/test/json', {'test': 'testing'})
异常处理
正如您所注意到的,Masonite具有异常处理功能,可用于在开发过程中显示有用的信息。
在测试过程中你可能想看到更加详细的异常内容。所以测试时,默认会关闭Masonite异常处理机制。如果你想打开Masonite的内置处理机制,通过如下实现:
def setUp(self):
super().setUp()
self.withExceptionHandling()
def test_csrf(self):
self.post('/unit/test/json', {'test': 'testing'})
获取输出
你可以在单元测试中调用captureOutput
方法来捕获输出内容:
def test_get_output(self):
with self.captureOutput() as o:
print('hello world!')
self.assertEqual(o, 'hello world!')
测试 JSON
经常我们需要测试API,提供了相关方法来实现。
测试数量
你确保返回的内容为指定数量。例如5篇文章:
def test_has_articles(self):
self.assertTrue(
self.json('GET', '/api/articles').count(5)
)
也可以使用 assertCount(5)
:
def test_has_articles(self):
self.json('GET', '/api/articles').assertCount(5)
你也可以使用amount
方法,它仅仅是count
的别名:
def test_has_articles(self):
self.assertTrue(
self.json('GET', '/api/articles').amount(5)
)
self.json('GET', '/api/articles').assertHasAmount(5)
self.json('GET', '/api/articles').assertNotHasAmount(10)
指定键元素数量
你可以检查JSON指定键其数量。例如:
"""
{
"name": "Joe",
"tags": ['python', 'framework'],
"age": 25
}
"""
def test_has_several_tags(self):
self.assertTrue(
self.json('GET', '/api/user').hasAmount('tags', 2)
)
测试指定内容
有时候你想测试返回JSON包含确切的值:
"""
{
"name": "Joe",
"title": "creator",
"age": 25
}
"""
def test_has_age(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson('age', 25)
)
还可以通过提供字典来检查多个值:
"""
{
"name": "Joe",
"title": "creator",
"age": 25
}
"""
def test_has_age(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson({
"name": "Joe",
"age": 25
})
)
你无需检查所有的元素。仅传递你想验证的元素。
还可以使用:
self.assertJsonHas('key', 'value')
检查集合内部值
你还可以检查响应内容里面列表的值:
"""
[
{
"name": "Joe",
"title": "creator",
"age": 25
},
{
"name": "Bob",
"title": "Co-Founder",
"age": 26
}
]
"""
def test_bob_in_result(self):
self.json('GET', '/api/user').assertJsonContains('name', 'Bob')
点标记
你还可以使用点标记来访问嵌套结构:
"""
{
"profile": {
"name": "Joe",
"title": "creator",
"age": 25
}
}
"""
def test_has_name(self):
self.assertTrue(
self.json('GET', '/api/user').hasJson('profile.name', 'Joe')
)
转换为字典
有时候你不想使用点标记方式,你想通过转换为字典来检查其元素值。也方便你在终端打印出来诊断。你可以使用如下方式:
"""
{
"profile": {
"name": "Joe",
"title": "creator",
"age": 25
}
}
"""
def test_has_name(self):
dictionary = self.json('GET', '/api/user').asDictonary()
self.assertEqual(dictionary['profile']['name'], 'Joe')
测试参数
你还可以测试参数中的值。例如,你想检查参数id
的值是否为5
:
def test_has_name(self):
# Route is: /dashboard/user/@id
self.assertTrue(
self.get('GET', '/dashboard/user/5').parameterIs('id', '5')
)
self.get('GET', '/dashboard/user/5').assertParameterIs('id', '5')
测试头部内容
你可以测试协议头部内容是否包含指定值。例如,你想检查Content-Type
是否值为text/html
:
def test_has_name(self):
# Route is: /dashboard/user/@id
self.assertTrue(
self.get('GET', '/dashboard/user/5').headerIs('Content-Type', 'text/html')
)
self.get('GET', '/dashboard/user/5').assertHeaderIs('Content-Type', 'text/html')
测试状态码
你可以通过使用isStatus
和assertIsStatus
方法来断言返回状态码:
self.assertTrue(
self.get('GET', '/dashboard/user/5').isStatus(200)
)
self.get('GET', '/dashboard/user/5').assertIsStatus(200)
你可以提供便利方法来断言404
:
self.get('GET', '/dashboard/not/exists').assertNotFound()
这里断言其输出状态码为404
效果一致。
子域名
默认,Masonite会关闭子域名,放置部署在一些PaaS平台会有问题,例如sunny-land-176892.herokuapp.com
。
在测试中要打开子域名功能,需要调用withSubdomains()
方法。然后,就可以设置其wsgi
属性来配置host。
def test_subdomains(self):
self.withSubdomains().get('/view', wsgi={
'HTTP_HOST': 'subb.domain.com'
}).assertIsStatus(404)
测试数据库
数据库
默认,为防止弄乱运行中的数据库。测试用例只使用sqlite
数据库。你可以通过将sqlite
属性设置为False
关闭。
from masonite.testing import TestCase
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
sqlite = False
def setUp(self):
super().setUp()
这样你就可以使用任意数据库驱动了。
事务和刷新
默认,所有的测试通过在一个事务中执行,所以所有的数据只会在测试生存周期中创建。一旦测试完成,数据库会回滚到上一个状态。这保持数据库整洁的有效方法。
虽然这样对于很多测试情况下较好方式,有时你可能真想将操作更新到数据库中。可以通过设置refreshes_database
属性为True
来实现。
from masonite.testing import TestCase
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = False
refreshes_database = True
def setUp(self):
super().setUp()
现在就会迁移和刷新数据库了。
小心有可能会破坏你现有数据库
工厂
工厂是用来生成一些虚拟数据到数据库中。你可以创建一个方法用来接收faker参数来构建数据。
Masonite提供一个便利方法seUpFactories
,它会测试启动时执行。它只会运行一次,而不是每个测试
让我们创建工厂方法,并使用setUpFactories方法来运行它。
以下创建了100个用户:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
self.make(User, self.user_factory, 100)
def user_factory(self, faker):
return {
'name': faker.name(),
'email': faker.email(),
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu',
# == 'secret'
}
def test_creates_users(self):
pass
你不一定需要依赖工厂来构建数据,可以通过如下方式来新增记录:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': 'user@example.com',
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret'
})
def test_creates_users(self):
pass
用户
我们可以加载用户到路由中,然后检查它是否可以查看路由。可用来检查中间件是否有效处理不同用户。使用acting_as()
方法来实现:
tests/test_unit.py
from app.User import User
...
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': 'user@example.com',
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret'
})
def test_user_can_see_dashboard(self):
self.assertTrue(
self.actingAs(User.find(1)).get('/dashboard').ok()
)
传递数据
也许你需要提供类似提交表单并指定传入数据。你可以通过 传递字典给get
或者post
方法第二参数来实现:
def test_user_can_see_dashboard(self):
self.assertTrue(
self.actingAs(User.find(1)).post('/dashboard', {
'name': 'Joe',
'active': 1
})
)
get方法同样可以接收参数,它将以查询参数的方式实现。
断言数据库值
不用非得通过模型来检查是否在数据库存在值,我们可以使用assertDatabaseHas
断言来执行对数据库进行检查:
def test_create_article(self):
self.assertTrue(
self.post('/articles', {
'name': 'Masonite is great',
'author_id': 1
}).assertDatabaseHas('articles.name', 'Masonite is great')
)
也可以使用相对的 assertDatabaseNotHas
断言:
def test_create_article(self):
self.assertTrue(
self.post('/articles', {
'name': 'Masonite is great',
'author_id': 1
}).assertDatabaseNotHas('articles.name', 'Masonite is bad')
)
测试范例
我们来举个完整的例子,检查用户是否成功创建:
from masonite.testing import TestCase
from app.User import User
class TestUser(TestCase):
"""Start and rollback transactions for this test
"""
transactions = True
def setUp(self):
super().setUp()
def setUpFactories(self):
User.create({
'name': 'Joe',
'email': 'user@example.com',
# == 'secret'
'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu',
})
def test_creates_users(self):
self.assertTrue(User.find(1))
就这样!该测试会检查用户是否成功创建.
运行测试
通过如下方式执行测试:
$ python -m pytest
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。