Otra biblioteca simulada

Buenas tardes Estoy comprometido con la automatización de pruebas. Como todos los ingenieros de automatización, tengo un conjunto de bibliotecas y herramientas que generalmente elijo para escribir pruebas. Pero periódicamente hay situaciones en las que ninguna de las bibliotecas familiares puede resolver el problema con el riesgo de hacer que las auto-pruebas sean inestables o frágiles. En este artículo, me gustaría contarles cómo la tarea aparentemente estándar de usar mock'ov me llevó a escribir mi módulo. También me gustaría compartir mi decisión y escuchar comentarios.


App


Uno de los sectores necesarios en el sector financiero es la auditoría. Los datos deben verificarse regularmente (conciliación). En este sentido, apareció la aplicación que probé. Para no hablar de algo abstracto, imaginemos que nuestro equipo está desarrollando una aplicación para procesar aplicaciones de mensajería instantánea. Para cada aplicación, se debe crear un evento apropiado en elasticsearch. La aplicación de verificación será nuestro monitoreo de que las aplicaciones no se omiten.


Entonces, imagine que tenemos un sistema que tiene los siguientes componentes:


  1. Servidor de configuración. Para el usuario, este es un punto de entrada único donde configura no solo la aplicación para la verificación, sino también otros componentes del sistema.
  2. Solicitud de verificación.
  3. Datos de la aplicación que procesan aplicaciones que se almacenan en elasticsearch.
  4. Datos de referencia El formato de datos depende del mensajero con el que está integrada la aplicación.

Desafío


Las pruebas de automatización en este caso parecen bastante sencillas:


  1. Preparación del medio ambiente:
    • Elasticsearch se instala con una configuración mínima (usando msi y la línea de comando).
    • Se instala una aplicación de verificación.
  2. Prueba de ejecución:
    • Se configura una aplicación de verificación.
    • Elasticsearch se llena con datos de prueba para la prueba correspondiente (cuántas aplicaciones se procesaron).
    • La aplicación recibe datos de "referencia" del messenger (cuántas aplicaciones se suponía que realmente eran).
    • Se verifica el veredicto emitido por la aplicación: la cantidad de aplicaciones verificadas con éxito, la cantidad de aplicaciones faltantes, etc.
  3. Limpieza del medio ambiente.

El problema es que estamos probando el monitoreo, pero para configurarlo, necesitamos datos del servidor de configuración. En primer lugar, instalar y configurar un servidor para cada ejecución es una operación que requiere mucho tiempo (tiene su propia base, por ejemplo). En segundo lugar, quiero aislar las aplicaciones para simplificar la localización de problemas al encontrar un defecto. Al final, se decidió usar simulacro.


Esto puede plantear la pregunta: "Si todavía nos burlamos del servidor, tal vez no podamos pasar tiempo instalando y llenando Elasticsearch, ¿pero reemplazar el simulacro? Pero aún así, siempre debe recordar que el uso de simulacro proporciona flexibilidad, pero agrega la obligación de controlar la relevancia del comportamiento simulado. Por lo tanto, me negué a reemplazar elasticsearch: es bastante simple instalarlo y llenarlo.


Primer simulacro


El servidor envía la configuración a las solicitudes GET de varias maneras en / configuration. Estamos interesados ​​en dos formas. El primero es /configuration/data_cluster con configuración de clúster


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

El segundo es /configuration/reconciliation con la configuración de la aplicación de perforación


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

La dificultad es que necesita poder cambiar la respuesta del servidor durante la prueba o entre pruebas para probar cómo responde la aplicación a los cambios de configuración, contraseñas incorrectas, etc.


Por lo tanto, los simulacros estáticos y las herramientas para simulacros en las pruebas unitarias (simulacro, monkeypatch de pytest, etc.) no funcionarán para nosotros. Encontré una gran biblioteca de pretenders que pensé que era adecuada para mí. Pretenders proporciona la capacidad de crear un servidor HTTP con reglas que determinan cómo responderá el servidor a las solicitudes. Las reglas se almacenan en preajustes, lo que permite aislar simulacros para diferentes conjuntos de pruebas. Los ajustes preestablecidos se pueden borrar y volver a llenar, lo que le permite actualizar las respuestas según sea necesario. Es suficiente elevar el servidor una vez durante la preparación del entorno:


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

Y en las pruebas necesitamos agregar el uso del cliente. En el caso más simple, cuando las respuestas están completamente codificadas en las pruebas, puede verse así:


 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 

Pero eso no es todo. Con su flexibilidad, pretenders tiene dos limitaciones que deben recordarse y deben abordarse en nuestro caso:


  1. Las reglas no se pueden eliminar de una en una. Para cambiar la respuesta, debe eliminar todo el preajuste y volver a crear todas las reglas.
  2. Todas las rutas utilizadas en las reglas son relativas. Los ajustes preestablecidos tienen una ruta única de la forma / mockhttp / <preset_name>, y esta ruta es un prefijo común para todas las rutas creadas en las reglas. La aplicación bajo prueba recibe solo el nombre de host y no puede conocer el prefijo.

La primera limitación es muy desagradable, pero se puede resolver escribiendo un módulo que encapsule el trabajo con la configuración. Por ejemplo, entonces


 configuration.data_cluster.port = 443 

o (para realizar solicitudes de actualización con menos frecuencia)


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

Tal encapsulación nos permite actualizar todas las rutas casi sin dolor. También puede crear un preajuste individual para cada punto final individual y un preajuste general (principal), redirigiendo (a través de 307 o 308) a los individuales. Entonces solo puede borrar un preset para actualizar la regla.


Para deshacerse de los prefijos, puede usar la biblioteca mitmproxy . Esta es una herramienta poderosa que permite, entre otras cosas, redirigir solicitudes. Eliminaremos los prefijos de la siguiente manera:


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

Los parámetros de este comando hacen lo siguiente:


  1. --listen-host 127.0.0.1 y --listen-port 80 obvios. Mitmproxy eleva su servidor, y con estos parámetros determinamos la interfaz y el puerto que escuchará este servidor.
  2. --mode reverse:http://127.0.0.1:8000 significa que las solicitudes al servidor mitproxy serán redirigidas a http://127.0.0.1:8000 . Lee más aquí .
  3. --replacements :~http:^/:/mockhttp/server/ define una plantilla mediante la cual se modificarán las solicitudes. Consta de tres partes: un filtro de solicitud ( ~http para solicitudes HTTP), una plantilla para cambiar ( ^/ para reemplazar el comienzo de la ruta) y reemplazar ( /mockhttp/server ). Lee más aquí .

En nuestro caso, agregamos mockhttp/server a todas las solicitudes HTTP y las redirigimos a http://127.0.0.1:8000 , es decir a nuestros pretendientes de servidor. Como resultado, hemos logrado que ahora la configuración se pueda obtener con una solicitud GET a http://127.0.0.1/configuration/data_cluster .


En general, quedé satisfecho con el diseño con pretenders y mitmproxy . Con la aparente complejidad, después de todo, 2 servidores en lugar de uno real, la preparación consiste en instalar 2 paquetes y ejecutar 2 comandos en la línea de comandos para iniciarlos. No todo es tan simple en la gestión de un simulacro, pero toda la complejidad radica en un solo lugar (gestión de presets) y se resuelve de manera bastante simple y confiable. Sin embargo, aparecieron nuevas circunstancias en el problema que me hicieron pensar en una nueva solución.


Segundo simulacro


Hasta ese momento, casi no dije de dónde provenían los datos de referencia. Un lector atento podría notar que en el ejemplo anterior, la ruta al sistema de archivos se usa como la dirección de origen de datos. Y realmente funciona de esta manera, pero solo para uno de los proveedores. Otro proveedor proporciona una API para recibir aplicaciones, y fue con él que surgió un problema. Es difícil aumentar la API del proveedor durante las pruebas, por lo que planeé reemplazarlo con simulacro de acuerdo con el mismo esquema que antes. Pero para recibir solicitudes, una solicitud del formulario


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

Hay 2 puntos aquí. En primer lugar, algunas opciones. El hecho es que los parámetros se pueden especificar en cualquier orden, lo que complica enormemente la expresión regular de la regla de pretenders . También es necesario recordar que los parámetros son opcionales, pero esto no es un problema como el orden aleatorio. En segundo lugar, los últimos parámetros (inicio y fin) especifican el intervalo de tiempo para filtrar el pedido. Y el problema en este caso es que no podemos predecir de antemano qué intervalo (no la magnitud, sino el tiempo de inicio) será utilizado por la aplicación para formar la respuesta simulada. En pocas palabras, necesitamos conocer y usar los valores de los parámetros para formar una respuesta "razonable". La "razonabilidad" en este caso es importante, por ejemplo, para que podamos probar que la aplicación pasa por todas las páginas de la paginación: si respondemos a todas las solicitudes de la misma manera, no podremos encontrar defectos debido al hecho de que solo se solicita una de cada cinco páginas .


Intenté buscar soluciones alternativas, pero al final decidí escribir la mía. Entonces había un servidor suelto . Esta es una aplicación Flask en la que se pueden configurar rutas y respuestas después de que se inicia. Fuera de la caja, él sabe cómo trabajar con reglas para el tipo de solicitud (GET, POST, etc.) y para la ruta. Esto le permite reemplazar pretenders y mitmproxy en la tarea original. También mostraré cómo se puede usar para crear un simulacro para la API del proveedor.


Una aplicación necesita 2 rutas principales:


  1. Punto final base. Este es el mismo prefijo que se usará para todas las reglas configuradas.
  2. Punto final de configuración. Este es el prefijo de esas solicitudes con las que puede configurar el servidor simulado.

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

En general, es mejor no configurar el punto final base y el punto final de configuración para que uno sea el padre del otro. De lo contrario, existe el riesgo de que las rutas de configuración y prueba entren en conflicto. El punto final de la configuración tendrá prioridad, ya que las reglas de Flask se agregan para la configuración antes que para las rutas dinámicas. En nuestro caso, podríamos usar --base-endpoint /configuration/ si no íbamos a incluir la API del proveedor en este simulacro.


La versión más simple de las pruebas no cambia mucho.


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

La fijación se ha vuelto más difícil, pero las reglas ahora se pueden eliminar de una en una, lo que simplifica el trabajo con ellas. Usar mitmproxy ya no es necesario.


Volvamos a la API del proveedor. Crearemos un nuevo tipo de reglas para un servidor suelto, que, según el valor del parámetro, dará diferentes respuestas. A continuación, utilizaremos esta regla para el parámetro de página.


Se deben crear nuevas reglas y respuestas tanto para el servidor como para el cliente. Comencemos con el 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 regla debe definir un método is_match_found , que determina si debe funcionar para una solicitud determinada o no. El parámetro de entrada para ello es el objeto de solicitud. Después de crear la nueva regla, es necesario "enseñar" al servidor a aceptarla del cliente. Para hacer esto, 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) 

Aquí creamos una fábrica para las reglas de forma predeterminada, de modo que contenga las reglas que usamos antes y registremos un nuevo tipo. En este caso, el cliente no necesita la información de la regla, por lo que el serializer realidad no hace nada. Además, esta fábrica se transfiere a la aplicación. Y ya se puede ejecutar como una aplicación Flask normal.


La situación con el cliente es similar: creamos una regla y una fábrica. Pero para el cliente, en primer lugar, no es necesario definir el método is_match_found , y en segundo lugar, el serializador en este caso es necesario para enviar la regla al 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) 

Queda por usar _create_client para crear el cliente, y las reglas se pueden usar en las pruebas. En el siguiente ejemplo, agregué el uso de otra regla predeterminada: CompositeRule . Le permite combinar varias reglas en una para que funcionen solo si cada una de ellas devuelve True para llamar a 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"], ) 

Conclusión


Una mitmproxy pretenders y mitmproxy proporciona una herramienta lo suficientemente potente y flexible para crear simulacros. Sus ventajas:


  1. Fácil instalación
  2. Capacidad para aislar conjuntos de consultas mediante ajustes preestablecidos.
  3. Elimina todo un conjunto aislado a la vez.

Por contra incluyen:


  1. La necesidad de crear expresiones regulares para reglas.
  2. La incapacidad de cambiar las reglas individualmente.
  3. La presencia de un prefijo para todas las rutas creadas o el uso de redirección utilizando mitmproxy .

Enlaces de documentación:
Pretendientes
Mitmproxi
Servidor suelto

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


All Articles