单元测试支持
目前,Pait
通过TestHelper
提供一个了简单的单元测试支持,TestHelper
在执行时会通过路由函数自动补充URL,HTTP方法等参数运行测试,
并在获得结果时会从response_modle_list
中获取与测试结果最匹配的响应模型进行简单的校验,从而减少开发者编写测试用例的代码量。
1.TestHelper使用方法
本次使用的示例代码是以首页的示例代码进行拓展,主要变动是在路由函数添加了一个名为return_error_resp
的参数,当return_error_resp
为True
时会返回不符合响应模型的响应,代码如下:
Flask Starlette Sanic Tornado
docs_source_code/docs_source_code/unit_test_helper/flask_test_helper_demo.py # flake8: noqa: E402
from typing import Type
from flask import Flask , Response , jsonify
from pydantic import BaseModel , Field
from pait.app.flask import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
class DemoResponseModel ( JsonResponseModel ):
class ResponseModel ( BaseModel ):
uid : int = Field ()
user_name : str = Field ()
description : str = "demo response"
response_data : Type [ BaseModel ] = ResponseModel
@pait ( response_model_list = [ DemoResponseModel ])
def demo_post (
uid : int = Json . t ( description = "user id" , gt = 10 , lt = 1000 ),
username : str = Json . t ( description = "user name" , min_length = 2 , max_length = 4 ),
return_error_resp : bool = Json . i ( description = "return error resp" , default = False ),
) -> Response :
if return_error_resp :
return jsonify ()
return jsonify ({ "uid" : uid , "user_name" : username , "a" : 123 })
app . run ( port = 8000 )
docs_source_code/docs_source_code/unit_test_helper/starlette_test_helper_demo.py # flake8: noqa: E402
from typing import Type
from pydantic import BaseModel , Field
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from pait.app.starlette import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
class DemoResponseModel ( JsonResponseModel ):
class ResponseModel ( BaseModel ):
uid : int = Field ()
user_name : str = Field ()
description : str = "demo response"
response_data : Type [ BaseModel ] = ResponseModel
@pait ( response_model_list = [ DemoResponseModel ])
async def demo_post (
uid : int = Json . t ( description = "user id" , gt = 10 , lt = 1000 ),
username : str = Json . t ( description = "user name" , min_length = 2 , max_length = 4 ),
return_error_resp : bool = Json . i ( description = "return error resp" , default = False ),
) -> JSONResponse :
if return_error_resp :
return JSONResponse ({})
return JSONResponse ({ "uid" : uid , "user_name" : username })
import uvicorn
uvicorn . run ( app )
docs_source_code/docs_source_code/unit_test_helper/sanic_test_helper_demo.py # flake8: noqa: E402
from typing import Type
from pydantic import BaseModel , Field
from sanic.app import Sanic
from sanic.response import HTTPResponse , json
from pait.app.sanic import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
class DemoResponseModel ( JsonResponseModel ):
class ResponseModel ( BaseModel ):
uid : int = Field ()
user_name : str = Field ()
description : str = "demo response"
response_data : Type [ BaseModel ] = ResponseModel
@pait ( response_model_list = [ DemoResponseModel ])
async def demo_post (
uid : int = Json . t ( description = "user id" , gt = 10 , lt = 1000 ),
username : str = Json . t ( description = "user name" , min_length = 2 , max_length = 4 ),
return_error_resp : bool = Json . i ( description = "return error resp" , default = False ),
) -> HTTPResponse :
if return_error_resp :
return json ({})
return json ({ "uid" : uid , "user_name" : username })
import uvicorn
uvicorn . run ( app )
docs_source_code/docs_source_code/unit_test_helper/tornado_test_helper_demo.py # flake8: noqa: E402
from typing import Type
from pydantic import BaseModel , Field
from tornado.web import Application , RequestHandler
from pait.app.tornado import pait
from pait.field import Json
from pait.model.response import JsonResponseModel
class DemoResponseModel ( JsonResponseModel ):
class ResponseModel ( BaseModel ):
uid : int = Field ()
user_name : str = Field ()
description : str = "demo response"
response_data : Type [ BaseModel ] = ResponseModel
class DemoHandler ( RequestHandler ):
@pait ( response_model_list = [ DemoResponseModel ])
def post (
self ,
uid : int = Json . t ( description = "user id" , gt = 10 , lt = 1000 ),
username : str = Json . t ( description = "user name" , min_length = 2 , max_length = 4 ),
return_error_resp : bool = Json . i ( description = "return error resp" , default = False ),
) -> None :
if return_error_resp :
self . write ({})
else :
self . write ({ "uid" : uid , "user_name" : username })
app . listen ( 8000 )
from tornado.ioloop import IOLoop
IOLoop . instance () . start ()
接着就可以通过TestHelper
来编写测试用例了,首先需要导入TestHelper
以及对应Web框架的测试客户端,同时还要进行测试框架的初始化:
在编写完测试用例的初始化代码后,就可以编写测试用例代码了,首先将演示如何通过TestHelper
来编写一个测试用例,代码如下:
Flask Starlette Sanic Tornado
docs_source_code/unit_test_helper/flask_test_helper_demo.py def test_demo_post_route_by_call_json ( client : FlaskClient ) -> None :
test_helper = FlaskTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/starlette_test_helper_demo.py def test_demo_post_route_by_call_json ( client : TestClient ) -> None :
test_helper = StarletteTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/sanic_test_helper_demo.py def test_demo_post_route_by_call_json ( client : SanicTestClient ) -> None :
test_helper = SanicTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/tornado_test_helper_demo.py class TestTornado ( AsyncHTTPTestCase ):
...
def test_demo_post_route_by_call_json ( self ) -> None :
test_helper = TornadoTestHelper (
client = self ,
func = DemoHandler . post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
在这个测试用例中, 会对TestHelper
进行初始化,TestHelper
的初始化需要Web框架对应的测试客户端、路由函数,以及路由函数的一些请求参数, 在初始化完成后就可以通过TestHelper
获得请求响应了。
在执行测试的时候,TestHelper
会通过路由函数自动发现了路由函数的URL
和HTTP方法,所以调用json
方法的时候TestHelper
会自动发起了post
请求,并通过post
请求获得响应结果,
然后把响应Body序列化为Python
的dict
对象并返回, 但是当该路由函数绑定了多个请求方法时,TestHelper
则无法自动执行,需要在调用json
方法时指定对应的HTTP方法,
使用方法如下:
Flask Starlette Sanic Tornado
docs_source_code/unit_test_helper/flask_test_helper_demo.py def test_demo_post_route_by_use_method ( client : FlaskClient ) -> None :
test_helper = FlaskTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json ( method = "POST" ) == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/starlette_test_helper_demo.py def test_demo_post_route_by_use_method ( client : TestClient ) -> None :
test_helper = StarletteTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json ( method = "POST" ) == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/sanic_test_helper_demo.py def test_demo_post_route_by_use_method ( client : SanicTestClient ) -> None :
test_helper = SanicTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json ( method = "POST" ) == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/tornado_test_helper_demo.py class TestTornado ( AsyncHTTPTestCase ):
...
def test_demo_post_route_by_use_method ( self ) -> None :
test_helper = TornadoTestHelper (
client = self ,
func = DemoHandler . post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
assert test_helper . json ( method = "POST" ) == { "uid" : 11 , "user_name" : "test" }
此外,在编写测试用例时,可能需要的是一个响应对象,而不是响应数据,以便对状态码,Header
之类的数据进行校验。
这时可以通过TestHelper
的HTTP系列方法进行调用并获得Web框架对应测试客户端的响应对象Response
。
如下代码就会通过post
方法对路由函数发起请求,并返回Web框架测试客户端的响应对象,最后再通过响应对象进行断言:
Flask Starlette Sanic Tornado
docs_source_code/unit_test_helper/flask_test_helper_demo.py def test_demo_post_route_by_raw_web_framework_response ( client : FlaskClient ) -> None :
test_helper = FlaskTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
resp = test_helper . post ()
assert resp . status_code == 200
assert resp . json == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/starlette_test_helper_demo.py def test_demo_post_route_by_raw_web_framework_response ( client : TestClient ) -> None :
test_helper = StarletteTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
resp = test_helper . post ()
assert resp . status_code == 200
assert resp . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/sanic_test_helper_demo.py def test_demo_post_route_by_raw_web_framework_response ( client : SanicTestClient ) -> None :
test_helper = SanicTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
resp = test_helper . post ()
assert resp . status_code == 200
assert resp . json == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/tornado_test_helper_demo.py class TestTornado ( AsyncHTTPTestCase ):
...
def test_demo_post_route_by_raw_web_framework_response ( self ) -> None :
test_helper = TornadoTestHelper (
client = self ,
func = DemoHandler . post ,
body_dict = { "uid" : 11 , "username" : "test" },
)
resp = test_helper . post ()
assert resp . code == 200
assert resp . body . decode () == '{"uid": 11, "user_name": "test"}'
虽然这种情况下TestHelper
与使用Web框架对应的测试客户端的使用方式没有太大的差别,但是TestHelper
在获取到路由函数的响应后,
会根据路由响应从路由函数的response_model_list
挑选一个最匹配的响应模型进行校验,如果检查到响应对象的HTTP状态码,Header与响应数据三者中有一个不符合响应模型的条件就会抛出错误并中断测试用例,如下例子:
Flask Starlette Sanic Tornado
docs_source_code/unit_test_helper/flask_test_helper_demo.py def test_demo_post_route_by_test_helper_check_response_error ( client : FlaskClient ) -> None :
test_helper = FlaskTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" , "return_error_resp" : True },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/starlette_test_helper_demo.py def test_demo_post_route_by_test_helper_check_response_error ( client : TestClient ) -> None :
test_helper = StarletteTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" , "return_error_resp" : True },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/sanic_test_helper_demo.py def test_demo_post_route_by_test_helper_check_response_error ( client : SanicTestClient ) -> None :
test_helper = SanicTestHelper (
client = client ,
func = demo_post ,
body_dict = { "uid" : 11 , "username" : "test" , "return_error_resp" : True },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
docs_source_code/unit_test_helper/tornado_test_helper_demo.py class TestTornado ( AsyncHTTPTestCase ):
...
def test_demo_post_route_by_test_helper_check_response_error ( self ) -> None :
test_helper = TornadoTestHelper (
client = self ,
func = DemoHandler . post ,
body_dict = { "uid" : 11 , "username" : "test" , "return_error_resp" : True },
)
assert test_helper . json () == { "uid" : 11 , "user_name" : "test" }
在执行完测试用例后,TestHelper
会发现路由函数的响应结果与路由函数定义的响应模型不匹配,此时会抛出异常,中断测试用例,并输出结果如下:
> raise exc
E pait.app.base.test_helper.CheckResponseException: maybe error result:
E >>>>>>>>>>
E check json content error, exec: 2 validation errors for ResponseModel
E uid
E field required ( type = value_error.missing)
E user_name
E field required ( type = value_error.missing)
E
E >>>>>>>>>>
E by response model:<class 'docs_source_code.unit_test_helper.flask_test_helper_demo.DemoResponseModel' >
通过输出结果可以发现,抛出的异常为CheckResponseException
。同时,根据异常信息可以了解到,本次校验的响应模型是DemoResponseModel
,
它在校验的过程中发现响应数据缺少了uid
字段和user_name
字段。
2.参数介绍
TestHelper
的参数分为初始化必填参数,请求相关的参数,响应相关的结果参数共3种。其中,初始化参数的说明如下:
参数
描述
client
Web框架的测试客户端
func
要进行测试的路由函数
而请求参数有多个,对于大部分Web框架来说只是封装了一层调用,但对于使用Tornado
之类的没对测试客户端做过多封装的框架的则能提供了一些便利,这些参数有:
body_dict: 发起请求时的Json数据。
cookie_dict: 发起请求时的cookie数据。
file_dict: 发起请求时的file数据。
form_dict: 发起请求时的form数据。
header_dict: 发起请求时的header数据。
path_dict: 发起请求时的path数据。
query_dict: 发起请求时的query数据。
除此之外,TestHelper
还有几个与响应结果校验相关的参数,比如strict_inspection_check_json_content
参数。
默认情况下,strict_inspection_check_json_content
参数的值为True,这会让TestHelper
对响应结果的数据结构进行严格校验,比如下面的例子:
a = {
"a" : 1 ,
"b" : {
"c" : 3
}
}
b = {
"a" : 2 ,
"b" : {
"c" : 3 ,
"d" : 4
}
}
在这个例子中,a
与b
的数据结构是不一样的,其中a变量代指响应模型的数据结构,b变量则是响应体的数据结构,当TestHelper
进行校验时, 会因为检测到b变量多出来一个结构['b']['d']
而直接抛出错误,
不过也可以直接设置参数strict_inspection_check_json_content
的值为False
,这样TestHelper
只会校验出现在响应模型中出现的字段,而不会检查响应模型之外的字段。
除了上述的参数外,TestHelper
还有另外几个参数,如下:
参数
描述
target_pait_response_class
如果值不为空,那么TestHelper
会通过target_pait_response_class
从response_model_list
中筛选出一批符合条件的response_model
来进行校验。该值通常是响应模型的父类,默认值为None
代表不匹配。
enable_assert_response
表示TestHelper
是否会对响应结果进行断言,默认值为True。