En el camino al 100% de cobertura de código con pruebas en Go usando sql-dumper como ejemplo

imagen


En esta publicación, hablaré sobre cómo escribí un programa de consola en lenguaje Go para cargar datos de una base de datos a archivos, tratando de cubrir todo el código con 100% de pruebas. Comenzaré con una descripción de por qué necesitaba este programa. Continuaré describiendo las primeras dificultades, algunas de las cuales son causadas por las características del lenguaje Go. A continuación, mencionaré una pequeña compilación en Travis CI, y luego hablaré sobre cómo escribí las pruebas, tratando de cubrir el código al 100%. Tocaré un poco para probar el trabajo con la base de datos y el sistema de archivos. En conclusión, diré a qué conduce el deseo de cubrir el código con pruebas y qué dice este indicador. Proporcionaré material con enlaces a documentación y ejemplos de confirmaciones de mi proyecto.


Propósito del programa


El programa debe iniciarse desde la línea de comandos con una indicación de la lista de tablas y algunas de sus columnas, un rango de datos para la primera columna especificada, una enumeración de las relaciones de las tablas seleccionadas entre sí, con la capacidad de especificar un archivo con la configuración de conexión de la base de datos. El resultado del trabajo debe ser un archivo que describa las solicitudes para crear las tablas especificadas con las columnas especificadas y las expresiones de inserción de los datos seleccionados. Se supuso que el uso de dicho programa simplificaría el escenario de extraer una porción de datos de una gran base de datos y desplegar esta porción localmente. Además, se suponía que estos archivos sql de descarga debían ser procesados ​​por otro programa, que reemplaza parte de los datos de acuerdo con una plantilla específica.


Se puede lograr el mismo resultado utilizando cualquiera de los clientes populares en la base de datos y una cantidad suficientemente grande de trabajo manual. Se suponía que la aplicación simplificaría este proceso y automatizaría tanto como fuera posible.


Este programa debería haber sido desarrollado por mis pasantes con el propósito de capacitación y uso posterior en su capacitación adicional. Pero la situación resultó tal que rechazaron esta idea. Pero decidí intentar escribir dicho programa en mi tiempo libre con el propósito de desarrollar mi práctica en el lenguaje Go.


La solución está incompleta, tiene una serie de limitaciones que se describen en README. En cualquier caso, este no es un proyecto de combate.


Ejemplos de uso y código fuente .


Primeras dificultades


La lista de tablas y sus columnas se pasa al programa como un argumento en forma de cadena, es decir, no se conoce de antemano. La mayoría de los ejemplos de trabajo con una base de datos en Go implican que la estructura de la base de datos se conoce de antemano, simplemente creamos una struct indica los tipos de cada columna. Pero en este caso no funciona de esa manera.


La solución a esto fue utilizar el método MapScan de github.com/jmoiron/sqlx , que creó un segmento de interfaz en el tamaño igual al número de columnas de muestra. La siguiente pregunta era cómo obtener un tipo de datos real de estas interfaces. La solución es un caso de cambio por tipo . Tal solución no se ve muy hermosa, porque todos los tipos necesitarán ser convertidos en la cadena: enteros como son, cadenas para escapar y encerrar entre comillas, pero al mismo tiempo para describir todos los tipos que pueden provenir de la base de datos. No encontré una forma más elegante de resolver este problema.


Con los tipos, también se manifestó una función de lenguaje Go: una variable de tipo string no puede tomar el valor nil , pero tanto una cadena vacía como NULL pueden provenir de la base de datos. Para resolver este problema, hay una solución en el paquete de la database/sql : use una strut especial, que almacena el valor y el signo, ya sea NULL o no.


Ensamblaje y cálculo del porcentaje de cobertura de código por pruebas


Para el ensamblaje, uso Travis CI, para obtener el porcentaje de cobertura de código con pruebas: Overoles. El archivo .travis.yml para el ensamblado es bastante simple:


 language: go go: - 1.9 script: - go get -t -v ./... - go get golang.org/x/tools/cmd/cover - go get github.com/mattn/goveralls - go test -v -covermode=count -coverprofile=coverage.out ./... - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN 

En la configuración de Travis CI, solo necesita especificar la variable de entorno COVERALLS_TOKEN , COVERALLS_TOKEN valor debe tomarse en el sitio .


Overol le permite averiguar convenientemente qué porcentaje del proyecto completo, para cada archivo, resalta una línea de código fuente que resultó ser una prueba descubierta. Por ejemplo, en la primera compilación está claro que no escribí pruebas para algunos casos de errores al analizar una solicitud de usuario.


La cobertura del 100% del código significa que se escriben pruebas que, entre otras cosas, ejecutan código para cada rama en if . Este es el trabajo más voluminoso al escribir pruebas y, en general, al desarrollar una aplicación.


Puede calcular la cobertura con pruebas localmente, por ejemplo, con la misma go test -v -covermode=count -coverprofile=coverage.out ./... , pero puede hacerlo mejor en CI, puede colocar una placa en Github.


Dado que estamos hablando de dados, entonces encuentro útiles los dados de https://goreportcard.com , que analiza los siguientes indicadores:


  • gofmt: formato de código, incluida la simplificación de construcciones
  • go_vet - comprueba construcciones sospechosas
  • gocyclo - muestra problemas en la complejidad ciclomática
  • golint - para mí está verificando la disponibilidad de todos los comentarios necesarios
  • licencia: el proyecto debe tener una licencia
  • ineffassign - verifica asignaciones ineficaces
  • error ortográfico: verifica los errores tipográficos

Dificultades para cubrir el código con pruebas 100%


Si analizar una solicitud de componentes de un usuario pequeño funciona principalmente con la conversión de cadenas a algunas estructuras a partir de cadenas y se cubre con bastante facilidad mediante pruebas, entonces para probar el código que funciona con una base de datos, la solución no es tan obvia.


Alternativamente, conéctese a un servidor de base de datos real, rellene previamente con datos en cada prueba, realice selecciones y borre. Pero esta es una solución difícil, lejos de las pruebas unitarias e impone sus requisitos en el entorno, incluido el servidor CI.


Otra opción podría ser utilizar una base de datos en la memoria, por ejemplo, sqlite ( sqlx.Open("sqlite3", ":memory:") ), pero esto implica que el código debe estar tan débilmente ligado al motor de la base de datos como sea posible, lo que complica enormemente el proyecto. pero para la prueba de integración es bastante buena.


Para pruebas unitarias, es adecuado usar simulacro para la base de datos. Encontré este . Con este paquete, puede probar el comportamiento tanto en el caso de un resultado normal como en el caso de errores, indicando qué solicitud debe devolver qué error.


Las pruebas de escritura mostraron que la función que se conecta a la base de datos real debe moverse a main.go, por lo que puede redefinirse en las pruebas para la que devolverá la instancia simulada.


Además de trabajar con la base de datos, es necesario hacer que el trabajo con el sistema de archivos sea una dependencia separada. Esto permitirá reemplazar la grabación de archivos reales con escritura en la memoria para facilitar las pruebas y reducir el acoplamiento. Así es como apareció la interfaz FileWriter , y con ella la interfaz del archivo que devuelve. Para probar los escenarios de error, se crearon implementaciones auxiliares de estas interfaces y se colocaron en el archivo filewriter_test.go , por lo que no entran en la compilación general, pero se pueden usar en las pruebas.


Después de un tiempo, tuve una pregunta sobre cómo cubrir main() pruebas. En ese momento, tenía suficiente código allí. Como mostraron los resultados de búsqueda, esto no se hace en Go . En cambio, todo el código que se puede extraer de main() debe extraerse. En mi código, dejé solo opciones de análisis y argumentos de línea de comando (paquete de marca), conectándome a la base de datos, creando instancias de un objeto que escribirá archivos y llamando a un método que hará el resto del trabajo. Pero estas líneas no le permiten obtener exactamente el 100% de cobertura.


Al probar Go, existe algo así como " Funciones de ejemplo ". Estas son funciones de prueba que comparan la salida con lo que se describe en el comentario dentro de dicha función. Se pueden encontrar ejemplos de tales pruebas en el código fuente de los paquetes go . Si dichos archivos no contienen pruebas y puntos de referencia, se nombran con el prefijo example_ y terminan con _test.go . El nombre de cada función de prueba debe comenzar con Example . Sobre esto, escribí una prueba para un objeto que escribe sql en un archivo, reemplazando el registro real en el archivo con un simulacro, desde el cual puede obtener el contenido y la salida. Esta conclusión se compara con el estándar. Convenientemente, no necesita escribir una comparación con sus manos, y es conveniente escribir algunas líneas en los comentarios. Pero cuando se trataba de probar un objeto que escribe datos en un archivo csv, surgieron dificultades. De acuerdo con RFC4180, las líneas en CSV deben estar separadas por CRLF, e go fmt reemplaza todas las líneas con LF, lo que lleva al hecho de que el estándar del comentario no coincide con la salida real debido a los diferentes separadores de línea. Tuve que escribir una prueba regular para este objeto, al mismo tiempo que cambiaba el nombre del archivo eliminando example_ de él.


La pregunta sigue siendo, si el archivo, por ejemplo, query.go prueba usando tanto ejemplos como pruebas convencionales, ¿debería haber dos archivos example_query_test.go y query_test.go ? Aquí, por ejemplo, solo hay un example_test.go . Usar la búsqueda de "ir ejemplo de prueba" sigue siendo divertido.


Aprendí a escribir pruebas en Go de acuerdo con las guías que Google da para "ir a escribir pruebas". La mayoría de los que encontré ( 1 , 2 , 3 , 4 ) sugieren comparar el resultado con el diseño esperado del formulario


 if v != 1.5 { t.Error("Expected 1.5, got ", v) } 

Pero cuando se trata de comparar tipos, una construcción familiar evoluciona evolutivamente en un montón de uso de "reflexión" o afirmación de tipo. O otro ejemplo, cuando necesita verificar que el segmento o mapa tiene el valor necesario. El código se vuelve engorroso. Entonces quiero escribir mis funciones auxiliares para la prueba. Aunque una buena solución aquí es usar una biblioteca para realizar pruebas. Encontré https://github.com/stretchr/testify . Le permite hacer comparaciones en una sola línea . Esta solución reduce la cantidad de código y simplifica la lectura y el soporte de las pruebas.


Código de fragmentación y pruebas


Escribir una prueba para una función de alto nivel que funciona con varios objetos le permite aumentar significativamente el valor de la cobertura de código mediante pruebas a la vez, porque durante esta prueba se ejecutan muchas líneas de código de objetos individuales. Si se establece el objetivo de solo el 100% de cobertura, la motivación para escribir pruebas unitarias en componentes pequeños del sistema desaparece, ya que esto no afecta el valor de la cobertura del código.


Además, si no verifica el resultado en la función de prueba, esto tampoco afectará el valor de la cobertura del código. Puede obtener un alto valor de cobertura, pero no puede detectar errores graves en la aplicación.


Por otro lado, si tiene un código con muchas ramas , después de lo cual se llama una función voluminosa, será difícil cubrirlo con pruebas. Y aquí tiene un incentivo para mejorar este código, por ejemplo, para tomar todas las ramas en una función separada y escribir una prueba separada en ella. Esto afectará positivamente la legibilidad del código.


Si el código tiene un fuerte acoplamiento, lo más probable es que no pueda escribir una prueba en él, lo que significa que tendrá que hacer cambios en él, lo que afectará positivamente la calidad del código.


Conclusión


Antes de este proyecto, no tenía que establecer una meta de cobertura del 100% del código con las pruebas. Pude obtener una aplicación que funcionara en 10 horas de desarrollo, pero me llevó entre 20 y 30 horas alcanzar el 95% de cobertura. Usando un pequeño ejemplo, tuve una idea de cómo el valor de la cobertura del código afecta su calidad y cuánto esfuerzo se necesita para mantenerlo.


Mi conclusión es que si ve un tablero con un valor de cobertura de código alto para alguien, no dice casi nada sobre qué tan bien se ha probado esta aplicación. De todos modos, debes ver las pruebas ellos mismos. Pero si usted mismo ha establecido un curso para un 100% honesto, esto le ayudará a escribir mejor una solicitud.


Puede leer más sobre esto en los siguientes materiales y comentarios sobre ellos:



Spoiler

La palabra "recubrimiento" se usa aproximadamente 20 veces. Lo siento

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


All Articles