Separar pruebas unitarias y pruebas de integración en Go

97

¿Existe una mejor práctica establecida para separar pruebas unitarias y pruebas de integración en GoLang (testificar)? Tengo una combinación de pruebas unitarias (que no dependen de ningún recurso externo y, por lo tanto, se ejecutan muy rápido) y pruebas de integración (que dependen de recursos externos y, por lo tanto, se ejecutan más lentamente). Entonces, quiero poder controlar si incluir o no las pruebas de integración cuando digo go test.

La técnica más sencilla parece ser definir una bandera -integrate en main:

var runIntegrationTests = flag.Bool("integration", false
    , "Run the integration tests (in addition to the unit tests)")

Y luego, para agregar una declaración if en la parte superior de cada prueba de integración:

if !*runIntegrationTests {
    this.T().Skip("To run this test, use: go test -integration")
}

¿Es esto lo mejor que puedo hacer? Busqué en la documentación de Testify para ver si tal vez hay una convención de nomenclatura o algo que logre esto para mí, pero no encontré nada. ¿Me estoy perdiendo de algo?

Craig Jones
fuente
2
Creo que stdlib usa -short para deshabilitar las pruebas que llegan a la red (y también otras cosas de larga duración). De lo contrario, su solución parece estar bien.
Volker
-short es una buena opción, al igual que sus banderas de compilación personalizadas, pero sus banderas no necesitan estar en main. si define la var como var integration = flag.Bool("integration", true, "Enable integration testing.")fuera de una función, la variable se mostrará en el alcance del paquete y la bandera funcionará correctamente
Atifm

Respuestas:

155

@ Ainar-G sugiere varios patrones excelentes para separar las pruebas.

Este conjunto de prácticas de Go de SoundCloud recomienda utilizar etiquetas de compilación ( descritas en la sección "Restricciones de compilación" del paquete de compilación ) para seleccionar qué pruebas ejecutar:

Escriba un integration_test.go y asígnele una etiqueta de compilación de integración. Defina indicadores (globales) para cosas como direcciones de servicio y cadenas de conexión, y utilícelas en sus pruebas.

// +build integration

var fooAddr = flag.String(...)

func TestToo(t *testing.T) {
    f, err := foo.Connect(*fooAddr)
    // ...
}

go test toma etiquetas de compilación al igual que go build, por lo que puede llamar go test -tags=integration. También sintetiza un paquete main que llama a flag.Parse, por lo que cualquier indicador declarado y visible será procesado y disponible para sus pruebas.

Como una opción similar, también puede hacer que las pruebas de integración se ejecuten de forma predeterminada utilizando una condición de compilación // +build !unity luego deshabilitarlas bajo demanda ejecutándolas go test -tags=unit.

@adamc comenta:

Para cualquier otra persona que intente usar etiquetas de compilación, es importante que el // +build testcomentario sea la primera línea de su archivo y que incluya una línea en blanco después del comentario; de lo contrario, el -tagscomando ignorará la directiva.

Además, la etiqueta utilizada en el comentario de la compilación no puede tener un guión, aunque se permiten guiones bajos. Por ejemplo, // +build unit-testsno funcionará, mientras que // +build unit_testssí.

Alex
fuente
1
He estado usando esto durante algún tiempo y es, con mucho, el enfoque más lógico y simple.
Ory Band
1
si tiene pruebas unitarias en el mismo paquete, necesita establecer las // + build unitpruebas unitarias y usar -tag unit para ejecutar las pruebas
LeoCBS
2
@ Tyler.z.yang ¿puede proporcionar un enlace o más detalles sobre la desaprobación de etiquetas? No encontré tal información. Estoy usando etiquetas con go1.8 para la forma descrita en la respuesta y también para simular tipos y funciones en las pruebas. Es una buena alternativa a las interfaces, creo.
Alexander I.Grafov
2
Para cualquier otra persona que intente utilizar etiquetas de compilación, es importante que el // +buildcomentario de prueba sea la primera línea de su archivo y que incluya una línea en blanco después del comentario; de lo contrario, el -tagscomando ignorará la directiva. Además, la etiqueta utilizada en el comentario de la compilación no puede tener un guión, aunque se permiten guiones bajos. Por ejemplo, // +build unit-testsno funcionará, mientras que // +build unit_tests
adamc
6
¿Cómo manejar comodines? go test -tags=integration ./...no funciona, ignora la etiqueta
Erika Dsouza
53

Para ampliar mi comentario a la excelente respuesta de @ Ainar-G, durante el año pasado he estado usando la combinación de -shortcon la Integrationconvención de nomenclatura para lograr lo mejor de ambos mundos.

Prueba de unidad e integración armonía, en el mismo archivo

Banderas de construcción previamente me obligó a tener varios archivos ( services_test.go, services_integration_test.go, etc.).

En su lugar, tome este ejemplo a continuación, donde los dos primeros son pruebas unitarias y tengo una prueba de integración al final:

package services

import "testing"

func TestServiceFunc(t *testing.T) {
    t.Parallel()
    ...
}

func TestInvalidServiceFunc3(t *testing.T) {
    t.Parallel()
    ...
}

func TestPostgresVersionIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    ...
}

Observe que la última prueba tiene la convención de:

  1. usando Integrationen el nombre de la prueba.
  2. comprobando si se ejecuta bajo la -shortdirectiva de bandera.

Básicamente, la especificación dice: "escribe todas las pruebas normalmente. Si se trata de una prueba de larga duración o una prueba de integración, sigue esta convención de nomenclatura y verifica -shortque seas amable con tus compañeros".

Ejecute solo pruebas unitarias:

go test -v -short

esto le proporciona un buen conjunto de mensajes como:

=== RUN   TestPostgresVersionIntegration
--- SKIP: TestPostgresVersionIntegration (0.00s)
        service_test.go:138: skipping integration test

Ejecute solo pruebas de integración:

go test -run Integration

Esto ejecuta solo las pruebas de integración. Útil para pruebas de humo en canarios en producción.

Obviamente, la desventaja de este enfoque es que si alguien ejecuta go test, sin el -shortindicador, ejecutará de forma predeterminada todas las pruebas: pruebas unitarias y de integración.

En realidad, si su proyecto es lo suficientemente grande como para tener pruebas unitarias y de integración, entonces lo más probable es que esté usando un Makefileen el que puede tener directivas simples para usar go test -shorten él. O simplemente póngalo en su README.mdarchivo y termine el día.

eduncan911
fuente
3
Amo la simplicidad
Jacob Stanley
¿Crea un paquete separado para que dicha prueba acceda solo a las partes públicas del paquete? ¿O todo mixto?
Dr.eel
@ Dr.eel Bueno, eso es OT de la respuesta. Pero personalmente, prefiero ambos: un nombre de paquete diferente para las pruebas para poder importprobar mi paquete, lo que termina mostrándome cómo se ve mi API para los demás. Luego sigo con cualquier lógica restante que deba cubrirse como nombres de paquetes de prueba internos.
eduncan911
@ eduncan911 ¡Gracias por la respuesta! Entonces, según tengo entendido, aquí package servicescontiene un sute de prueba de integración, por lo que para probar la API del paquete como una caja negra, deberíamos nombrarlo de otra manera package services_integration_test, no nos dará la oportunidad de trabajar con estructuras internas. Por lo tanto, el paquete para las pruebas unitarias (acceso a los componentes internos) debe nombrarse package services. ¿Es tan?
Dr.eel
Eso es correcto, sí. Aquí hay un ejemplo claro de cómo lo hago: github.com/eduncan911/podcast (observe la cobertura del código del 100%, usando ejemplos)
eduncan911
50

Veo tres posibles soluciones. La primera es utilizar el modo corto para pruebas unitarias. Por lo tanto, usaría go test -shortcon pruebas unitarias y lo mismo pero sin la -shortbandera para ejecutar sus pruebas de integración también. La biblioteca estándar usa el modo corto para omitir las pruebas de ejecución prolongada o hacer que se ejecuten más rápido al proporcionar datos más simples.

El segundo es usar una convención y llamar a sus pruebas TestUnitFooo TestIntegrationFooy luego usar la -runmarca de prueba para indicar qué pruebas ejecutar. Por lo que lo usaría go test -run 'Unit'para pruebas unitarias y go test -run 'Integration'para pruebas de integración.

La tercera opción es usar una variable de entorno y obtenerla en la configuración de sus pruebas con os.Getenv. Luego, usaría simple go testpara pruebas unitarias y FOO_TEST_INTEGRATION=true go testpara pruebas de integración.

Personalmente, preferiría la -shortsolución ya que es más simple y se usa en la biblioteca estándar, por lo que parece que es una forma de facto de separar / simplificar las pruebas de larga duración. Pero las soluciones -runy os.Getenvofrecen más flexibilidad (también se requiere más precaución, ya que las expresiones regulares están involucradas -run).

Ainar-G
fuente
1
tenga en cuenta que los corredores de prueba de la comunidad (por ejemplo Tester-Go) comunes a los IDE (Atom, Sublime, etc.) tienen la opción incorporada para ejecutarse con -shortbandera, junto con -coveragey otros. por lo tanto, utilizo una combinación de Integración en el nombre de la prueba, junto con if testing.Short()comprobaciones dentro de esas pruebas. me permite tener lo mejor de ambos mundos: ejecutar -shortdentro de IDE y ejecutar explícitamente solo pruebas de integración congo test -run "Integration"
eduncan911
5

Estaba tratando de encontrar una solución para lo mismo recientemente. Estos fueron mis criterios:

  • La solución debe ser universal
  • Sin paquete separado para pruebas de integración
  • La separación debería ser completa (debería poder ejecutar pruebas de integración únicamente )
  • Sin convención de nomenclatura especial para las pruebas de integración
  • Debería funcionar bien sin herramientas adicionales

Las soluciones antes mencionadas (bandera personalizada, etiqueta de compilación personalizada, variables de entorno) realmente no cumplían con todos los criterios anteriores, así que después de investigar un poco y jugar, se me ocurrió esta solución:

package main

import (
    "flag"
    "regexp"
    "testing"
)

func TestIntegration(t *testing.T) {
    if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name()) {
        t.Skip("skipping as execution was not requested explicitly using go test -run")
    }

    t.Parallel()

    t.Run("HelloWorld", testHelloWorld)
    t.Run("SayHello", testSayHello)
}

La implementación es sencilla y mínima. Aunque requiere una convención simple para las pruebas, es menos propenso a errores. Una mejora adicional podría ser exportar el código a una función auxiliar.

Uso

Ejecute pruebas de integración solo en todos los paquetes de un proyecto:

go test -v ./... -run ^TestIntegration$

Ejecute todas las pruebas ( regulares e integradas):

go test -v ./... -run .\*

Ejecute solo pruebas regulares :

go test -v ./...

Esta solución funciona bien sin herramientas, pero un Makefile o algunos alias pueden facilitar su uso. También se puede integrar fácilmente en cualquier IDE que admita la ejecución de pruebas go.

El ejemplo completo se puede encontrar aquí: https://github.com/sagikazarmark/modern-go-application

mark.sagikazar
fuente