Errores comunes al escribir pruebas unitarias. Conferencia de Yandex

Si domina una pequeña lista de errores típicos que ocurren al escribir pruebas unitarias, incluso puede amar escribirlos. Hoy, el jefe del grupo de desarrollo Yandex.Browser para Android Konstantin kzaikin Zaikin compartirá su experiencia con los lectores de Habr.


- Tengo un informe práctico. Espero que los beneficie a todos ustedes: aquellos que ya están escribiendo pruebas unitarias, y aquellos que solo están pensando en escribir, y aquellos que lo están intentando y que no han tenido éxito.

Tenemos un proyecto bastante grande. Uno de los mayores proyectos móviles en Rusia. Tenemos mucho código, muchas pruebas. Las pruebas se persiguen en cada solicitud de grupo, no caen al mismo tiempo.

¿Quién sabe qué cobertura de prueba tiene en el proyecto? Cero, está bien. ¿Quién tiene pruebas unitarias en el proyecto? ¿Y quién cree que no se necesitan pruebas unitarias? No veo nada malo en eso, hay personas que están sinceramente convencidas de esto, y mi historia debería ayudarles a convencerse de esto.

Afortunadamente, miles de pruebas ecológicas: no vinimos de inmediato. No hay bala de plata, y la idea principal de mi informe en la pantalla:



El dicho chino está escrito en jeroglíficos que dice que un viaje de aproximadamente mil comienza con un paso. Parece que hay un análogo de este dicho.

Hace mucho tiempo, tomamos la decisión de que necesitamos mejorar nuestro producto, nuestro código, y estamos avanzando hacia este propósito. En este camino nos encontramos con muchos golpes, un rastrillo submarino, y reunimos junto con esto algunas creencias.



¿Por qué necesitamos pruebas?

Para que las características antiguas no caigan cuando presentamos otras nuevas. Tener una insignia en GitHub. Para refactorizar las características existentes, un pensamiento profundo, debe revelarse a aquellos que no escriben pruebas. Para que las funciones existentes no caigan durante la refactorización, nos protegeremos con pruebas. Para que el jefe envíe una solicitud de grupo, sí.

Mi opinión, por favor no la asocie con la opinión de mi equipo, de que las pruebas nos ayudan. Le permiten ejecutar su código sin ponerlo en producción, sin instalarlo en dispositivos, lo inicia y lo ejecuta muy rápidamente. Puede huir de todos los casos de esquina que no obtiene en la vida en el dispositivo y en la producción, y su probador no los encontrará. Pero usted, como desarrollador, los inventará, verificará y corregirá errores en una etapa temprana.

Muy importante: las pruebas indican cómo, según el desarrollador, el código debería funcionar y qué, según el desarrollador, deberían hacer sus métodos. Estos no son comentarios que se alejan y después de un tiempo los útiles se vuelven dañinos. Sucede que en los comentarios se escribe una cosa, y en el código es completamente diferente. Las pruebas unitarias en este sentido no pueden mentir. Si la prueba es verde, documenta lo que está sucediendo allí. La prueba falló: usted violó la intención principal del desarrollador.

Cometer contratos. Estos no son contratos firmados y sellados, sino contratos de software para el comportamiento de clase. Si refactoriza, en este caso los contratos serán violados y las pruebas caerán si los rompe. Si se guardan los contratos, las pruebas permanecerán verdes, tendrá más confianza en que su refactorización es correcta.



Esta es la idea general de todo mi informe. Puedes mostrar la primera línea y salir.

Mucha gente piensa que el código de prueba es regular, no es para producción, por lo que puede escribirlo de manera regular. Estoy totalmente en desacuerdo con esto y creo que las pruebas deben abordarse en primer lugar de manera responsable, así como el código de producción. Si los aborda de la misma manera, las pruebas lo beneficiarán. De lo contrario, será una mancha.

Más específicamente, las dos líneas a continuación se refieren a cualquier código, al parecer.

BESO: mantenlo simple, estúpido. No hay necesidad de complicarse. Las pruebas deben ser simples. Y el código de producción debe ser simple, pero las pruebas son especialmente. Si tiene exámenes que son fáciles de leer, entonces estos serán exámenes que probablemente estén bien escritos, estén bien expresados ​​y sean fáciles de evaluar. Incluso durante la solicitud de grupo, una persona que mira sus nuevas pruebas comprenderá lo que quería decir. Y si algo se rompe, puede comprender fácilmente lo que sucedió.

SECO - no te repitas. En las pruebas, el desarrollador a menudo se inclina a usar la técnica prohibida que nadie parece usar en la producción: copiar y pegar. En la producción de un desarrollador que copiará y pegará activamente, simplemente no lo entenderán. En las pruebas, esta es una práctica normal, desafortunadamente. No es necesario hacer esto, porque - la primera línea. Si escribe las pruebas honestamente, como un código realmente bueno, las pruebas le serán útiles.

Mientras estábamos desarrollando nuestros cientos de miles de líneas de código, escribiendo miles de pruebas, recolectando rastrillos, había acumulado comentarios típicos sobre las pruebas. Soy bastante vago, y cuando fui a las solicitudes del grupo y observé los mismos errores, basado en el principio DRY, decidí anotar estos problemas típicos, y lo hice primero en el Wiki interno y luego publiqué olores de pruebas prácticas en GitHub que puedes seguir cuando escribes pruebas.



Voy a enumerar por puntos. Incremente un contador en su mente si recuerda ese olor a prueba. Si cuentas hasta cinco, puedes levantar la mano y gritar "¡Bingo!" Y al final, me pregunto quién contó cuánto. Mi contador será igual al número de puntos, los recogí todos yo mismo.


Enlace GitHub

Lo más difícil en la programación que sabes. Y en las pruebas esto es realmente importante. Si no nombra bien la prueba, lo más probable es que no pueda formular lo que verifica la prueba.

Los humanos son criaturas bastante simples, son fácilmente atrapados en los nombres. Por lo tanto, le pido que llame a las pruebas bien. Formule una prueba para verificar y seguir reglas simples.

no_action_or_assertion


Si el nombre de la prueba no contiene una descripción de lo que verifica la prueba, por ejemplo, si tiene la clase Controller y escribe la prueba testController, ¿qué verifica? ¿Qué debe hacer esta prueba? Lo más probable, ya sea nada o demasiadas cosas para verificar. Ni uno ni el otro nos conviene. Por lo tanto, en nombre de la prueba, debe escribir lo que verificamos.

nombre_largo


No puedes ir al otro extremo. El nombre de la prueba debe ser lo suficientemente corto como para que una persona pueda analizarlo fácilmente. En este sentido, Kotlin es excelente porque le permite escribir nombres de prueba entre comillas con espacios en inglés normal. Son más fáciles de leer. Pero aún así, los nombres largos son olor.

Si el nombre de su prueba es demasiado largo, lo más probable es que ponga demasiados métodos de prueba en una clase de prueba, y necesita aclarar lo que está verificando. En este caso, debe dividir su clase de prueba en varias. No hay que tener miedo de esto. Tendrá un nombre de clase de prueba que verifica el nombre de su código de producción, y habrá nombres cortos de prueba.

prefijo_antiguo


Esto es atavismo. Anteriormente, en Java, todos probaban con JUnit, donde hasta la cuarta versión había un acuerdo de que los métodos de prueba deberían comenzar con la palabra prueba. Sucedió que todos todavía lo llaman así. Pero hay un problema, en inglés la palabra test es el verbo "check". Las personas quedan atrapadas fácilmente en esta trampa y ya no escriben ningún otro verbo. Escribe testController. Es fácil comprobarlo usted mismo: si no escribió un verbo sobre lo que debe hacer su clase de prueba, lo más probable es que no haya verificado algo, no lo ha escrito lo suficientemente bien. Por lo tanto, siempre le pido que elimine la palabra prueba de los nombres de los métodos de prueba.

Les digo cosas muy simples, pero curiosamente, ayudan. Si las pruebas se llaman bien, lo más probable es que debajo del capó se vean bien. Es muy simple



De hecho, leí los ID de olores de prueba como en GitHub. El enlace está debajo, puedes caminar y usar.

multiple_asserts


En el método de prueba hay muchas afirmaciones. ¿Entonces tal vez o no? Tal vez ¿Es bueno o malo? Creo que esto es muy malo. Si escribió varias afirmaciones en un método de prueba, entonces verifica varias declaraciones. Si prueba su prueba y cae la primera afirmación, ¿llegará la prueba a la segunda afirmación? No alcanzará. Usted ya después de la caída de su ensamblaje en algún lugar del CI obtiene que la prueba cayó, vaya a arreglar algo, llénelo nuevamente, caerá en la siguiente afirmación. Muy bien podría ser.

En este caso, sería mucho más genial si dividiera este método de prueba en varios, y todos los métodos con varias afirmaciones cayeran al mismo tiempo, porque se lanzarían independientemente uno del otro.

Algunas afirmaciones más pueden enmascarar las diferentes acciones que se realizan con la clase de prueba. Recomiendo escribir una prueba, una afirmación. Al mismo tiempo, las afirmaciones pueden ser bastante complejas. Mi colega, en el primer informe, demostró una pieza de código en la que utilizó la excelente afirmación de que la construcción y la coincidencia. Realmente me encantan los enfrentamientos en JUnit, así que puedes usar eso también. Para el lector de prueba, resulta ser solo una breve declaración. GitHub tiene ejemplos de todos estos olores y cómo solucionarlos. Hay un ejemplo de código incorrecto y algún código bueno. Todo esto se realiza en forma de un proyecto que puede descargar, abrir, compilar y ejecutar todas las pruebas.

many_tests_in_one


El siguiente olor está estrechamente relacionado con el anterior. Haces algo con el sistema: haces una afirmación. Hacer otra cosa con el sistema, algunas operaciones largas, hacer una afirmación, hacer otra cosa. De hecho, simplemente vio varios métodos, y obtiene métodos de prueba sólidos y buenos.

repeating_setup


Esto se refiere a la verbosidad. Si tiene una clase de prueba, y cada método de prueba ejecuta los mismos métodos al principio.

Una clase de prueba en la que se ejecutan los mismos métodos al principio. Esto parece un poco, pero en todos los métodos de prueba esta basura está presente. Y si es común a todos los métodos de prueba, entonces, ¿por qué no arrastrarlo al constructor o al bloque Before o Before Each block en JUnit 5. Si hace esto, la legibilidad de cada método mejorará y además se librará de DRY sin. Tales pruebas son más fáciles de mantener y más fáciles de leer.



La fiabilidad de las pruebas es muy importante. Hay signos por los cuales se puede determinar que la prueba llorará, será verde o roja. Cuando el desarrollador lo escribe, está seguro de que es verde y, por alguna razón, las pruebas se vuelven verdes o rojas, lo que nos da dolor e incertidumbre en general de que las pruebas son útiles. No estamos seguros de las pruebas, lo que significa que no estamos seguros de que sean útiles.

al azar


Yo mismo escribí una vez pruebas que tenían Math.random () dentro, hice números aleatorios, hice algo con ellos. No hay necesidad de hacer esto. Esperamos que el sistema de prueba ingrese al sistema de prueba en la misma configuración, y la salida del mismo también debe ser la misma. Por lo tanto, en pruebas unitarias, por ejemplo, nunca necesita realizar ninguna operación con la red. Debido a que el servidor puede no responder, puede haber diferentes tiempos, algo más.

Si necesita una prueba que funcione con la red, haga un proxy, local, cualquier cosa, pero en ningún caso vaya a una red real. Este es el mismo azar. Y, por supuesto, no puede usar datos aleatorios. Si tiene que hacer algo, haga algunos ejemplos con condiciones límite, con malas condiciones, pero deben estar codificadas.

caminar_sueño


Un problema clásico que enfrentan los desarrolladores cuando intentan probar algún tipo de código asincrónico. Es que hice algo en la prueba, y luego tengo que esperar hasta que se complete. Cómo hacer? Thread.sleep (), por supuesto.

Hay un problema Cuando desarrolló su prueba, por ejemplo, lo hizo en alguna de su máquina de escribir, funciona a cierta velocidad. Ejecutas las pruebas en otra máquina. ¿Y qué sucederá si su sistema no logra funcionar durante el tiempo Thread.sleep ()? La prueba se pone roja. Esto es inesperado Por lo tanto, la recomendación aquí es, si está realizando operaciones asincrónicas, no las pruebe en absoluto. Casi cualquier operación asincrónica se puede implementar para que tenga algún tipo de mecanismo condicional que proporcione una operación asincrónica y un bloque de código ejecutado sincrónicamente. Por ejemplo, AsyncTask tiene un bloque de código ejecutado sincrónicamente. Puede probarlo sincrónicamente fácilmente, sin asincronismo. No hay necesidad de probar AsyncTask en sí, es una clase de marco, ¿por qué probarlo? Ponlo entre corchetes y tu vida será más fácil.

Thread.sleep () es mucho dolor. Además del hecho de que empeora la confiabilidad de las pruebas, ya que les permite llorar debido a diferentes tiempos en los dispositivos, también ralentiza la ejecución de sus pruebas. ¿A quién le gustaría que sus pruebas unitarias, que deberían ejecutarse en milisegundos, se ejecuten durante cinco segundos, porque configuré el modo de suspensión?

modificar_global


Es típico que hayamos cambiado alguna variable estática global al comienzo de la prueba para verificar que nuestro sistema funciona correctamente, pero no regresó al final. Luego tenemos una situación interesante: en la máquina, el desarrollador ejecutó las pruebas en una secuencia, primero verificó la variable global con el valor predeterminado, luego la cambió en otra prueba y luego hizo otra cosa. Ambas pruebas son verdes. Y en CI, sucedió que las pruebas comenzaron en el orden inverso. Y una o ambas pruebas serán rojas, aunque todas fueron verdes.

Necesitas limpiar después de ti mismo. Scout gobierna en este sentido: cambió la variable global - regrese al estado original. Mejor aún, asegúrese de que no se usen estados globales. Pero este es un pensamiento más profundo. Se trata del hecho de que las pruebas a veces resaltan defectos en la arquitectura. Si tenemos que cambiar los estados globales y devolverlos a su estado original para escribir pruebas, ¿nos está yendo bien en nuestra arquitectura? ¿Realmente necesitamos variables globales, por ejemplo? Como regla general, puede prescindir de ellos inyectando algunas clases de contextos o algo así, para que pueda reinicializarlos, inyectarlos y reinicializarlos en la prueba cada vez.

@VisibleForTesting


Prueba de olor para avanzado. La necesidad de usar tal cosa no surge el primer día, como regla. Ya probaste algo y luego necesitabas traducir la clase a un estado específico. Y te haces una puerta trasera. Tienes una clase de producción, y haces un método específico que nunca se llamará en producción, y a través de él inyectas algo en la clase o cambias su estado. Por lo tanto, maliciosamente rompiendo la encapsulación. En producción, su clase funciona de alguna manera, pero en las pruebas, de hecho, es una clase diferente, se comunica con ella a través de otras entradas y salidas. Y aquí puede obtener una situación en la que cambia la producción, pero las pruebas no lo notan. Las pruebas continúan pasando por la puerta trasera y no notaron que, por ejemplo, las excepciones comenzaron a dispararse en el constructor, ya que pasan por otro constructor.

En general, debe probar sus clases a través de las mismas entradas y salidas que en la producción. No debe haber acceso a ningún método solo para pruebas.



¿Cuántas de nuestras 15 mil pruebas se realizan? Aproximadamente 20 minutos, en cada solicitud de grupo, en Team City, los desarrolladores se ven obligados a esperar. Solo porque 15 mil son muchas pruebas. Y en esta sección, he compilado olores que ralentizan las pruebas. Aunque thread_sleep ya estaba allí.

prueba_android_necesaria


Android tiene pruebas de instrumentación, son hermosas, se ejecutan en un dispositivo o emulador. Esto elevará su proyecto por completo, de verdad, pero son muy lentos. Y para ellos necesitas incluso levantar un emulador completo. Incluso si imagina que tiene un emulador elevado en CI, solo coincide que tiene uno, entonces ejecutar la prueba en el emulador tomará mucho más tiempo que en la máquina host, por ejemplo, usando Robolectric. Aunque hay otros métodos. Este es un marco de trabajo que le permite trabajar con clases desde el marco de Android en la máquina host, en Java puro. Lo usamos bastante activamente. Anteriormente, Google era algo genial al respecto, pero ahora los propios googlers hablan de ello en varios informes, se recomienda su uso.

innecesario_roboeléctrico


El marco de Android de Robolectric está emulado. No está completo allí, aunque la implementación cuanto más lejos, más completa. Es casi un Android real, solo se ejecuta en su computadora de escritorio, computadora portátil o CI. Pero tampoco necesita ser usado en todas partes. Robolectric no es gratis. Si tiene una prueba que transfirió heroicamente de instrumentación de Android a Robolectric, debería pensar: ¿tal vez ir más allá, deshacerse de Robolectric, convertirlo en la prueba JUnit más simple? Las pruebas de Robolectric tardan en inicializarse, intentar cargar recursos, inicializar su actividad, aplicación y todo lo demás. Toma algo de tiempo Esto no es un segundo, son milisegundos, a veces decenas y cientos. Pero cuando hay muchas pruebas, incluso eso importa.

Existen técnicas que eliminan Robolectric. Puede aislar su código a través de interfaces envolviendo toda la parte de la plataforma con interfaces. Entonces solo habrá una prueba de host JUnit. JUnit en la máquina host es muy rápido, hay una cantidad mínima de sobrecarga, tales pruebas se pueden ejecutar en miles y decenas de miles, se ejecutarán un minuto, unos minutos. Desafortunadamente, nuestras pruebas tardan mucho tiempo en completarse porque tenemos muchas pruebas de instrumentación de Android, porque tenemos una parte nativa en el navegador y nos vemos obligados a ejecutarlas en un emulador o dispositivo real. ¿Por qué tanto tiempo?

No te aburriré más. ¿Cuántos olores tienes? Hasta ahora, siete como máximo. Suscríbete al canal , pon las estrellas.

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


All Articles