¿Es una buena idea usar pruebas unitarias para contar una historia?

13

Entonces, tengo un módulo de autenticación que escribí hace algún tiempo. Ahora estoy viendo los errores de mi camino y escribiendo pruebas unitarias para ello. Mientras escribo pruebas unitarias, me resulta difícil encontrar buenos nombres y buenas áreas para probar. Por ejemplo, tengo cosas como

  • RequiereLogin_should_redirect_when_not_logged_in
  • RequiereLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Personalmente, creo que es un poco feo, aunque parezca "correcto". También tengo problemas para diferenciar las pruebas con solo escanearlas (tengo que leer el nombre del método al menos dos veces para saber qué acaba de fallar)

Entonces, pensé que tal vez en lugar de escribir pruebas que puramente probaran la funcionalidad, tal vez escribiera un conjunto de pruebas que cubran escenarios.

Por ejemplo, este es un talón de prueba que se me ocurrió:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

¿Es esto un patrón de oído? He visto historias de aceptación y cosas por el estilo, pero esto es fundamentalmente diferente. La gran diferencia es que se me ocurren escenarios para "forzar" las pruebas. En lugar de intentar manualmente generar posibles interacciones que tendré que probar. Además, sé que esto fomenta las pruebas de unidad que no evalúan exactamente un método y una clase. Aunque creo que esto está bien. Además, soy consciente de que esto causará problemas al menos en algunos marcos de prueba, ya que generalmente asumen que las pruebas son independientes entre sí y que el orden no importa (donde lo haría en este caso).

De todos modos, ¿es este un patrón aconsejable? O, ¿sería esto un ajuste perfecto para las pruebas de integración de mi API en lugar de como pruebas de "unidad"? Esto es solo en un proyecto personal, así que estoy abierto a experimentos que pueden o no ir bien.

    
pregunta Earlz 23.01.2013 - 07:47

3 respuestas

15

Sí, es una buena idea dar a sus pruebas los nombres de los escenarios de ejemplo que está probando. Y usar la herramienta de prueba de unidad para más que solo pruebas de unidad puede que también esté bien, muchas personas lo hacen con éxito (yo también).

Pero no, definitivamente no es una buena idea escribir sus pruebas de una manera en la que el orden de ejecución de las pruebas sea importante. Por ejemplo, NUnit le permite al usuario seleccionar de forma interactiva qué prueba quiere ejecutar, por lo que esto ya no funcionará de la manera prevista.

Aquí puede evitar esto fácilmente separando la parte principal de prueba de cada prueba (incluida la "afirmación") de las partes que configuran su sistema en el estado inicial correcto. Utilizando el ejemplo anterior: escriba métodos para crear una cuenta, iniciar sesión y publicar un comentario, sin ninguna aseveración. Luego reutiliza esos métodos en diferentes pruebas. También deberá agregar algo de código al método [Setup] de sus dispositivos de prueba para asegurarse de que el sistema se encuentre en un estado inicial correctamente definido (por ejemplo, no hay cuentas en la base de datos, no hay nadie conectado hasta el momento, etc.).

EDITAR: Por supuesto, esto parece ir en contra de la naturaleza de "historia" de sus pruebas, pero si le da a sus métodos de ayuda nombres significativos, encontrará sus historias dentro de cada prueba.

Entonces, debería verse así:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}
    
respondido por el Doc Brown 23.01.2013 - 10:48
4

Un problema al contar una historia con pruebas unitarias es que no hace explícito que las pruebas unitarias deben organizarse y ejecutarse de forma totalmente independiente entre sí.

Una buena prueba de unidad debe estar completamente aislada de todos los demás códigos dependientes, es la unidad más pequeña de código que se puede probar.

Esto brinda el beneficio de que, además de confirmar que el código funciona, si una prueba falla, se obtiene el diagnóstico de dónde exactamente está equivocado el código. Si una prueba no está aislada, debe ver de qué depende para averiguar exactamente qué salió mal y perder un beneficio importante de la prueba unitaria. El hecho de que el orden de ejecución sea importante también puede generar muchos falsos negativos, si una prueba falla, es posible que las siguientes pruebas fallen a pesar de que el código de la prueba funciona perfectamente bien.

Un buen artículo en más profundidad es el clásico en pruebas híbridas sucias .

Para hacer que las clases, los métodos y los resultados sean legibles, el excelente Arte de la unidad de pruebas utiliza la convención de nomenclatura

Clase de prueba:

  

ClassUnderTestTests

Métodos de prueba:

  

MethodUnderTest_Condition_ExpectedResult

Para copiar el ejemplo de @Doc Brown, en lugar de usar [Configuración] que se ejecuta antes de cada prueba, escribo métodos de ayuda para crear objetos aislados para probar.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Por lo tanto, las pruebas que fallan tienen un nombre significativo que le da una descripción de qué método falló, la condición y el resultado esperado.

Así es como siempre he escrito pruebas de unidad, pero un amigo ha tenido mucho éxito con Gerkin .

    
respondido por el StuperUser 23.01.2013 - 12:29
3

Lo que estás describiendo suena más como Behavior Driven Design (BDD) que las pruebas unitarias para mí. Eche un vistazo a SpecFlow que es una tecnología .NET BDD que se basa en el Gherkin DSL.

Cosas poderosas que cualquier humano puede leer / escribir sin saber nada acerca de la codificación. Nuestro equipo de pruebas está disfrutando de un gran éxito aprovechándolo para nuestras suites de pruebas de integración.

En cuanto a las convenciones para las pruebas unitarias, la respuesta de @ DocBrown parece sólida.

    
respondido por el Drew Marsh 23.01.2013 - 23:39

Lea otras preguntas en las etiquetas