Python Web 模块之 Flask v0.1

第 3 部分 源码阅读之测试用例

3.1 BasicFunctionality

3.1.2 Session

def test_session(self):
    app = flask.Flask(__name__)
    app.secret_key = 'testkey'
    @app.route('/set', methods=['POST'])
    def set():
        flask.session['value'] = flask.request.form['value']
        return 'value set'
    @app.route('/get')
    def get():
        return flask.session['value']

    c = app.test_client()
    assert c.post('/set', data={'value': '42'}).data == 'value set'
    assert c.get('/get').data == '42'

这个测试用例也很简单 , set 函数用于设置当前请求中 session 中 'value' 的值 , 设置为表单中 'value' 字段的值 , 最终返回 'value set' ; get 函数就返回设置的 session 中的 value 字段 。

然后模拟一个客户端发送请求 , 通过判断相应的值来确认功能是否正常 。

3.1.3 Request Processing

def test_request_processing(self):
    app = flask.Flask(__name__)
    evts = []
    @app.before_request
    def before_request():
        evts.append('before')
    @app.after_request
    def after_request(response):
        response.data += '|after'
        evts.append('after')
        return response
    @app.route('/')
    def index():
        assert 'before' in evts
        assert 'after' not in evts
        return 'request'
    assert 'after' not in evts
    rv = app.test_client().get('/').data
    assert 'after' in evts
    assert rv == 'request|after'

这个测试用例主要是测试响应前与响应后的功能 , before_request 视图函数注册了 before_request 路由 , 在执行视图函数之前会先行执行它 ; 同理 after_request 视图函数注册了 after_request 路由 , 在视图函数执行完毕后在执行它 。

因此 before_request 函数中先向 evts 列表中添加了 'before' , 然后在执行 index 视图函数 , 这里视图函数首先判断 'before' 是否在 evts 中 , 如果在就继续 , 否则就失败 ; 然后判断 'after' 是否在 evts 中 , 如果不在就继续 , 否则失败 ; 最终返回 'request' ; 然后执行 after_request 函数 , 它会在之前的相应数据中添加 '|after' , 同时将 'after' 添加到 evts 中 。 整个步骤就是这样的 , 在进行判断操作 。

3.1.4 Error Handling

def test_error_handling(self):
    app = flask.Flask(__name__)
    @app.errorhandler(404)
    def not_found(e):
        return 'not found', 404
    @app.errorhandler(500)
    def internal_server_error(e):
        return 'internal server error', 500
    @app.route('/')
    def index():
        flask.abort(404)
    @app.route('/error')
    def error():
        1/0
    c = app.test_client()
    rv = c.get('/')
    assert rv.status_code == 404
    assert rv.data == 'not found'
    rv = c.get('/error')
    assert rv.status_code == 500
    assert 'internal server error' in rv.data

这个测试用例是为了测试错误处理功能是否正常 。

not_found 函数通过 errorhandler 注册了 404 代码的处理方法 , 返回 'not found', 404 ; internal_server_error 注册了一个 500 代码的处理方法 , 返回 'internal server error', 500 ; 访问 index 的时候 , 直接以 404 异常中止 ; error 是以 Python 错误语句来导致 Python 内部错误 , 可以被 internal_server_error 捕获 。

因此这里也很好理解 , 当请求 '/' 时会被 404 异常中止服务 , 那么状态码应该为 404 , 执行结果为 'not found' 。 同理后面的步骤也是这样 。

3.1.5 Response Creation

def test_response_creation(self):
    app = flask.Flask(__name__)
    @app.route('/unicode')
    def from_unicode():
        return u'Hällo Wörld'
    @app.route('/string')
    def from_string():
        return u'Hällo Wörld'.encode('utf-8')
    @app.route('/args')
    def from_tuple():
        return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain'
    c = app.test_client()
    assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8')
    assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8')
    rv = c.get('/args')
    assert rv.data == 'Meh'
    assert rv.headers['X-Foo'] == 'Testing'
    assert rv.status_code == 400
    assert rv.mimetype == 'text/plain'

这个 case 是测试请求响应的 , 前面的判断都很好理解 , 我有些疑惑的是 from_tuple 视图函数响应的时候会是 data , headers , status_code 和 mimetype 在返回值中 , 应该是响应的时候经过了某些步骤的处理吧 。

3.1.6 URL Generation

def test_url_generation(self):
    app = flask.Flask(__name__)
    @app.route('/hello/<name>', methods=['POST'])
    def hello(): # 这里添加参数 name => def hello(name) 较好
        pass  # 这里改成 return "name" 较好
    with app.test_request_context():
        assert flask.url_for('hello', name='test x') == '/hello/test%20x'

这个 case 也比较简单 , 注册一个路由之后 , 在请求上下文中判断响应的链接是否正确 , 这里的 test_request_context 其实就是创建请求上下文 , 其代码如下 :

def test_request_context(self, *args, **kwargs):
    return self.request_context(create_environ(*args, **kwargs))

这里的 request_context 之前已经解析过 , 就不再解析 ; url_for 函数是用来生成 URL 链接的 , 根据给定的参数生成链接 , 其代码如下 :

def url_for(endpoint, **values):
    """Generates a URL to the given endpoint with the method provided.

    :param endpoint: the endpoint of the URL (name of the function)
    :param values: the variable arguments of the URL rule
    """
    return _request_ctx_stack.top.url_adapter.build(endpoint, values)

由于 build 不是 Flask 的代码 , 这里就不在解析 。

最终这个 case 通过判断生成链接是否符合预期来判断功能是否正常 。

3.1.7 Static Files

def test_static_files(self):
    app = flask.Flask(__name__)
    rv = app.test_client().get('/static/index.html')
    assert rv.status_code == 200
    assert rv.data.strip() == '<h1>Hello World!</h1>'
    with app.test_request_context():
        assert flask.url_for('static', filename='index.html') \
            == '/static/index.html'

这里的 index.html 文件内容就是 <h1>Hello World!</h1> , 在这里并没有设置 static 文件目录 , 这是因为 Flask 0.1 中已经设置了 static 目录为与 Flask 实例同级 , 因此没有设置 , 同时是直接请求静态文件 , 所以不需要视图函数 。

因此请求一个已知路径的静态文件是可以正常请求到的 , 因此这里的 status_code 为正常的 200 , 返回值也用 strip 函数预处理了一下 , 最后又测试了一下 url_for 生成链接的功能 , 这里就不在解析 。

3.2 Context

上文中已经解析完毕基础功能相关的测试用例 , 这一节解析上下文相关的用例 。

3.2.1 Static Files

def test_context_binding(self):
    app = flask.Flask(__name__)
    @app.route('/')
    def index():
        return 'Hello %s!' % flask.request.args['name']
    @app.route('/meh')
    def meh():
        return flask.request.url

    with app.test_request_context('/?name=World'):
        assert index() == 'Hello World!'
    with app.test_request_context('/meh'):
        assert meh() == 'http://localhost/meh'

这个测试用例两个视图函数分别是 : index 最终返回请求参数与 "Hello " 相连的字符串 ; meh 最终返回当前请求的链接 。

首先模拟一个请求上下文 , 请求链接是 '/' , 参数是 name=World , 这里需要注意一下 , 在实际的链接中 , "?" 之后的就是链接的请求参数 , 所以其返回值为 Hello World , 类似于基本功能的测试用例里面创建一个 Client , 然后请求 GET '/?name=World' , 这里直接在请求上下文里面操作 , 省去了请求的步骤 ; 下面的步骤同理 。 不过需要注意一下 , 如果自定义了 server host , 链接中就不是 localhost 了 。

3.3 Templating

上文中已经解析完毕基础功能及上下文相关的测试用例 , 这一节解析模板相关的用例 。

3.3.1 Context Processing

def test_context_processing(self):
    app = flask.Flask(__name__)
    @app.context_processor
    def context_processor():
        return {'injected_value': 42}
    @app.route('/')
    def index():
        return flask.render_template('context_template.html', value=23)
    rv = app.test_client().get('/')
    assert rv.data == '<p>23|42'

测试用例开始之前 , 使用 context_processor 注册了一个上下文处理器 , 这个上下文处理器返回了一个字典 {'injected_value': 42} ; 同时主页使用 render_template 函数动态渲染了一个静态模板 , 最终通过 get 请求主页后的值进行比对 , 来判断测试功能是否正常 。

首先先看一下 context_processor 方法 :

[flask.py]

def context_processor(self, f):
    """Registers a template context processor function."""
    self.template_context_processors.append(f)
    return f

就是把参数对象添加到模板处理器列表 template_context_processors 中 , Flask 初始化的时候已经初始化为 self.template_context_processors = [_default_template_ctx_processor] 这个 _default_template_ctx_processor 实际上就是一个 dict 对象 :

def _default_template_ctx_processor():
    """Default template context processor.  Injects `request`,
    `session` and `g`.
    """
    reqctx = _request_ctx_stack.top
    return dict(
        request=reqctx.request,   # 当前请求
        session=reqctx.session,   # 当前请求的 session
        g=reqctx.g
    )

接下来解析 render_template 函数 :

def render_template(template_name, **context):
    """Renders a template from the template folder with the given
    context.

    :param template_name: the name of the template to be rendered
    :param context: the variables that should be available in the
                    context of the template.
    """
    current_app.update_template_context(context)
    return current_app.jinja_env.get_template(template_name).render(context)

传入两个参数 , 第一个是模板文件名称 , 第二个就是参数字典 。 最终返回 jinja 渲染的文本 。

当然在渲染之前 , 会先执行模板上下文处理器 template_context_processors :

[flask.py]

def update_template_context(self, context):
    """Update the template context with some commonly used variables.
    This injects request, session and g into the template context.

    :param context: the context as a dictionary that is updated in place
                    to add extra variables.
    """
    reqctx = _request_ctx_stack.top
    for func in self.template_context_processors:
        context.update(func())

将处理器全部执行一遍之后才会执行渲染步骤 , 这个过程就是为了更新上下文的变量 。

那这个测试用例就很明了了 , 先执行 index 函数 , 但是 index 函数中有渲染模板的功能 , 在模板渲染函数 render_template 中 , 会首先执行模板上下文处理器 , 因此会先行执行 context_processor 函数 , 再渲染模板 , 这个模板语句很简单 :

<p>{{ value }}|{{ injected_value }}

经过渲染后 , 分别将 value 和 injected_value 替换到模板文件中 , 最终结果为 : <p>23|42 , 因此正常情况下应该是通过的 。

3.3.2 Escaping

def test_escaping(self):
    text = '<p>Hello World!'
    app = flask.Flask(__name__)
    @app.route('/')
    def index():
        return flask.render_template('escaping_template.html', text=text,
                                     html=flask.Markup(text))
    lines = app.test_client().get('/').data.splitlines()
    assert lines == [
        '&lt;p&gt;Hello World!',
        '<p>Hello World!',
        '<p>Hello World!',
        '<p>Hello World!',
        '&lt;p&gt;Hello World!',
        '<p>Hello World!'
    ]

[escaping_template.html]

{{ text }}
{{ html }}
{% autoescape false %}{{ text }}
{{ html }}{% endautoescape %}
{% autoescape true %}{{ text }}
{{ html }}{% endautoescape %}

这个测试用例和上一个相差不多 , 不过这个测试用例测试的是转义功能 , 在模板语句中 , 自动转义不容易出现某些安全相关的问题 , 但是会出现一些奇怪的符号 , 例如上文中 &lt;p&gt; , 这些都是 html 中的符号 , 如果将自动转义功能关闭 , 则不会将某些符号自动转换 , 例如第二个 text 的值为 <p>Hello World! 而第一个经过转义后为 &lt;p&gt;Hello World! , 这个 case 也结束了 。

第 4 部分 总结

Flask 0.1 版本的源代码解析到此结束 , 但是我发现还有几个函数没有解析 , 有两个是没有用到 , 还有几个是用到了但是忘记解析了 , 不过这几个函数都挺简单的 , 等后面再写吧 。

通过阅读 Flask 初始版本 , 学到了一些知识 , 也学到了大佬的一些语句的写法 , 反正不管怎么说 , 花费了时间去阅读源代码 , 多少会有收获的 。 继续加油 。

Deadline: 2021-04-12 23:27