Un caso para la generación de código Go: testifique

Nov 07 2022
Si ha estado usando Go por un tiempo, probablemente esté familiarizado con la biblioteca de prueba stretchr/testify. Facilita la escritura de pruebas y proporciona varias funciones de aserción, como Igual, Contiene, Mayor y muchas más.

Si ha estado usando Go por un tiempo, probablemente esté familiarizado con la biblioteca de pruebas stretchr/testify . Facilita la escritura de pruebas y proporciona varias funciones de aserción, como Equal, Contains, Greatery muchas más .

Las funciones de aserción se comportan de manera diferente según el alcance. Por ejemplo, cuando se llama desde el paquete asserto . requireEl primero registra el error y continúa, mientras que el segundo detiene la prueba inmediatamente. Ambos paquetes ofrecen las mismas firmas de funciones. Las aserciones también se denominan métodos de una suiteestructura, que envuelve el *testing.Tpuntero. Y cada aserción tiene una contraparte de formato con un *fsufijo y msg string, params ...interface{}parámetros adicionales ( ) al final.

Esto nos deja con tres dimensiones de las funciones de aserción: funciones estáticas, métodos de conjunto y formateados. Un total de ocho versiones diferentes de cada función :

Ejemplo de todas las implementaciones de .Equal

Este es un caso interesante para la generación de código. Parece que podemos implementar todas las funciones como contenedor de una implementación de aserción canónica. En su mayoría, es un código repetitivo o repetitivo que, de lo contrario, sería tedioso escribir a mano. Y, como era de esperar, eso es exactamente lo que hace el paquete.

El resto de esta publicación es una descarga de cerebro a medida que analizo el código, tratando de comprender cómo funciona Testify internamente y cómo organizaron su código para usar la generación de código.

Ejemplo: Equalfunción

Primero veamos la Equalfunción y cómo difiere el comportamiento en asserty require:

La implementación subyacente de .Equales la misma. Comparar dos valores y proporcionar una salida de texto no debería cambiar en assertlas requireimplementaciones. Lo que cambia aquí es el flujo de la prueba. Se detiene o registra el error y continúa.

La assert.Equalfunción donde se realiza la implementación canónica de Equal.

Me ahorraré los detalles sobre cómo funciona la comparación más allá de la ObjectsAreEqualfunción. Centrémonos en el código circundante y cómo se invoca en otras partes del código base.

Una cosa importante a tener en cuenta aquí es que las assert.*funciones devuelven un archivo bool. Esto es apalancado por su require.*contraparte.

require.Equalse ve exactamente lo que usted esperaría:

Envuelve la assert.Equalfunción y llama t.FailNow()en caso de que la afirmación falle (devuelta false).

Lo mismo es cierto para cada función de aserción. En lugar de escribir las firmas de funciones repetitivas para cada afirmación, veamos cómo usaron plantillas para generar cuerpos de funciones.

Afirmar plantillas de funciones

requiere paquete

Cada función en el requirepaquete tiene el mismo código repetitivo. Debe (1) llamar a assert.*la función; y (2) fallar la prueba si la afirmación devuelve falso.

{{.Comment}}
func {{.DocInfo.Name}}(t TestingT, {{.Params}}) {
	if h, ok := t.(tHelper); ok { h.Helper() }
	if assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) {
                return
        }
        t.FailNow()
}

Métodos de conjunto

Dado que estas aserciones son métodos en la estructura de la suite, necesitan un receptor, que será del tipo *Assertionen ambas estructuras .Assert()y .Require().

{{.CommentWithoutT "a"}}
func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) {
	if h, ok := a.t.(tHelper); ok { h.Helper() }
	{{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
}

Solo usaron plantillas de funciones de formato para el assertpaquete. Todos los demás formatos se generan en función de las assertfunciones del paquete, incluidas las prefijadas de formato. Es por eso que solo encontrará una plantilla de formato en el assertpaquete.

{{.CommentFormat}}
func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool {
	if h, ok := t.(tHelper); ok { h.Helper() }
	return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}})
}

Combinando estas tres plantillas, puede generar todos los diseños de funciones proporcionados por testify:

  1. Use *funa plantilla para generar funciones de formato basadas en las assertimplementaciones canónicas.
  2. Utilice requirela plantilla para generar funciones de assertfunciones de contenedor.
  3. Use la plantilla de método de la suite para generar funciones con *Assertionsel receptor que envuelve llamadas assert.*y require.*funciones.

Hay un único archivo generador en la _codegencarpeta, que utilizan todos los tipos de funciones. Se llama desde cada paquete usando parámetros de bandera. Por ejemplo, el comando utilizado para generar require.*funciones:

//go:generate sh -c "cd ../_codegen && go build && cd - && ../_codegen/_codegen -output-package=require -template=require.go.tmpl -include-format-funcs"

Hay mucho más sobre cómo funciona el script de generación de código, pero no creo que sea útil analizar esa lógica aquí. La parte más importante para comprender su enfoque es observar esta estructura:

Analizar el pkgarchivo de entrada (que por defecto apunta a assert) genera una porción de testFunc. Cada uno de estos elementos se utiliza para generar una función de contraparte basada en una plantilla. La estructura ofrece algunas funciones auxiliares para construir argumentos y parámetros:

Puedes adivinar fácilmente lo que hacen según los nombres.

La generación de código es compleja, pero es una complejidad oculta que no se infiltra en el resto del código de la biblioteca. Una vez que se crea el código de análisis, es poco probable que deba cambiarse.

Un enfoque alternativo

La generación de código es divertida, pero la mayoría de las veces, cuando me pregunto si realmente es necesario, la respuesta es no. Las generaciones de código evitan escribir muchas líneas de código repetitivo. Pero deja una función de generador compleja, a menudo sin probar, que utiliza una astlógica desordenada.

Cada vez que siento la necesidad de usar la generación de código, me pregunto si el problema que estoy tratando de resolver es un problema artificial. Si es un problema real, ¿se puede resolver de otras formas? Apliquemos este razonamiento al paquete testify:

Hay tres razones principales por las que se utiliza aquí la generación de código. Las mismas funciones de afirmación se pueden ofrecer con diferentes sabores:

  1. Funciones estáticas frente a métodos: proporcionar una función o un método en la estructura que envuelve *testing.Tes un problema artificial. Un enfoque obstinado sería proporcionar únicamente métodos.
  2. Con formato y sin formato: esto parece un problema legítimo, pero hay otra solución en lugar de ofrecer un sufijo de función diferente.
  3. Falla rápido: también es una característica legítima, pero se puede resolver con construcciones más simples que proporcionar diferentes funciones de ámbito de paquete.

Como ejercicio de reflexión, aquí hay una alternativa que se puede construir sin necesidad de generar código:

Si aceptamos que (1) es un problema artificial y proporcionamos solo métodos, podría diseñar aserciones utilizando un patrón de construcción. Las afirmaciones pertenecen a una assertionestructura que tiene funciones de configuración para establecer el comportamiento del flujo de control.

Aquí hay un prototipo de una estructura de aserción que cumple con esa interfaz y tiene propiedades de formato y flujo como parte del contexto de prueba:

Vale la pena mencionar que esta no es una implementación seria, es más un ejercicio de reflexión sobre cómo podría estructurarse. Aquí faltan funciones como brindar soporte para *testing.Bllamadas y probablemente otros casos de uso que no puedo ver en este momento.

compensaciones

Creo que la mayor compensación aquí es: la complejidad pasó del testifypaquete al código de prueba del cliente. Escribir pruebas usando testify es extremadamente simple. Rara vez he abierto su documentación después de las primeras veces que escribí pruebas. Lo cual es una experiencia única en comparación con otras bibliotecas de prueba. Por ejemplo, con chai de JavaScript, donde a menudo olvidé el orden y las formas idiomáticas en las que debo escribir afirmaciones.

Este es un gran rasgo de testificar. Las buenas bibliotecas son aquellas en las que no tienes que pensar demasiado en ellas, y simplemente funcionan .

La ruta de generación de código deja una carga de mantenimiento para el desarrollador de la biblioteca, pero podría simplificar la API del paquete, lo que hace que la adopción sea más rápida. Creo que testifylos desarrolladores probablemente tomaron la decisión correcta aquí al simplificar esto para el usuario, incluso si corre el riesgo de complicar la capacidad de mantenimiento de su herramienta.

Después de todo, es mejor tener toda la complejidad en un solo lugar que un poco de complejidad en todas partes.

© Copyright 2021 - 2022 | unogogo.com | All Rights Reserved