下午好 我从事测试自动化。 像所有自动化工程师一样,我有一组通常选择编写测试的库和工具。 但是,在某些情况下,经常会出现一些熟悉的库无法解决此问题,并存在使自动测试不稳定或脆弱的风险。 在本文中,我想告诉您使用嘲笑ov的看似标准任务是如何促使我编写模块的。 我也想分享我的决定并听听反馈。
应用程式
审计是金融部门的必要部门之一。 数据需要定期检查(对帐)。 在这方面,我测试过的应用程序出现了。 为了不谈论抽象的东西,让我们想象一下我们的团队正在开发一个用于处理即时通讯程序中的应用程序的应用程序。 对于每个应用程序,必须在elasticsearch中创建一个适当的事件。 验证应用程序将是我们对未跳过应用程序的监视。
因此,假设我们有一个包含以下组件的系统:
- 配置服务器。 对于用户而言,这是一个单一的入口点,他不仅可以配置用于验证的应用程序,还可以配置系统的其他组件。
- 验证申请。
- 来自应用程序的数据处理存储在elasticsearch中的应用程序。
- 参考数据。 数据格式取决于与应用程序集成的Messenger。
挑战赛
在这种情况下,测试自动化看起来非常简单:
- 环境准备:
- 使用最小的配置(使用msi和命令行)安装Elasticsearch。
- 已安装验证应用程序。
- 测试执行:
- 验证应用程序已配置。
- Elasticsearch充满了对应测试的测试数据(处理了多少个应用程序)。
- 该应用程序从Messenger接收“参考”数据(假定实际上有多少个应用程序)。
- 检查由应用程序发布的判决:成功验证的应用程序数量,丢失的应用程序数量等。
- 清洁环境。
问题是我们正在测试监视,但是要配置它,我们需要来自配置服务器的数据。 首先,为每次运行安装和配置服务器是一项耗时的操作(例如,它有自己的基础)。 其次,我想隔离应用程序,以简化发现缺陷时问题的定位。 最后,决定使用模拟。
这可能会引发一个问题:“如果我们仍然模拟服务器,也许我们不能花时间安装和填充Elasticsearch,而要替换模拟吗?”。 但是,您始终需要记住,模拟的使用提供了灵活性,但是增加了监视模拟行为的相关性的义务。 因此,我拒绝替换elasticsearch:安装和填充它非常简单。
第一次模拟
服务器以/配置中的几种方式将配置发送到GET请求。 我们对两种方式感兴趣。 第一个是/configuration/data_cluster
与集群配置
{ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass" } }
第二是/configuration/reconciliation
与钻井应用程序的配置/configuration/reconciliation
{ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass" } } }
困难在于,您需要能够在测试期间或测试之间更改服务器响应,以测试应用程序如何响应配置更改,错误的密码等。
因此,静态模拟和用于单元测试中的模拟工具(模拟,来自pytest的Monkeypatch等)对我们不起作用。 我找到了一个我认为很适合我的出色的pretenders
库。 伪装程序提供使用规则确定HTTP服务器如何响应请求的HTTP服务器的功能。 规则存储在预设中,从而可以隔离不同测试套件的模拟。 预设可以清除并重新填写,使您可以根据需要更新答案。 在准备环境期间,只需将服务器本身提升一次即可:
python -m pretenders.server.server --host 127.0.0.1 --port 8000
在测试中,我们需要增加客户端的使用。 在最简单的情况下,当答案在测试中被完全硬编码时,可能看起来像这样:
import json import pytest from pretenders.client.http import HTTPMock from pretenders.common.constants import FOREVER @pytest.fixture def configuration_server_mock(request): mock = HTTPMock(host="127.0.0.1", port=8000, name="server") request.addfinalizer(mock.reset) return mock def test_something(configuration_server_mock): configuration_server_mock.when("GET /configuration/data_cluster").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, }), status=200, times=FOREVER, ) configuration_server_mock.when("GET /configuration/reconciliation").reply( headers={"Content-Type": "application/json"}, body=json.dumps({ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass", }, }, }), status=200, times=FOREVER, )
但这还不是全部。 凭借其灵活性, pretenders
有两个必须记住的局限性,在我们的案例中必须加以解决:
- 不能一次删除一个规则。 为了更改答案,您必须删除整个预设并再次重新创建所有规则。
- 规则中使用的所有路径都是相对的。 预设具有格式为/嘲讽http / <预设名称>的唯一路径,并且该路径是规则中所有已创建路径的通用前缀。 被测应用程序仅接收主机名,并且不知道前缀。
第一个限制是非常不愉快的,但是可以通过编写一个模块来封装该配置来解决。 例如
configuration.data_cluster.port = 443
或(以减少更新请求的频率)
data_cluster_config = get_default_data_cluster_config() data_cluster_config.port = 443 configuration.update_data_cluster(data_cluster_config)
这种封装使我们几乎可以轻松地更新所有路径。 您还可以为每个单独的端点创建一个单独的预设,并创建一个通用(主要)预设,将其重定向(通过307或308)到单独的预设。 然后,您只能清除一个预设以更新规则。
为了摆脱前缀,您可以使用mitmproxy库。 这是一个功能强大的工具,除其他功能外,还可以重定向请求。 我们将删除前缀,如下所示:
mitmdump --mode reverse:http://127.0.0.1:8000 --replacements :~http:^/:/mockhttp/server/ --listen-host 127.0.01 --listen-port 80
该命令的参数执行以下操作:
--listen-host 127.0.0.1
和--listen-port 80
明显。 Mitmproxy提升其服务器,并使用这些参数确定该服务器将侦听的接口和端口。--mode reverse:http://127.0.0.1:8000
意味着对mitproxy服务器的请求将重定向到http://127.0.0.1:8000
。 在这里阅读更多。--replacements :~http:^/:/mockhttp/server/
定义了用于更改请求的模板。 它由三部分组成:一个请求过滤器(HTTP请求为/mockhttp/server
),一个用于更改的模板(用^/
替换路径的开头)和实际替换的/mockhttp/server
( /mockhttp/server
)。 在这里阅读更多。
在我们的示例中,我们向所有HTTP请求添加了mockhttp/server
,并将其重定向到http://127.0.0.1:8000
,即 给我们的服务器伪装者。 结果,我们已经实现了,现在可以通过对http://127.0.0.1/configuration/data_cluster
的GET请求获得配置。
总的来说,我对pretenders
和mitmproxy
的设计感到满意。 由于表面上很复杂-毕竟是2台服务器而不是一台真正的服务器-准备工作包括安装2个软件包并在命令行上执行2个命令以启动它们。 并不是所有的事情都像管理模拟那样简单,但是所有的复杂性都只在一个地方(管理预设),并且可以非常简单,可靠地解决。 但是,新的情况出现在问题中,这使我开始思考新的解决方案。
第二次模拟
在那一刻之前,我几乎没有说过参考数据来自何处。 细心的读者可能会注意到,在上面的示例中,文件系统的路径用作数据源的地址。 它确实可以像这样工作,但仅适用于其中一个供应商。 另一个供应商提供了一个用于接收应用程序的API,正是这个问题引起了他的兴趣。 在测试过程中很难提高供应商的API,因此我计划按照与以前相同的方案将其替换为模拟。 但是要接收申请,需要一个表格
GET /application-history?page=2&size=5&start=1569148012&end=1569148446
这里有2分。 首先,一些选择。 事实是,可以以任何顺序指定参数,这极大地使pretenders
规则的正则表达式复杂化。 还必须记住,参数是可选的,但这不是诸如随机顺序之类的问题。 其次,最后一个参数(开始和结束)指定过滤订单的时间间隔。 这种情况下的问题在于,我们无法预先预测应用程序将使用哪个间隔(不是幅度,而是开始时间)来形成模拟响应。 简而言之,我们需要知道并使用参数值来形成“合理的”答案。 例如,在这种情况下,“合理性”很重要,因此我们可以测试应用程序是否遍历了分页的所有页面:如果我们以相同的方式回答所有请求,那么由于仅请求五分之一的页面,我们将无法发现缺陷。 。
我试图寻找替代解决方案,但最终我决定尝试编写自己的解决方案。 因此服务器松动了 。 这是一个Flask应用程序,在启动后可以在其中配置路径和响应。 开箱即用,他知道如何使用有关请求类型(GET,POST等)和路径的规则。 这使您可以替换原始任务中的pretenders
和mitmproxy
。 我还将展示如何将其用于为供应商API创建模拟。
应用程序需要2条主要路径:
- 基本端点。 这是将用于所有已配置规则的前缀。
- 配置端点。 这是您可以用来配置模拟服务器本身的那些请求的前缀。
python -m looseserver.default.server.run --host 127.0.0.1 --port 80 --base-endpoint / --configuration-endpoint /_mock_configuration/
通常,最好不要配置基本端点和配置端点,以使一个端点是另一个端点的父节点。 否则,存在配置和测试路径冲突的风险。 配置端点优先,因为Flask规则是在配置之前添加的,而不是在动态路径中添加的。 在本例中,如果我们不打算在此模拟中包括供应商API,则可以使用--base-endpoint /configuration/
。
测试的最简单版本变化不大
import json import pytest from looseserver.default.client.http import HTTPClient from looseserver.default.client.rule import PathRule from looseserver.default.client.response import FixedResponse @pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = HTTPClient(configuration_url="http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_rule(self, path, json_response): rule = self._client.create_rule(PathRule(path=path)) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) def _delete_rules(self): for rule_id in self._rule_ids: self._client.remove_rule(rule_id=rule_id) mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): configuration_server_mock.create_rule( path="configuration/data_cluster", json_response={ "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass", }, } ) configuration_server_mock.create_rule( path="configuration/reconciliation", json_response={ "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///applications", "credentials": { "username": "user", "password": "pass", }, }, } )
固定装置变得越来越困难,但是现在可以一次删除一个规则,从而简化了使用它们的工作。 不再需要使用mitmproxy
。
让我们回到供应商API。 我们将为松散的服务器创建一种新型的规则,根据参数值,它将给出不同的答案。 接下来,我们将这个规则用于page参数。
需要为服务器和客户端创建新的规则和答案。 让我们从服务器开始:
from looseserver.server.rule import ServerRule class ServerParameterRule(ServerRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER"): super(ServerParameterRule, self).__init__(rule_type=rule_type) self._parameter_name = parameter_name self._parameter_value = parameter_value def is_match_found(self, request): if self._parameter_value is None: return self._parameter_name not in request.args return request.args.get(self._parameter_name) == self._parameter_value
每个规则都必须定义一个is_match_found
方法,该方法确定它是否应适用于给定的请求。 输入参数是请求对象。 创建新规则后,有必要“教导”服务器以使其从客户端接受。 为此,请使用RuleFactory
:
from looseserver.default.server.rule import create_rule_factory from looseserver.default.server.application import configure_application def _create_application(base_endpoint, configuration_endpoint): server_rule_factory = create_rule_factory(base_endpoint) def _parse_param_rule(rule_type, parameters): return ServerParameterRule( rule_type=rule_type, parameter_name=parameters["parameter_name"], parameter_value=parameters["parameter_value"], ) server_rule_factory.register_rule( rule_type="PARAMETER", parser=_parse_param_rule, serializer=lambda rule_type, rule: None, ) return configure_application( rule_factory=server_rule_factory, base_endpoint=base_endpoint, configuration_endpoint=configuration_endpoint, ) if __name__ == "__main__": application = _create_application(base_endpoint="/", configuration_endpoint="/_mock_configuration") application.run(host="127.0.0.1", port=80)
在这里,我们默认情况下为规则创建一个工厂,以便它包含我们之前使用的规则,并注册一个新类型。 在这种情况下,客户端不需要规则信息,因此serializer
实际上不执行任何操作。 此工厂进一步转移到应用程序。 它可以像常规的Flask应用程序一样运行。
与客户的情况相似:我们创建规则和工厂。 但是对于客户端,首先,没有必要定义is_match_found
方法,其次,在这种情况下,需要序列化程序才能将规则发送到服务器。
from looseserver.client.rule import ClientRule from looseserver.default.client.rule import create_rule_factory class ClientParameterRule(ClientRule): def __init__(self, parameter_name, parameter_value=None, rule_type="PARAMETER", rule_id=None): super(ClientParameterRule, self).__init__(rule_type=rule_type, rule_id=rule_id) self.parameter_name = parameter_name self.parameter_value = parameter_value def _create_client(configuration_url): def _serialize_param_rule(rule): return { "parameter_name": rule.parameter_name, "parameter_value": rule.parameter_value, } client_rule_factory = create_rule_factory() client_rule_factory.register_rule( rule_type="PARAMETER", parser=lambda rule_type, parameters: ClientParameterRule(rule_type=rule_type, parameter_name=None), serializer=_serialize_param_rule, ) return HTTPClient(configuration_url=configuration_url, rule_factory=client_rule_factory)
仍然可以使用_create_client
创建客户端,并且可以在测试中使用规则。 在下面的示例中,我添加了另一个默认规则的使用: CompositeRule
。 它使您可以将多个规则组合为一个,以便仅当每个规则调用is_match_found
返回True时它们才起作用。
@pytest.fixture def configuration_server_mock(request): class MockFactory: def __init__(self): self._client = _create_client("http://127.0.0.1/_mock_configuration/") self._rule_ids = [] def create_paged_rule(self, path, page, json_response): rule_prototype = CompositeRule( children=[ PathRule(path=path), ClientParameterRule(parameter_name="page", parameter_value=page), ] ) rule = self._client.create_rule(rule_prototype) self._rule_ids.append(rule.rule_id) response = FixedResponse( headers={"Content-Type": "application/json"}, status=200, body=json.dumps(json_response), ) self._client.set_response(rule_id=rule.rule_id, response=response) ... mock = MockFactory() request.addfinalizer(mock._delete_rules) return mock def test_something(configuration_server_mock): ... configuration_server_mock.create_paged_rule( path="application-history", page=None, json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="1", json_response=["1", "2", "3"], ) configuration_server_mock.create_paged_rule( path="application-history", page="2", json_response=["4", "5"], )
结论
pretenders
和mitmproxy
为创建mitmproxy
提供了强大而灵活的工具。 优点:
- 设置简单。
- 能够使用预设隔离查询集。
- 一次删除整个隔离集。
缺点包括:
- 需要为规则创建正则表达式。
- 无法单独更改规则。
- 所有创建的路径都存在前缀,或者使用
mitmproxy
进行重定向。
文档链接:
假装者
三甲氧基
服务器松动