Escalando pruebas de Android en Odnoklassniki



¡Hola! Mi nombre es Roman Ivanitsky, trabajo en el equipo de automatización de pruebas de Odnoklassniki. OK es un gran servicio con más de 70 millones de usuarios. Si hablamos de dispositivos móviles, la mayoría usa OK.RU en teléfonos inteligentes con Android. Por esta razón, nos tomamos muy en serio la prueba de nuestra aplicación de Android. En este artículo contaré la historia del desarrollo de las pruebas automatizadas en nuestra empresa.

2012, Odnoklassniki, la compañía está experimentando un aumento activo en el número de usuarios y un aumento en el número de funciones de usuario. Para satisfacer los objetivos del negocio, era necesario acortar el ciclo de lanzamiento, pero esto se vio obstaculizado por el hecho de que todas las funcionalidades se probaron manualmente. La solución a este problema vino por sí sola: necesitamos pruebas automáticas. Por lo tanto, en 2012 en Odnoklassniki apareció un equipo de automatización de pruebas, y el primer paso fue comenzar a escribir pruebas.

Un poco de historia


Las primeras pruebas automáticas en Odnoklassniki se escribieron en Selenium, para su lanzamiento plantearon Jenkins, Selenium Grid con Selenium Hub y un conjunto de Selenium Node.

Solución rápida, inicio rápido, beneficio rápido: perfecto.

Con el tiempo, el número de pruebas aumentó y aparecieron servicios auxiliares, por ejemplo, servicios de lanzamiento, servicio de informes, servicio de datos de prueba. A finales de 2014, tuvimos mil pruebas que se ejecutaron en unos quince o veinte minutos. Esto no nos convenía, ya que estaba claro que el número de pruebas aumentaría, y con ello aumentaría el tiempo necesario para ejecutarlas.

En ese momento, la infraestructura de prueba automatizada se veía así:



Sin embargo, con una cantidad de Nodo de selenio mayor o igual a 200, el Hub no pudo hacer frente a la carga. Ahora este problema ya ha sido estudiado, y es por eso que aparecieron herramientas como Zalenium o el Selenoid favorito de todos. Pero en 2014 no había una solución estándar, por lo que decidimos hacer la nuestra.

Definió los requisitos mínimos que debe cumplir el servicio:

  1. Escalabilidad. No queremos depender de las limitaciones del Selenium Hub.
  2. Estabilidad. En 2014, el Selenium Hub no era famoso por su funcionamiento estable.
  3. Tolerancia a fallos. Necesitamos la capacidad de continuar el proceso de prueba en caso de falla del centro de datos o de cualquiera de los servidores.

Por lo tanto, apareció nuestra solución para escalar Selenium Grid, que constaba de un coordinador y gestores de nodos, aparentemente muy similar al Selenium Grid estándar, pero con sus propias características. Estas características se discutirán más a fondo.

Coordinador




De hecho, es un agente de recursos (los recursos se entienden como navegadores). Tiene una API externa a través de la cual las pruebas envían solicitudes de recursos. Estas consultas se guardan en la base de datos como tareas a ejecutar. El coordinador sabe todo acerca de la configuración de nuestro clúster: qué administradores de nodos existen, qué tipos de recursos pueden proporcionar estos administradores de nodos, la cantidad total de recursos, cuántos recursos están involucrados actualmente en las tareas. Al mismo tiempo, supervisa los recursos: actividad, estabilidad y, en cuyo caso, notifica a los responsables.

Una característica del coordinador es que integra a todos los administradores de nodos en las llamadas granjas.

Así es como se ve la granja. Se utiliza más de la mitad de los recursos y todos los nodos en línea:



También puede mostrar los nodos fuera de línea o ingresarlos en rotación en un cierto porcentaje, esto es necesario si es necesario reducir la carga en un nodo en particular.

Cada granja se puede combinar con otras en una unidad lógica, que llamamos servicio. Al mismo tiempo, se puede incluir una granja en varios servicios diferentes. En primer lugar, permite establecer límites y priorizar los recursos utilizados por cada servicio específico. En segundo lugar, le permite administrar fácilmente la configuración: tenemos la capacidad de agregar el número de administradores de nodos en el servicio sobre la marcha, o viceversa, eliminarlos de la granja para poder interactuar con estos administradores de nodos, por ejemplo, configurar o actualizar, etc. .



La API del coordinador es bastante simple: es posible solicitar al servicio la cantidad actual de recursos utilizados, obtener su límite e iniciar o detener algún recurso.

Administrador de nodos


Este es un servicio que puede hacer dos cosas bien: recibir tareas del coordinador y lanzar algunos recursos a pedido. De forma predeterminada, está diseñado para que cada lanzamiento del recurso esté aislado, es decir, ninguno de los lanzamientos anteriores puede afectar el lanzamiento de pruebas posteriores. Como respuesta, el coordinador usa un grupo de hosts y un conjunto de puertos elevados. Por ejemplo, el host en el que se lanzó el servidor Selenium y su puerto.



En el host, se ve así: el servicio Node Manager se está ejecutando y administra todo el ciclo de vida de los recursos. Levanta los navegadores, los completa, se asegura de que no se olviden de cerrar. Para garantizar el aislamiento mutuo, todo esto sucede en nombre del usuario del servicio.

Interacción


La prueba interactúa con la infraestructura descrita anteriormente de la siguiente manera: se dirige al coordinador con una solicitud de los recursos necesarios, el coordinador guarda esta tarea por requerir ejecución.

El administrador del nodo, a su vez, recurre al coordinador de tareas. Habiendo recibido la tarea, comienza el recurso. Después de eso, envía el resultado del lanzamiento al coordinador, los inicios fallidos también se informan al coordinador. La prueba recibe el resultado de la solicitud de recursos y, si tiene éxito, comienza a trabajar directamente con el recurso.



Las ventajas de este enfoque son reducir la carga sobre el coordinador al obtener la capacidad de trabajar directamente con el recurso. Contras: la necesidad de implementar la lógica de interacción con el coordinador dentro de los marcos de prueba, pero para nosotros esto es aceptable.
Hoy podemos ejecutar más de 800 navegadores en paralelo en tres centros de datos. Para el coordinador, este no es el límite.

La tolerancia a fallas se garantiza mediante el lanzamiento de varias instancias de coordinador que se encuentran detrás del firewall de DNS en diferentes centros de datos. Esto garantiza el acceso a la instancia de trabajo en caso de un problema con el centro de datos o el servidor.

Como resultado, obtuvimos una solución que cumplió con todos los requisitos establecidos inicialmente. Ha estado operando constantemente desde 2015 y ha demostrado su efectividad.

Androide


Cuando se trata de probar en Android, generalmente hay dos enfoques principales. El primero es usar WebDriver: así es como funcionan Selendroid y Appium. El segundo, al trabajar con herramientas nativas, implementó Robotium, UI Automator o Espresso.

Las similitudes fundamentales entre estos enfoques son obtener el dispositivo y obtener el navegador.

Hay muchas más diferencias, la principal es la necesidad de instalar el APK probado, con el que tomaremos artefactos en forma de registros, capturas de pantalla, etc. y también, el hecho de que las pruebas se llevan a cabo en el dispositivo en sí, y no en el CI.

En 2015, Odnoklassniki comenzó a cubrir su aplicación de Android con autotest. Seleccionamos una máquina Linux, conectamos un dispositivo real a través de USB y comenzamos a escribir pruebas en Robotium. Esta solución simple le permitió obtener resultados rápidamente.

Pasó el tiempo, aumentó la cantidad de pruebas y la cantidad de dispositivos. Para resolver las tareas de administración, se creó Device Manager, un contenedor sobre comandos adb (Android Debug Bridge), que permite que la interfaz http api los ejecute.

Así es como se veía la primera API para Device Manager: con su ayuda, podría obtener una lista de dispositivos, instalar / desinstalar APK, ejecutar pruebas y obtener resultados.



Sin embargo, notamos que los resultados de la prueba se degradan al inicio en el servidor ADB al que está conectado más de un dispositivo. La solución que nos ayudó a mejorar la estabilidad se encontró al aislar cada servidor ADB con Docker.

La granja está lista: puede conectar teléfonos.



Muchos están familiarizados con esta imagen. Escuché que si estás involucrado en granjas de Android, estás como en el infierno todos los días.



Un emulador de Android vino en nuestra ayuda. Su uso se debió a dos factores: en primer lugar, en ese momento ya había alcanzado el nivel de estabilidad necesario y, en segundo lugar, no teníamos ninguna característica que dependiera específicamente del hierro en nuestras pruebas. Además, este emulador se proyectó bien en la infraestructura existente en ese momento. El siguiente paso fue enseñarle al administrador del Nodo a lanzar nuevos tipos de recursos.

¿Qué se requiere para ejecutar el emulador de Android?

Primero, necesita un SDK de Android con un conjunto de utilidades.

Luego, debe crear AVD (dispositivo virtual Android): así es como se organizará su emulador de Android: qué arquitectura tendrá, cuántos núcleos usará, si los servicios de Google estarán disponibles, etc.



Después de eso, debe seleccionar el nombre del AVD creado, establecer los parámetros, por ejemplo, transferir el puerto en el que se iniciará ADB e iniciar.

Sin embargo, existe una peculiaridad en dicho esquema: el sistema le permite ejecutar solo un emulador de instancia en un AVD específico.

La solución a este problema fue crear un AVD básico, que se almacenó en la memoria, esto hizo posible copiarlo en otro lugar. Durante el lanzamiento del emulador de Android, el AVD base se copió a un directorio temporal asignado a la memoria, después de lo cual comenzó. Tal esquema funcionó rápidamente, pero fue engorroso. Hasta la fecha, este problema se ha resuelto con la opción de solo lectura, que le permite ejecutar emuladores de Android en cantidades ilimitadas desde un AVD

Actuación


En base a los resultados de trabajar con AVD, desarrollamos varias recomendaciones internas:

  1. 86 , ARM . dev/kvm Linux HAXM- Mac Windows
  2. GPU- . , . , , , Android-
  3. .
  4. , localhost,

En cuanto a las imágenes de Docker para probar en Android, quiero resaltar Agoda y Selenoid, utilizan al máximo las capacidades de los emuladores de Android.

La diferencia entre ellos es que en el Selenoid predeterminado tienen Appium , Agoda y el emulador "limpio". Además, Selenoid tiene más apoyo de la comunidad.

A finales de 2018, se creó CloudNode-Manager, se pone en contacto con el coordinador, recibe tareas y se inicia utilizando comandos en la nube. En lugar de máquinas de hierro, este servicio utiliza los recursos de una nube , la propia nube privada de Odnoklassniki.

Logramos escalar al enseñar a DeviceManager cómo trabajar con el Coordinador. Para hacer esto, tuve que cambiar la API del administrador de dispositivos para agregar la capacidad de solicitar un tipo de dispositivo (virtual / real).

Esto es lo que sucede si intenta ejecutar ADB Install en 250 emuladores desde una sola máquina.



Los asistentes reaccionaron inmediatamente a esto y comenzaron un incidente: la máquina cargó la interfaz de red gigabit con tráfico saliente. Esta complejidad se resolvió aumentando el rendimiento en el servidor. No puedo decir que este problema nos haya causado muchos problemas, pero no debes olvidarlo.

Parecería que el éxito es el Devicemanager, coordinador, escalamiento. Podemos ejecutar pruebas en toda la granja. En principio, podemos ejecutarlos en cada solicitud de extracción, y el desarrollador recibirá rápidamente comentarios.



Pero no todo es tan color de rosa. Es posible que haya notado que hasta ahora no se ha dicho nada sobre la calidad de las pruebas.



Así es como se veían nuestros lanzamientos. Y lo más interesante es que entre los lanzamientos podrían caerse pruebas completamente diferentes. Estas fueron caídas inestables. Y ni yo, ni los desarrolladores, ni los evaluadores confiamos en estos resultados.

¿Cómo lidiamos con este problema? Simplemente copiaron todo, desde Robotium a Espresso, y se volvió bueno ... En realidad, no.

Para resolver este problema, no solo reescribimos todo en Espresso, sino que también comenzamos a usar la API para todo tipo de acciones, como subir fotos, crear publicaciones, agregar a amigos, etc., iniciamos una sesión rápidamente, usamos diplinks que te permitían ir directamente a la pantalla deseada y, por supuesto, analizamos todos los casos de prueba.

Ahora las ejecuciones de prueba se ven así:



puede notar que las pruebas rojas permanecen, pero es importante recordar que estas son pruebas de punta a punta que se ejecutan en producción. Tenemos un límite en el número de pruebas que pueden caer en la rama principal de la aplicación.

Ahora tenemos pruebas estables y escalado. Sin embargo, la infraestructura de prueba todavía está muy vinculada a las pruebas. Al mismo tiempo, debido a la expectativa de pruebas de extremo a extremo, CI está ocupado y otros ensamblados pueden hacer cola, esperando agentes libres. Además, no existe un esquema claro para trabajar con inicios paralelos.

Las razones mencionadas anteriormente se convirtieron en el ímpetu para el desarrollo de QueueRunner, un servicio que le permite ejecutar pruebas de forma asincrónica sin bloquear el CI. Para trabajar, necesita un APK de prueba y prueba, así como un conjunto de pruebas. Una vez recibidos los datos necesarios, podrá organizar corridas en la cola, asignando y liberando los recursos necesarios. QueueRunner descarga los resultados de la ejecución a Jira y Stash, y también los envía por correo y en el messenger.

QueueRunner tiene un flujo de prueba: monitorea el ciclo de vida de la prueba. El flujo predeterminado que usamos ahora consta de cinco pasos:

  1. Dispositivo receptor En este punto, el Administrador de dispositivos solicita a través del coordinador un dispositivo real o virtual.
  2. . APK , – , .
  3. ,

Como resultado, cinco pasos simples son el ciclo de vida completo de la prueba en nuestro servicio.



¿Qué beneficios nos dio QueueRunner? En primer lugar, utiliza todos los recursos posibles al máximo: se puede escalar a toda la granja y obtener resultados rápidamente. En segundo lugar, con la bonificación, tuvimos la oportunidad de controlar la secuencia de pruebas. Por ejemplo, podemos ejecutar las pruebas más largas o problemáticas al principio y así reducir el tiempo que lleva esperar a que se ejecuten.

QueueRunner también le permite hacer retransmisiones inteligentes. Almacenamos todos los datos en la base de datos, por lo que en cualquier momento podemos ver el historial de la prueba. Por ejemplo, es posible observar la proporción de pases exitosos y no exitosos de la prueba y decidir si, en principio, vale la pena reiniciar la prueba.

QueueRunner y Devicemanager nos han dado la capacidad de adaptarnos a la cantidad de recursos. Ahora podemos escalar a toda la granja, gracias al uso de emuladores, es decir, un número casi ilimitado de dispositivos virtuales nos dio la oportunidad de ejecutar muchas más pruebas, pero si por alguna razón los recursos no están disponibles, el servicio esperará a que regresen y no habrá pérdida de lanzamientos. Usamos solo los recursos disponibles para nosotros, en consecuencia, después de algún tiempo los resultados aún se obtendrán y, al mismo tiempo, el CI no se bloqueará. Y lo más importante, la infraestructura de prueba y las pruebas ahora están separadas.
Ahora, para ejecutar pruebas en Android, solo necesita darnos un APK de prueba y una lista de pruebas.

Hemos recorrido un largo camino desde la granja Selenium en máquinas virtuales hasta el lanzamiento de pruebas de Android en la nube. Sin embargo, este camino aún no se ha completado.

Proceso de desarrollo


Veamos cómo se relaciona la infraestructura de prueba con el proceso de desarrollo y cómo lo ven los probadores y desarrolladores.

Nuestro equipo de Android utiliza el GitFlow estándar:



cada función tiene su propia rama. El desarrollo principal tiene lugar en la rama de desarrollo. Un desarrollador que decide crear una nueva súper característica comienza su desarrollo en su propia rama, mientras que otros desarrolladores pueden trabajar en otras ramas en paralelo. Cuando un desarrollador considera que el código idealmente bello y mejor del mundo está listo y necesita implementarse a los usuarios lo más rápido posible, hace una solicitud de extracción en el desarrollo, la unidad se ensambla automáticamente, se ejecutan pruebas unitarias y pruebas de componentes. Simultáneamente, los APK se ensamblan, se envían a QueueRunner y se ejecutan pruebas de extremo a extremo. Después de eso, los resultados de las pruebas en ejecución llegan al desarrollador.

Sin embargo, existe una alta probabilidad de que después de la creación de la rama de características en desarrollo, haya muchas confirmaciones. Esto significa que el desarrollo puede no ser lo que solía ser. Por lo tanto, la fusión previa ocurre primero: fusionamos el desarrollo en la rama de características actual, y es en este estado prematuro que construimos, pruebas unitarias, pruebas de componentes, de extremo a extremo, y en base a estos resultados, hacemos un informe. Por lo tanto, entendemos cuán funcional es la característica en la versión actual de desarrollo y, si todo está bien, se envía a los usuarios.



Informes


Así es como se ven los informes de Stash:



nuestro bot primero escribe que las pruebas han comenzado, y cuando pasan, actualiza el mensaje y agrega cuántos han pasado, cuántos han caído, cuántos errores conocidos y cuántas pruebas escamosas. Escribe lo mismo en Jira y agrega un enlace a una comparación de lanzamientos.

Así es como se ve la comparación de los dos lanzamientos:



aquí se compara la ejecución actual en la rama de características con la última ejecución en el desarrollo. Contiene información sobre el número de pruebas ejecutadas, problemas de coincidencia, pruebas descartadas y pruebas inestables de Flaky que estaban en un estado y cambiaron a otro.

Si cae al menos una prueba unitaria o más de un umbral de pruebas de extremo a extremo, se bloqueará la fusión.

Para entender si las pruebas caen de manera estable, comparamos los hash de los rastros de las caídas, antes de que se eliminen preliminarmente los dígitos, solo quedan los números de línea. Si los hashes coinciden, entonces esta es la misma caída, si son diferentes, entonces lo más probable es que las caídas sean diferentes.

Resumen


Como resultado, implementamos una solución estable y tolerante a fallas que se adapta bien a nuestra infraestructura. Luego, la infraestructura resultante se adaptó para las pruebas de Android. Nos ayudó en esto el Administrador de dispositivos, que nos ayuda a trabajar tanto con dispositivos reales como virtuales, así como con QueueRunner, que nos ayudó a separar la infraestructura y las pruebas, y no bloquear CI durante la duración de las pruebas.

Parecía el tiempo de prueba de una semana en 2016, de cincuenta minutos o más.



Así es como se ve ahora:



este gráfico muestra las ejecuciones que tuvieron lugar durante 2 horas de un día hábil promedio. El tiempo de ejecución se redujo a un máximo de 15 minutos, el número de carreras aumentó notablemente.

All Articles