¿Debería ser "Organizar-Afirmar-Actuar-Afirmar"?


94

Con respecto al patrón de prueba clásico de Arrange-Act-Assert , con frecuencia me encuentro agregando una contraafirmación que precede a Act. De esta manera sé que la afirmación pasajera realmente pasa como resultado de la acción.

Lo considero análogo al rojo en red-green-refactor, donde solo si he visto la barra roja en el curso de mis pruebas, sé que la barra verde significa que he escrito un código que marca la diferencia. Si escribo una prueba satisfactoria , cualquier código la satisfará; De manera similar, con respecto a Arrange-Assert-Act-Assert, si mi primera afirmación falla, sé que cualquier Ley habría pasado la Afirmación final, por lo que en realidad no verificaba nada sobre la Ley.

¿Sus pruebas siguen este patrón? ¿Por qué o por qué no?

Aclaración de actualización : la afirmación inicial es esencialmente lo opuesto a la afirmación final. No es una afirmación de que Arrange funcionó; es una afirmación de que Act aún no ha funcionado.

Respuestas:


121

Esto no es lo más común, pero lo suficientemente común como para tener su propio nombre. Esta técnica se llama Aserción de guardia . Puede encontrar una descripción detallada en la página 490 en el excelente libro xUnit Test Patterns de Gerard Meszaros (muy recomendable).

Normalmente, no uso este patrón yo mismo, ya que me parece más correcto escribir una prueba específica que valide cualquier condición previa que sienta la necesidad de asegurar. Tal prueba siempre debería fallar si falla la condición previa, y esto significa que no la necesito incrustada en todas las demás pruebas. Esto proporciona un mejor aislamiento de las preocupaciones, ya que un caso de prueba solo verifica una cosa.

Puede haber muchas condiciones previas que deban cumplirse para un caso de prueba determinado, por lo que es posible que necesite más de una afirmación de guardia. En lugar de repetirlos en todas las pruebas, tener una (y solo una) prueba para cada condición previa mantiene su código de prueba más sostenible, ya que tendrá menos repetición de esa manera.


+1, muy buena respuesta. La última parte es especialmente importante, porque muestra que puede guardar cosas como una prueba de unidad separada.
murrekatt

3
En general, también lo he hecho de esta manera, pero hay un problema con tener una prueba separada para garantizar las condiciones previas (especialmente con una base de código grande con requisitos cambiantes): la prueba de condiciones previas se modificará con el tiempo y no estará sincronizada con la 'principal' prueba que presupone esas condiciones previas. Por lo tanto, las condiciones previas pueden estar bien y en verde, pero esas condiciones previas no se cumplen en la prueba principal, que ahora siempre se muestra en verde y bien. Pero si las condiciones previas estuvieran en la prueba principal, habrían fallado. ¿Se ha encontrado con este problema y ha encontrado una buena solución?
nchaud

2
Si cambia mucho sus pruebas, es posible que tenga otros problemas , porque eso tenderá a hacer que sus pruebas sean menos confiables. Incluso frente a los requisitos cambiantes, considere la posibilidad de diseñar el código de forma solo para agregar .
Mark Seemann

@MarkSeemann Tiene razón, tenemos que minimizar la repetición, pero por otro lado puede haber muchas cosas que pueden afectar el Arrange para la prueba específica, aunque la prueba para Arrange sí pasaría. Por ejemplo, la limpieza para la prueba Arrange o después de que otra Prueba fallara y Arrange no sería igual que en la prueba Arrange.
Rekshino


8

He aquí un ejemplo.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Podría ser que escribiera Range.includes()simplemente para devolver la verdad. No lo hice, pero puedo imaginar que podría haberlo hecho. O podría haberlo escrito mal de muchas otras formas. Esperaría y esperaría que con TDD lo hiciera bien, queincludes() lo hiciera simplemente funciona, pero tal vez no lo hice. Entonces, la primera afirmación es una verificación de cordura, para garantizar que la segunda afirmación sea realmente significativa.

Leído por sí mismo, assertTrue(range.includes(7));está diciendo: "afirmar que el rango modificado incluye 7". Si se lee en el contexto de la primera afirmación, dice: "afirmar que invocando abarcar () hace que incluya 7. Y como abarcar es la unidad que estamos probando, creo que tiene algún (pequeño) valor.

Estoy aceptando mi propia respuesta; muchos otros malinterpretaron mi pregunta como si se tratara de probar la configuración. Creo que esto es un poco diferente.


Gracias por volver con un ejemplo, Carl. Bueno, en la parte roja del ciclo TDD, hasta que el abarcar () realmente hace algo; la primera afirmación no tiene sentido, es sólo una duplicación de la segunda. En verde, comienza a ser útil. Cobra sentido durante la refactorización. Sería bueno tener un marco UT que haga esto automáticamente.
philant

Suponga que TDD esa clase Range, ¿no habrá otra prueba fallida probando el Range ctor, cuando lo rompa?
philant

1
@philippe: No estoy seguro de haber entendido la pregunta. El constructor Range e includes () tienen sus propias pruebas unitarias. ¿Podría darnos más detalles, por favor?
Carl Manaster

Para que la primera aserción assertFalse (range.includes (7)) falle, debe tener un defecto en el Range Constructor. Así que quise preguntar si las pruebas para el constructor Range no se romperán al mismo tiempo que esa aserción. ¿Y qué hay de afirmar después de la Ley sobre otro valor: por ejemplo, assertFalse (rango.incluye (6))?
philant

1
La construcción de rango, en mi opinión, viene antes que funciones como incluye (). Entonces, aunque estoy de acuerdo, solo un constructor defectuoso (o un include () defectuoso) causaría que esa primera afirmación fallara, la prueba del constructor no incluiría una llamada a includes (). Sí, todas las funciones hasta la primera afirmación ya están probadas. Pero esta afirmación negativa inicial está comunicando algo y, en mi opinión, algo útil. Incluso si todas esas afirmaciones pasan cuando se escriben inicialmente.
Carl Manaster

7

Una Arrange-Assert-Act-Assertprueba siempre se puede refactorizar en dos pruebas:

1. Arrange-Assert

y

2. Arrange-Act-Assert

La primera prueba solo afirmará lo que se configuró en la fase Organizar, y la segunda prueba solo confirmará lo que sucedió en la fase Actuar.

Esto tiene la ventaja de brindar una retroalimentación más precisa sobre si es la fase de Arreglo o la de Actuar la que falló, mientras que en el original Arrange-Assert-Act-Assertse combinan y tendría que profundizar y examinar exactamente qué afirmación falló y por qué falló para saber si fue el Acuerdo o el Acto el que falló.

También satisface mejor la intención de realizar pruebas unitarias, ya que separa la prueba en unidades independientes más pequeñas.

Por último, tenga en cuenta que siempre que vea secciones Organizar similares en una prueba diferente, debe intentar extraerlas en métodos auxiliares compartidos, para que sus pruebas sean más SECAS y más fáciles de mantener en el futuro.


3

Ahora estoy haciendo esto. AAAA de un tipo diferente

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Ejemplo de prueba de actualización:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

La razón es que el ACT no contiene la lectura del ReadUpdated porque no forma parte del acto. El acto solo cambia y ahorra. Entonces, realmente, ARRANGE ReadUpdated para aserción, estoy llamando a ASSEMBLE para aserción. Esto es para evitar confundir la sección ARRANGE

ASSERT solo debe contener afirmaciones. Eso deja ASSEMBLE entre ACT y ASSERT, lo que configura la aserción.

Por último, si está fallando en el Arrange, sus pruebas no son correctas porque debería tener otras pruebas para prevenir / encontrar estos errores triviales . Porque para el escenario que presento, ya debería haber otras pruebas que prueben READ y CREATE. Si crea una "Aserción de guardia", puede estar interrumpiendo DRY y creando mantenimiento.


1

Lanzar una afirmación de "comprobación de cordura" para verificar el estado antes de realizar la acción que está probando es una técnica antigua. Por lo general, los escribo como andamios de prueba para demostrarme a mí mismo que la prueba hace lo que espero y los elimino más tarde para evitar saturar las pruebas con andamios de prueba. A veces, dejar el andamio ayuda a que la prueba sirva como narrativa.


1

Ya leí acerca de esta técnica, posiblemente de usted por cierto, pero no la uso; principalmente porque estoy acostumbrado a la forma triple A para mis pruebas unitarias.

Ahora, tengo curiosidad y tengo algunas preguntas: ¿cómo escribe su prueba, hace que esta afirmación falle, siguiendo un ciclo de refactorización rojo-verde-rojo-verde, o la agrega después?

¿Fallas a veces, quizás después de refactorizar el código? Qué te dice esto ? Quizás podría compartir un ejemplo en el que ayudó. Gracias.


Por lo general, no fuerzo a que falle la afirmación inicial; después de todo, no debería fallar, como debería hacerlo una afirmación TDD, antes de escribir su método. Yo no escribo, cuando escribo que, antes , justo en el curso normal de escribir la prueba, no después. Honestamente, no recuerdo que haya fallado, tal vez eso sugiera que es una pérdida de tiempo. Intentaré dar un ejemplo, pero no tengo ninguno en mente en este momento. Gracias por las preguntas; son útiles.
Carl Manaster

1

He hecho esto antes al investigar una prueba que falló.

Después de un considerable rascado de cabeza, determiné que la causa era que los métodos llamados durante "Organizar" no funcionaban correctamente. El fracaso de la prueba fue engañoso. Agregué una afirmación después del arreglo. Esto hizo que la prueba fallara en un lugar que resaltaba el problema real.

Creo que también hay un olor a código aquí si la parte Arrange de la prueba es demasiado larga y complicada.


Un punto menor: consideraría el Arrange demasiado complicado más como un olor de diseño que un olor a código; a veces, el diseño es tal que solo un Arrange complicado le permitirá probar la unidad. Lo menciono porque esa situación quiere una solución más profunda que un simple olor a código.
Carl Manaster

1

En general, me gusta mucho "Organizar, actuar, afirmar" y lo uso como mi estándar personal. Sin embargo, lo único que no me recuerda que haga es desordenar lo que he arreglado cuando se hacen las afirmaciones. En la mayoría de los casos, esto no causa mucha molestia, ya que la mayoría de las cosas desaparecen automáticamente a través de la recolección de basura, etc. Sin embargo, si ha establecido conexiones con recursos externos, probablemente querrá cerrar esas conexiones cuando haya terminado. con sus afirmaciones o muchos tienen un servidor o recurso costoso en algún lugar que se aferra a conexiones o recursos vitales que debería poder regalar a otra persona. Esto es particularmente importante si está uno de esos desarrolladores que no usa TearDown o TestFixtureTearDownpara limpiar después de una o más pruebas. Por supuesto, "Organizar, actuar, afirmar" no es responsable de que no cierre lo que abro; ¡Solo menciono este "gotcha" porque todavía no he encontrado un buen sinónimo de "A-word" para "disponer" para recomendar! ¿Alguna sugerencia?


1
@carlmanaster, ¡eres lo suficientemente cerca para mí! Lo voy a pegar en mi próximo TestFixture para probarme el tamaño. Es como ese pequeño recordatorio de hacer lo que tu madre debería haberte enseñado: "¡Si lo abres, ciérralo! ¡Si lo estropeas, límpialo!" Tal vez alguien más pueda mejorarlo, pero al menos comienza con una "¡a!" ¡Gracias por tu sugerencia!
John Tobler

1
@carlmanaster, le di una oportunidad a "Annul". Es mejor que "desmontaje" y funciona, pero todavía estoy buscando otra palabra "A" que se me quede en la cabeza tan perfectamente como "Organizar, actuar, afirmar". Tal vez "¡¿Aniquilar ?!"
John Tobler

1
Así que ahora tengo "Organizar, asumir, actuar, afirmar, aniquilar". ¡Hmmm! Estoy complicando demasiado las cosas, ¿eh? Tal vez sea mejor que me Bese y vuelva a "¡Organizar, actuar y afirmar!"
John Tobler

1
¿Quizás usar una R para reiniciar? Sé que no es una A, pero suena como un pirata diciendo: ¡Aaargh! y Restablecer rimas con Assert: o
Marcel Valdez Orozco

1

Eche un vistazo a la entrada de Wikipedia sobre Diseño por contrato . La Santísima Trinidad Arrange-Act-Assert es un intento de codificar algunos de los mismos conceptos y se trata de demostrar la corrección del programa. Del artículo:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

Existe una compensación entre la cantidad de esfuerzo invertido en configurar esto y el valor que agrega. AAA es un recordatorio útil de los pasos mínimos requeridos, pero no debe disuadir a nadie de crear pasos adicionales.


0

Depende de su entorno / idioma de prueba, pero generalmente si algo falla en la parte Arrange, se lanza una excepción y la prueba falla al mostrarlo en lugar de iniciar la parte Act. Entonces no, generalmente no uso una segunda parte de Assert.

Además, en el caso de que su parte Arrange sea bastante compleja y no siempre arroje una excepción, tal vez podría considerar envolverla dentro de algún método y escribir una prueba propia para ella, para que pueda estar seguro de que no fallará (sin lanzar una excepción).


0

No uso ese patrón, porque creo que hacer algo como:

Arrange
Assert-Not
Act
Assert

Puede ser inútil, porque supuestamente sabe que su parte Arrange funciona correctamente, lo que significa que todo lo que está en la parte Arrange debe probarse también o ser lo suficientemente simple como para no necesitar pruebas.

Usando el ejemplo de su respuesta:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}

Me temo que no comprende realmente mi pregunta. La afirmación inicial no se trata de probar Arrange; simplemente se asegura de que la Ley sea lo que haga que el estado se afirme al final.
Carl Manaster

Y mi punto es que, lo que sea que ponga en la parte Assert-Not, ya está implícito en la parte Arrange, porque el código en la parte Arrange se ha probado a fondo y ya sabe lo que hace.
Marcel Valdez Orozco

Pero creo que hay valor en la parte Assert-Not, porque estás diciendo: Dado que la parte Arrange deja 'el mundo' en 'este estado', entonces mi 'Act' dejará 'el mundo' en este 'nuevo estado' ; y si la implementación del código del que depende la parte Arrange cambia, la prueba también se interrumpirá. Pero nuevamente, eso podría estar en contra de DRY, porque también (debería) tener pruebas para cualquier código del que dependa en la parte Arrange.
Marcel Valdez Orozco

Tal vez en proyectos donde hay varios equipos (o un equipo grande) trabajando en el mismo proyecto, tal cláusula sería bastante útil, de lo contrario, la encuentro innecesaria y redundante.
Marcel Valdez Orozco

Probablemente, una cláusula de este tipo sería mejor en las pruebas de integración, las pruebas del sistema o las pruebas de aceptación, donde la parte de Arreglo generalmente depende de más de un componente y hay más factores que podrían hacer que el estado inicial del 'mundo' cambie inesperadamente. Pero no veo un lugar para eso en las pruebas unitarias.
Marcel Valdez Orozco

0

Si realmente quieres probar todo en el ejemplo, prueba más pruebas ... como:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Porque, de lo contrario, se pierden muchas posibilidades de error ... por ejemplo, después de abarcar, el rango solo incluye 7, etc. También hay pruebas para la longitud del rango (para asegurarse de que no abarque también un valor aleatorio), y otro conjunto de pruebas completamente para tratar de abarcar 5 en el rango ... ¿qué esperaríamos: una excepción en el abarcamiento o que el rango no se altere?

De todos modos, el punto es que si hay alguna suposición en el acto que desea probar, póngala en su propia prueba, ¿no?


0

Yo suelo:

1. Setup
2. Act
3. Assert 
4. Teardown

Porque una configuración limpia es muy importante.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.