Outra biblioteca simulada

Boa tarde Estou envolvido na automação de testes. Como todos os engenheiros de automação, tenho um conjunto de bibliotecas e ferramentas que geralmente escolho para escrever testes. Periodicamente, porém, há situações em que nenhuma das bibliotecas familiares pode resolver o problema com o risco de tornar os autotestes instáveis ​​ou frágeis. Neste artigo, gostaria de dizer como a tarefa aparentemente padrão de usar zombarias me levou a escrever meu módulo. Eu também gostaria de compartilhar minha decisão e ouvir comentários.


App


Um dos setores necessários no setor financeiro é a auditoria. Os dados precisam ser verificados regularmente (reconciliação). Nesse sentido, o aplicativo que eu testei apareceu. Para não falar sobre algo abstrato, vamos imaginar que nossa equipe esteja desenvolvendo um aplicativo para processar aplicativos de mensageiros instantâneos. Para cada aplicativo, um evento apropriado deve ser criado na elasticsearch. O aplicativo de verificação será nosso monitoramento de que os aplicativos não são ignorados.


Então, imagine que temos um sistema que possui os seguintes componentes:


  1. Servidor de configuração Para o usuário, este é um único ponto de entrada em que ele configura não apenas o aplicativo para verificação, mas também outros componentes do sistema.
  2. Aplicativo de verificação.
  3. Dados dos aplicativos de processamento de aplicativos armazenados na elasticsearch.
  4. Dados de referência. O formato dos dados depende do messenger com o qual o aplicativo está integrado.

Desafio


A automação de teste, neste caso, parece bastante direta:


  1. Preparação do ambiente:
    • O Elasticsearch é instalado com configuração mínima (usando o msi e a linha de comando).
    • Um aplicativo de verificação está instalado.
  2. Execução de teste:
    • Um aplicativo de verificação está configurado.
    • O Elasticsearch é preenchido com dados de teste para o teste correspondente (quantos aplicativos foram processados).
    • O aplicativo recebe dados de "referência" do messenger (quantos aplicativos supostamente eram realmente).
    • O veredicto emitido pelo aplicativo é verificado: o número de aplicativos verificados com sucesso, o número de aplicativos ausentes etc.
  3. Limpando o meio ambiente.

O problema é que estamos testando o monitoramento, mas para configurá-lo, precisamos de dados do servidor de configuração. Em primeiro lugar, instalar e configurar um servidor para cada execução é uma operação demorada (possui sua própria base, por exemplo). Em segundo lugar, quero isolar aplicativos para simplificar a localização de problemas ao encontrar um defeito. No final, foi decidido usar o mock.


Aqui pode surgir a pergunta: "Se ainda simularmos o servidor, talvez não possamos gastar tempo instalando e preenchendo a pesquisa elástica, mas substituí-la por uma simulação?". Ainda assim, você sempre precisa lembrar que o uso de simulação fornece flexibilidade, mas acrescenta uma obrigação de monitorar a relevância do comportamento da simulação. Portanto, recusei-me a substituir o elasticsearch: é bastante simples de instalar e preencher.


Primeiro mock


O servidor envia a configuração para solicitações GET de várias maneiras em / configuration. Estamos interessados ​​em duas maneiras. O primeiro é /configuration/data_cluster com configuração de cluster


 { "host": "127.0.0.1", "port": 443, "credentials": { "username": "user", "password": "pass" } } 

O segundo é /configuration/reconciliation com a configuração do aplicativo de perfuração


 { "reconciliation_interval": 3600, "configuration_update_interval": 60, "source": { "address": "file:///c:/path", "credentials": { "username": "user", "password": "pass" } } } 

A dificuldade é que você precisa alterar a resposta do servidor durante o teste ou entre testes para testar como o aplicativo reage a alterações na configuração, senhas incorretas etc.


Portanto, zombarias estáticas e ferramentas para zombarias em testes de unidade (simulação, monkeypatch do pytest etc.) não funcionarão para nós. Encontrei uma ótima biblioteca de pretenders que achava certa para mim. Pretenders fornece a capacidade de criar um servidor HTTP com regras que determinam como o servidor responderá às solicitações. As regras são armazenadas em predefinições, o que permite isolar simulações para diferentes suítes de teste. As predefinições podem ser limpas e preenchidas novamente, permitindo que você atualize as respostas conforme necessário. Basta elevar o próprio servidor uma vez durante a preparação do ambiente:


 python -m pretenders.server.server --host 127.0.0.1 --port 8000 

E nos testes, precisamos adicionar o uso do cliente. No caso mais simples, quando as respostas são completamente codificadas nos testes, pode ser assim:


 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, ) # test application 

Mas isso não é tudo. Com sua flexibilidade, os pretenders têm duas limitações que devem ser lembradas e devem ser tratadas em nosso caso:


  1. As regras não podem ser excluídas uma de cada vez. Para alterar a resposta, você deve excluir toda a predefinição e recriar todas as regras novamente.
  2. Todos os caminhos usados ​​nas regras são relativos. As predefinições têm um caminho exclusivo no formato / mockhttp / <preset_name>, e esse caminho é um prefixo comum para todos os caminhos criados nas regras. O aplicativo em teste recebe apenas o nome do host e não pode saber sobre o prefixo.

A primeira limitação é muito desagradável, mas pode ser resolvida escrevendo um módulo que encapsula o trabalho com a configuração. Por exemplo, então


 configuration.data_cluster.port = 443 

ou (para fazer solicitações de atualização com menos frequência)


 data_cluster_config = get_default_data_cluster_config() data_cluster_config.port = 443 configuration.update_data_cluster(data_cluster_config) 

Esse encapsulamento nos permite atualizar todos os caminhos quase sem dor. Você também pode criar uma predefinição individual para cada terminal individual e uma predefinição geral (principal), redirecionando (até 307 ou 308) para as individuais. Em seguida, você pode limpar apenas uma predefinição para atualizar a regra.


Para se livrar dos prefixos, você pode usar a biblioteca mitmproxy . Essa é uma ferramenta poderosa que permite, entre outras coisas, redirecionar solicitações. Removeremos os prefixos da seguinte maneira:


 mitmdump --mode reverse:http://127.0.0.1:8000 --replacements :~http:^/:/mockhttp/server/ --listen-host 127.0.01 --listen-port 80 

Os parâmetros deste comando fazem o seguinte:


  1. --listen-host 127.0.0.1 e --listen-port 80 óbvias. O Mitmproxy eleva seu servidor e, com esses parâmetros, determinamos a interface e a porta que esse servidor escutará.
  2. --mode reverse:http://127.0.0.1:8000 significa que as solicitações ao servidor mitproxy serão redirecionadas para http://127.0.0.1:8000 . Leia mais aqui .
  3. --replacements :~http:^/:/mockhttp/server/ define um modelo pelo qual as solicitações serão alteradas. Ele consiste em três partes: um filtro de solicitação ( ~http para solicitações HTTP), um modelo para alterar ( ^/ para substituir o início do caminho) e realmente substituir ( /mockhttp/server ). Leia mais aqui .

No nosso caso, adicionamos mockhttp/server a todas as solicitações HTTP e as redirecionamos para http://127.0.0.1:8000 , ou seja, para os nossos pretendentes servidor. Como resultado, conseguimos que agora a configuração possa ser obtida com uma solicitação GET para http://127.0.0.1/configuration/data_cluster .


Em geral, fiquei satisfeito com o design com pretenders e mitmproxy . Com a aparente complexidade - afinal, 2 servidores em vez de um real - a preparação consiste em instalar 2 pacotes e executar 2 comandos na linha de comando para iniciá-los. Nem tudo é tão simples no gerenciamento de uma simulação, mas toda a complexidade está em apenas um lugar (gerenciamento de predefinições) e é resolvida de maneira simples e confiável. No entanto, novas circunstâncias surgiram no problema que me fez pensar em uma nova solução.


Segunda simulação


Até aquele momento, eu quase não disse de onde vinham os dados de referência. Um leitor atento pode perceber que, no exemplo acima, o caminho para o sistema de arquivos é usado como o endereço da fonte de dados. E realmente funciona algo assim, mas apenas para um dos fornecedores. Outro fornecedor fornece uma API para o recebimento de aplicativos e foi com ele que surgiu um problema. É difícil elevar a API do fornecedor durante os testes, então planejei substituí-la por simulação de acordo com o mesmo esquema de antes. Mas, para receber solicitações, uma solicitação do formulário


 GET /application-history?page=2&size=5&start=1569148012&end=1569148446 

Existem 2 pontos aqui. Em primeiro lugar, algumas opções. O fato é que os parâmetros podem ser especificados em qualquer ordem, o que complica bastante a expressão regular da regra dos pretenders . Também é necessário lembrar que os parâmetros são opcionais, mas isso não é um problema como ordem aleatória. Em segundo lugar, os últimos parâmetros (início e fim) especificam o intervalo de tempo para filtrar a ordem. E o problema nesse caso é que não podemos prever com antecedência qual intervalo (não a magnitude, mas a hora de início) será usado pelo aplicativo para formar a resposta simulada. Simplificando, precisamos conhecer e usar os valores dos parâmetros para formar uma resposta "razoável". A “razoabilidade” neste caso é importante, por exemplo, para que possamos testar se o aplicativo percorre todas as páginas da paginação: se respondermos a todos os pedidos da mesma maneira, não conseguiremos encontrar defeitos devido ao fato de que apenas uma página em cada cinco é solicitada .


Tentei procurar soluções alternativas, mas no final decidi tentar escrever as minhas. Portanto, havia um servidor solto . Este é um aplicativo Flask no qual os caminhos e as respostas podem ser configurados após o início. Fora da caixa, ele sabe como trabalhar com regras para o tipo de solicitação (GET, POST etc.) e para o caminho. Isso permite substituir pretenders e mitmproxy na tarefa original. Também mostrarei como ele pode ser usado para criar uma simulação para a API do fornecedor.


Um aplicativo precisa de 2 caminhos principais:


  1. Ponto final de base. Este é o mesmo prefixo que será usado para todas as regras configuradas.
  2. Ponto de extremidade de configuração. Esse é o prefixo daqueles pedidos com os quais você pode configurar o próprio servidor simulado.

 python -m looseserver.default.server.run --host 127.0.0.1 --port 80 --base-endpoint / --configuration-endpoint /_mock_configuration/ 

Em geral, é melhor não configurar o ponto de extremidade base e o ponto de extremidade de configuração para que um seja o pai do outro. Caso contrário, existe o risco de os caminhos de configuração e teste entrarem em conflito. O ponto de extremidade da configuração terá precedência, pois as regras do Flask foram adicionadas para configuração mais cedo do que para caminhos dinâmicos. No nosso caso, poderíamos usar --base-endpoint /configuration/ se não formos incluir a API do fornecedor nesse mock.


A versão mais simples dos testes não muda muito


 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", }, }, } ) 

O equipamento ficou mais difícil, mas agora as regras podem ser excluídas uma de cada vez, o que simplifica o trabalho com elas. O uso de mitmproxy não mitmproxy mais necessário.


Vamos voltar à API do fornecedor. Criaremos um novo tipo de regras para um servidor solto que, dependendo do valor do parâmetro, fornecerá respostas diferentes. Em seguida, usaremos essa regra para o parâmetro da página.


Novas regras e respostas precisam ser criadas para o servidor e o cliente. Vamos começar com o servidor:


 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 

Cada regra deve definir um método is_match_found , que determina se deve funcionar para uma determinada solicitação ou não. O parâmetro de entrada para ele é o objeto de solicitação. Após a criação da nova regra, é necessário "ensinar" o servidor a aceitá-la do cliente. Para fazer isso, use 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) 

Aqui, criamos uma fábrica para as regras por padrão, para que ela contenha as regras que usamos antes e registramos um novo tipo. Nesse caso, o cliente não precisa das informações da regra; portanto, o serializer não faz nada. Além disso, esta fábrica é transferida para a aplicação. E já pode ser executado como um aplicativo Flask comum.


A situação com o cliente é semelhante: criamos uma regra e uma fábrica. Mas para o cliente, em primeiro lugar, não é necessário definir o método is_match_found e, em segundo lugar, o serializador nesse caso é necessário para enviar a regra ao servidor.


 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) 

Resta usar _create_client para criar o cliente, e as regras podem ser usadas em testes. No exemplo abaixo, adicionei o uso de outra regra padrão: CompositeRule . Ele permite combinar várias regras em uma, para que funcionem apenas se cada uma retornar True para chamar is_match_found .


 @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"], ) 

Conclusão


Uma mitmproxy pretenders e mitmproxy fornece uma ferramenta poderosa e flexível o suficiente para criar zombarias. Suas vantagens:


  1. Configuração fácil.
  2. Capacidade de isolar conjuntos de consultas usando predefinições.
  3. Exclui um conjunto isolado inteiro de uma só vez.

Por contras incluem:


  1. A necessidade de criar expressões regulares para regras.
  2. A incapacidade de alterar as regras individualmente.
  3. A presença de um prefixo para todos os caminhos criados ou o uso de redirecionamento usando mitmproxy .

Links para documentação:
Pretenders
Mitmproxy
Servidor solto

Source: https://habr.com/ru/post/pt476904/


All Articles