5 técnicas avanzadas de prueba de Go

¡Saludos a todos! Queda menos de una semana antes del inicio del curso "Desarrollador de Golang" y seguimos compartiendo material útil sobre el tema. Vamos!



Go tiene una biblioteca incorporada buena y confiable para realizar pruebas. Si escribe en Go, ya lo sabe. En este artículo, hablaremos sobre varias estrategias que pueden mejorar sus habilidades de prueba con Go. Por la experiencia de escribir nuestra impresionante base de código en Go, aprendimos que estas estrategias realmente funcionan y, por lo tanto, ayudan a ahorrar tiempo y esfuerzo al trabajar con el código.

Usar suites de prueba

Si aprende por sí mismo solo una cosa útil de este artículo, debe ser el uso de conjuntos de pruebas. Para aquellos que no están familiarizados con este concepto, la prueba por kits es el proceso de desarrollar una prueba para probar una interfaz común que se puede usar en muchas implementaciones de esta interfaz. A continuación puede ver cómo pasamos varias implementaciones diferentes de Thinger y las ejecutamos con las mismas pruebas.

 type Thinger interface { DoThing(input string) (Result, error) } // Suite tests all the functionality that Thingers should implement func Suite(t *testing.T, impl Thinger) { res, _ := impl.DoThing("thing") if res != expected { t.Fail("unexpected result") } } // TestOne tests the first implementation of Thinger func TestOne(t *testing.T) { one := one.NewOne() Suite(t, one) } // TestOne tests another implementation of Thinger func TestTwo(t *testing.T) { two := two.NewTwo() Suite(t, two) } 

Los lectores afortunados han trabajado con bases de código que usan este método. A menudo se utilizan en sistemas basados ​​en complementos, las pruebas escritas para probar una interfaz pueden ser utilizadas por todas las implementaciones de esta interfaz para comprender cómo cumplen los requisitos de comportamiento.

El uso de este enfoque potencialmente ayudará a ahorrar horas, días e incluso tiempo suficiente para resolver el problema de la igualdad de las clases P y NP . Además, al reemplazar un sistema base por otro, desaparece la necesidad de escribir (un gran número) de pruebas adicionales, y también existe la confianza de que este enfoque no interrumpirá el funcionamiento de su aplicación. Implícitamente, debe crear una interfaz que defina el área del área probada. Con la inyección de dependencia, puede personalizar un conjunto de un paquete que se pasa a la implementación de todo el paquete.

Puedes encontrar un ejemplo completo aquí . A pesar de que este ejemplo es descabellado, uno puede imaginar que una base de datos es remota y la otra está en la memoria.

Otro buen ejemplo de esta técnica se encuentra en la biblioteca estándar en el paquete golang.org/x/net/nettest . Proporciona un medio para verificar que net.Conn está satisfaciendo la interfaz.

Evitar la contaminación de la interfaz

No puede hablar sobre pruebas en Go, pero no hable sobre interfaces.

Las interfaces son importantes en el contexto de las pruebas, ya que son la herramienta más poderosa en nuestro arsenal de pruebas, por lo que debe usarlas correctamente.

Los paquetes a menudo exportan interfaces a los desarrolladores, y esto lleva al hecho de que:

A) Los desarrolladores crean su propio simulacro para implementar el paquete;
B) El paquete exporta su propio simulacro.

"Cuanto más grande es la interfaz, más débil es la abstracción"
- Rob Pike, refranes de Go

Las interfaces deben verificarse cuidadosamente antes de la exportación. A menudo es tentador exportar interfaces para dar a los usuarios la capacidad de simular el comportamiento que necesitan. En su lugar, documente qué interfaces se adaptan a sus estructuras para no crear una relación estrecha entre el paquete del consumidor y el suyo. Un gran ejemplo de esto es el paquete de errores .

Cuando tenemos una interfaz que no queremos exportar, podemos usar el subárbol interno / paquete para guardarlo dentro del paquete. Por lo tanto, no podemos temer que el usuario final pueda depender de él y, por lo tanto, puede ser flexible para cambiar la interfaz de acuerdo con los nuevos requisitos. Por lo general, creamos interfaces con dependencias externas para poder ejecutar pruebas localmente.

Este enfoque permite al usuario implementar sus propias interfaces pequeñas simplemente envolviendo alguna parte de la biblioteca para realizar pruebas. Para obtener más información sobre este concepto, lea el artículo de rakyl sobre la contaminación de la interfaz .

No exportar primitivas de concurrencia

Go ofrece primitivas de concurrencia fáciles de usar que a veces también pueden conducir a su uso excesivo debido a la misma simplicidad. En primer lugar, nos preocupan los canales y el paquete de sincronización. A veces es tentador exportar un canal de su paquete para que otros puedan usarlo. Además, un error común es incrustar sync.Mutex sin configurarlo como privado. Esto, como siempre, no siempre es malo, pero crea ciertos problemas al probar su programa.

Si exporta canales, también complica la vida del usuario del paquete, lo que no vale la pena hacer. Tan pronto como el canal se exporta desde el paquete, crea dificultades al probar el que usa este canal. Para una prueba exitosa, el usuario debe saber:

  • Cuando los datos terminan siendo enviados por el canal.
  • ¿Hubo algún error al recibir los datos?
  • ¿Cómo un paquete descarga el canal después de la finalización, si es que descarga?
  • ¿Cómo envolver un paquete API para que no lo llames directamente?

Eche un vistazo al ejemplo de lectura de cola. Aquí hay una biblioteca de ejemplo que lee de la cola y proporciona al usuario una fuente para leer.

 type Reader struct {...} func (r *Reader) ReadChan() <-chan Msg {...} 

Ahora el usuario de su biblioteca quiere implementar una prueba para su consumidor:

 func TestConsumer(t testing.T) { cons := &Consumer{ r: libqueue.NewReader(), } for msg := range cons.r.ReadChan() { // Test thing. } } 


El usuario puede decidir que la inyección de dependencia es una buena idea y escribir sus propios mensajes en el canal:

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for msg := range cons.r.ReadChan() { // Test thing. } } 


Espera, ¿qué hay de los errores?

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for { select { case msg := <-cons.r.ReadChan(): // Test thing. case err := <-cons.r.ErrChan(): // What caused this again? } } } 


Ahora tenemos que generar eventos de alguna manera para poder escribir realmente en este código auxiliar, que replica el comportamiento de la biblioteca que estamos utilizando. Si la biblioteca acaba de escribir la API síncrona, entonces podríamos agregar todo el paralelismo al código del cliente, por lo que las pruebas se vuelven más fáciles.

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } msg, err := cons.r.ReadMsg() // handle err, test thing } 


Si tiene dudas, recuerde que siempre es fácil agregar paralelismo al paquete del consumidor (paquete consumidor), y es difícil o imposible eliminarlo después de exportarlo desde la biblioteca. Y lo más importante, no olvide escribir en la documentación del paquete si la estructura / paquete es seguro para el acceso simultáneo a varias gorutinas.
A veces todavía es deseable o necesario exportar el canal. Para mitigar algunos de los problemas mencionados anteriormente, puede proporcionar canales a través de accesores en lugar de acceso directo y dejarlos abiertos solo para leer ( ←chan ) o solo para escribir ( chan← ) al declarar.

Use net/http/httptest

Httptest permite ejecutar código http.Handler sin iniciar un servidor o vincularlo a un puerto. Esto acelera las pruebas y le permite ejecutar pruebas en paralelo a un costo menor.

Aquí hay un ejemplo de la misma prueba implementada de dos maneras. Aquí no hay nada grandioso, pero este enfoque reduce la cantidad de código y ahorra recursos.

 func TestServe(t *testing.T) { // The method to use if you want to practice typing s := &http.Server{ Handler: http.HandlerFunc(ServeHTTP), } // Pick port automatically for parallel tests and to avoid conflicts l, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } defer l.Close() go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } func TestServeMemory(t *testing.T) { // Less verbose and more flexible way req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil) w := httptest.NewRecorder() ServeHTTP(w, req) greeting, err := ioutil.ReadAll(w.Body) if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } 

Quizás la característica más importante es que con httptest solo puede dividir la prueba en la función que desea probar. No hay enrutadores, middleware ni ningún otro efecto secundario que surja al configurar servidores, servicios, fábricas de procesadores, fábricas de procesadores o cualquier otra cosa que considere una buena idea.

Para ver este principio en acción, consulte el artículo de Marc Berger .

Usar paquete separado _test

La mayoría de las pruebas en el ecosistema se crean en los archivos pkg_test.go , pero aún permanecen en el mismo paquete: package pkg . Un paquete de prueba separado es el paquete que crea en el nuevo archivo, foo_test.go , en el directorio del módulo que desea probar, foo/ , con el package foo_test declaración package foo_test . Desde aquí puede importar github.com/example/foo y otras dependencias. Esta característica le permite hacer muchas cosas. Esta es la solución recomendada para las dependencias cíclicas en las pruebas, evita la aparición de "pruebas frágiles" y permite al desarrollador sentir cómo es usar su propio paquete. Si su paquete es difícil de usar, entonces las pruebas con este método también serán difíciles.

Esta estrategia evita pruebas frágiles al restringir el acceso a variables privadas. En particular, si sus pruebas se rompen y usa paquetes de prueba separados, es casi seguro que un cliente que use una función que rompa las pruebas también se romperá cuando se le llame.

Finalmente, ayuda a evitar ciclos de importación en las pruebas. Es más probable que la mayoría de los paquetes dependan de otros paquetes que escribió además de los de prueba, por lo que terminará en una situación en la que el ciclo de importación se produce de forma natural. Un paquete externo se encuentra sobre ambos paquetes en la jerarquía de paquetes. Tomemos un ejemplo de The Go Programming Language (Capítulo 11 Sección 2.4), donde net/url implementa un analizador de URL que net/http importa para su uso. Sin embargo, net / url debe probarse con un caso de uso real importando net / http . Así net/url_test .

Ahora, cuando usa un paquete de prueba por separado, es posible que necesite acceso a entidades no exportadas en el paquete donde estaban disponibles anteriormente. Algunos desarrolladores se enfrentan a esto por primera vez cuando prueban algo basado en el tiempo (por ejemplo, el tiempo. Ahora se convierte en un trozo usando una función). En este caso, podemos usar un archivo adicional para proporcionar entidades exclusivamente durante las pruebas, ya que los archivos _test.go excluidos de las compilaciones normales.

¿Qué necesitas recordar?

Es importante recordar que ninguno de los métodos descritos anteriormente es una panacea. El mejor enfoque en cualquier negocio es analizar críticamente la situación y elegir independientemente la mejor solución para el problema.

¿Desea obtener más información sobre las pruebas con Go?
Lee estos artículos:

La tabla de escritura de Dave Cheney condujo pruebas en Go
El capítulo del lenguaje de programación Go sobre pruebas.
O mira estos videos:
Pruebas avanzadas de Hashimoto con Go talk de Gophercon 2017
Conversación sobre técnicas de prueba de Andrew Gerrand de 2014

Esperamos que esta traducción te haya sido útil. Estamos esperando comentarios, y todos los que quieran aprender más sobre el curso, los invitamos a la jornada de puertas abiertas , que se llevará a cabo el 23 de mayo.

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


All Articles