¿Realmente necesito un marco de prueba de unidad?

19

Actualmente en mi trabajo, tenemos un gran conjunto de pruebas unitarias para nuestra aplicación C ++. Sin embargo, no usamos un marco de prueba de unidad. Simplemente utilizan una macro C que básicamente envuelve una afirmación y un cout. Algo como:

VERIFY(cond) if (!(cond)) {std::cout << "unit test failed at " << __FILE__ << "," << __LINE__; asserst(false)}

Luego simplemente creamos funciones para cada una de nuestras pruebas como

void CheckBehaviorYWhenXHappens()
{
    // a bunch of code to run the test
    //
    VERIFY(blah != blah2);
    // more VERIFY's as needed
}

Nuestro servidor CI recoge "error de prueba de unidad" y falla la compilación, enviando el mensaje por correo electrónico a los desarrolladores.

Y si tenemos un código de configuración duplicado, simplemente lo refactorizamos como lo haríamos con cualquier otro código duplicado que tendríamos en producción. Lo ajustamos detrás de las funciones auxiliares, hacemos que algunas clases de prueba terminen configurando escenarios de uso frecuente.

Sé que hay marcos como CppUnit y prueba de unidad de impulso. Me pregunto qué valor agregan estos. ¿Me estoy perdiendo lo que estos traen a la mesa? ¿Hay algo útil que pueda obtener de ellos? Dudo en agregar una dependencia a menos que agregue un valor real, especialmente porque parece que lo que tenemos es muy simple y funciona bien.

Doug T.
fuente

Respuestas:

8

Como otros ya han dicho, ya tienes tu propio marco simple y casero.

Parece trivial hacer uno. Sin embargo, hay algunas otras características de un marco de prueba de unidad que no son tan fáciles de implementar, ya que requieren un conocimiento avanzado del lenguaje. Las características que generalmente requiero de un marco de prueba y que no son tan fáciles de preparar son:

  • Recogida automática de casos de prueba. Es decir, definir un nuevo método de prueba debería ser suficiente para ejecutarlo. JUnit recopila automáticamente todos los métodos cuyos nombres comienzan con test, NUnit tiene la [Test]anotación, Boost.Test utiliza las macros BOOST_AUTO_TEST_CASEy BOOST_FIXTURE_TEST_CASE.

    Es sobre todo conveniencia, pero cada conveniencia que pueda obtener mejora la posibilidad de que los desarrolladores realmente escriban las pruebas que deberían y las conecten correctamente. Si tiene instrucciones largas, alguien perderá parte de ellas de vez en cuando y quizás algunas pruebas no se ejecutarán y nadie se dará cuenta.

  • Capacidad para ejecutar casos de prueba seleccionados, sin modificar el código y volver a compilar. Cualquier marco de prueba de unidad decente le permite especificar qué pruebas desea ejecutar en la línea de comandos. Si desea depurar en pruebas unitarias (es el punto más importante en ellas para muchos desarrolladores), debe poder seleccionar solo algunas para ejecutar, sin modificar el código por todas partes.

    Supongamos que acaba de recibir el informe de error # 4211 y se puede reproducir con la prueba unitaria. Entonces escribes uno, pero debes decirle al corredor que ejecute solo esa prueba, para que puedas depurar lo que realmente está mal allí.

  • Capacidad para marcar las fallas esperadas de las pruebas, por caso de prueba, sin modificar las comprobaciones mismas. De hecho, cambiamos los marcos en el trabajo para obtener este.

    Cualquier conjunto de pruebas de tamaño decente tendrá pruebas, que están fallando porque las características que prueban aún no se implementaron, aún no estaban terminadas, nadie tuvo tiempo de arreglarlas aún o algo así. Sin la capacidad de marcar las pruebas como fallas esperadas, no notará otra falla cuando las haya regularmente, por lo que las pruebas dejan de cumplir su objetivo principal.

Jan Hudec
fuente
gracias Creo que esta es la mejor respuesta. En este momento mi macro hace su trabajo, pero no puedo hacer ninguna de las funciones que mencionas.
Doug T.
1
@ Jan Hudec "Es sobre todo conveniencia, pero cada conveniencia que pueda obtener mejora la posibilidad de que los desarrolladores escriban las pruebas que deberían y las conecten correctamente"; Todos los marcos de prueba son (1) no triviales de instalar, a menudo tienen instrucciones de instalación más obsoletas o no exhaustivas que las instrucciones válidas actualizadas; (2) si se compromete con un marco de prueba directamente, sin una interfaz en el medio, está casado con él, cambiar los marcos no siempre es fácil.
Dmitry
@ Jan Hudec Si esperamos que más personas escriban pruebas unitarias, debemos tener más resultados en google para "¿Qué es una prueba unitaria?", Que "¿Qué es una prueba unitaria?". No tiene sentido hacer pruebas unitarias si no tiene idea de qué es una prueba unitaria independiente de cualquier marco de prueba unitaria o definición de prueba unitaria. No puede hacer pruebas unitarias a menos que comprenda bien lo que es una prueba unitaria, ya que de lo contrario no tiene sentido hacer pruebas unitarias.
Dmitry
No compro este argumento de conveniencia. Escribir código de prueba es muy difícil si abandonas el trivial mundo de los ejemplos. Todas estas maquetas, configuraciones, bibliotecas, programas de servidor de maquetas externas, etc. Todos requieren que conozca el marco de prueba de adentro hacia afuera.
Lothar
@Lothar, sí, todo es mucho trabajo y mucho que aprender, pero aún así tener que escribir repeticiones simples una y otra vez porque te faltan un par de utilidades útiles hace que el trabajo sea mucho menos placentero y eso hace una notable diferencia en la efectividad.
Jan Hudec
27

Parece que ya usas un framework, uno hecho en casa.

¿Cuál es el valor agregado de los marcos más populares? Diría que el valor que agregan es que cuando tiene que intercambiar código con personas ajenas a su empresa, puede hacerlo, ya que se basa en el marco conocido y ampliamente utilizado .

Un marco hecho en casa, por otro lado, te obliga a nunca compartir tu código o a proporcionar el marco en sí, lo que puede volverse engorroso con el crecimiento del marco en sí.

Si le da su código a un colega tal como está, sin explicación y sin marco de prueba de unidad, no podrá compilarlo.

Un segundo inconveniente de los marcos caseros es la compatibilidad . Los marcos de prueba de unidad populares tienden a garantizar la compatibilidad con diferentes IDE, sistemas de control de versiones, etc. Por el momento, puede que no sea muy importante para usted, pero ¿qué sucederá si algún día necesita cambiar algo en su servidor de CI o migrar? a un nuevo IDE o un nuevo VCS? ¿Reinventarás la rueda?

Por último, pero no menos importante, los marcos más grandes proporcionan más características que puede necesitar implementar en su propio marco algún día. Assert.AreEqual(expected, actual)No siempre es suficiente. ¿Qué pasa si necesita:

  • medir precisión?

    Assert.AreEqual(3.1415926535897932384626433832795, actual, 25)
    
  • prueba nula si se ejecuta durante demasiado tiempo? Reimplementar un tiempo de espera puede no ser sencillo incluso en lenguajes que facilitan la programación asincrónica.

  • probar un método que espera que se produzca una excepción?

  • tiene un código más elegante?

    Assert.Verify(a == null);
    

    está bien, pero ¿no es más expresivo tu intención de escribir la siguiente línea?

    Assert.IsNull(a);
    
Arseni Mourzenko
fuente
El "marco" que utilizamos está en un archivo de encabezado muy pequeño y sigue la semántica de afirmar. Así que no me preocupo demasiado por los inconvenientes que enumeras.
Doug T.
44
Considero que afirma la parte más trivial del marco de prueba. El corredor que recopila y ejecuta los casos de prueba y verifica los resultados es la parte importante no trivial.
Jan Hudec
@ Jan No entiendo del todo. Mi corredor es una rutina principal común a todos los programas de C ++. ¿Un corredor de prueba de unidad hace algo más sofisticado y útil?
Doug T.
1
Su marco solo permite la semántica de hacer valer y ejecutar pruebas en un método principal ... hasta ahora. Solo espere hasta que tenga que agrupar sus afirmaciones en múltiples escenarios, agrupar escenarios relacionados en función de los datos inicializados, etc.
James Kingsbery
@DougT .: Sí, el corredor de framework de prueba de unidad decente hace algunas cosas útiles más sofisticadas. Mira mi respuesta completa.
Jan Hudec
4

Como otros ya han dicho, usted ya tiene su propio marco de trabajo hecho en casa.

La única razón que puedo ver para usar algún otro marco de prueba sería desde el punto de vista del "conocimiento común" de la industria. Los nuevos desarrolladores no tendrían que aprender su forma casera (aunque parece muy simple).

Además, otros marcos de prueba pueden tener más funciones que podría aprovechar.

ozz
fuente
1
Convenido. Si no tiene limitaciones con su estrategia de prueba actual, veo pocas razones para cambiar. Un buen marco probablemente proporcionaría mejores capacidades de organización e informes, pero tendría que justificar el trabajo adicional requerido para integrarse con su base de código (incluido su sistema de compilación).
TMN
3

Ya tiene un marco, incluso si es simple.

Según veo, las principales ventajas de un marco más grande es la capacidad de tener muchos tipos diferentes de afirmaciones (como el aumento de afirmaciones), un orden lógico para las pruebas unitarias y la capacidad de ejecutar solo un subconjunto de pruebas unitarias en un momento. Además, el patrón de las pruebas de xUnit es bastante agradable de seguir si puede, por ejemplo, el setUP () y el tearDown (). Por supuesto, eso te encierra en dicho marco. Tenga en cuenta que algunos marcos tienen una mejor integración simulada que otros, por ejemplo, simulacro de Google y prueba.

¿Cuánto tiempo le llevará refactorizar todas sus pruebas unitarias a un nuevo marco? Los días o unas pocas semanas tal vez valen la pena, pero más tal vez no tanto.

Sardathrion - Restablece a Monica
fuente
2

A mi modo de ver, ambos tienen la ventaja y están en una "desventaja" (sic).

La ventaja es que tiene un sistema con el que se siente cómodo y que funciona para usted. Está contento de que confirme la validez de su producto, y probablemente no encuentre ningún valor comercial al intentar cambiar todas sus pruebas para algo que use un marco diferente. Si puede refactorizar su código y sus pruebas recogen los cambios, o mejor aún, si puede modificar sus pruebas y su código existente no pasa las pruebas hasta que se refactorice, entonces tendrá todas sus bases cubiertas. Sin embargo...

Una de las ventajas de tener una API de prueba de unidad bien diseñada es que hay una gran cantidad de soporte nativo en la mayoría de los IDE modernos. Esto no afectará a los usuarios de VI y emacs de núcleo duro que se burlan de los usuarios de Visual Studio, pero para aquellos que usan un buen IDE, tienen la capacidad de depurar sus pruebas y ejecutarlas dentro El IDE mismo. Esto es bueno, sin embargo, hay una ventaja aún mayor dependiendo del marco que use, y eso está en el lenguaje utilizado para probar su código.

Cuando digo lenguaje , no estoy hablando de un lenguaje de programación, sino que estoy hablando de un conjunto de palabras enriquecidas envueltas en una sintaxis fluida que hace que el código de prueba se lea como una historia. En particular, me he convertido en un defensor del uso de los marcos BDD . Mi API favorita de DotNet BDD es StoryQ, pero hay varios otros con el mismo propósito básico, que es sacar un concepto de un documento de requisitos y escribirlo en código de forma similar a como está escrito en la especificación. Sin embargo, las API realmente buenas van aún más lejos, interceptando cada declaración individual dentro de una prueba e indicando si esa declaración se ejecutó con éxito o falló. Esto es increíblemente útil, ya que puede ver toda la prueba ejecutada sin regresar antes, lo que significa que sus esfuerzos de depuración se vuelven increíblemente eficientes, ya que solo necesita enfocar su atención en las partes de la prueba que fallaron, sin necesidad de decodificar toda la llamada secuencia. La otra cosa buena es que el resultado de la prueba muestra toda esta información,

Como ejemplo de lo que estoy hablando, compare lo siguiente:

Usando afirmaciones:

Assert(variable_A == expected_value_1); // if this fails...
Assert(variable_B == expected_value_2); // ...this will not execute
Assert(variable_C == expected_value_3); // ...and nor will this!

Usando una API BDD fluida: (Imagine que los bits en cursiva son básicamente punteros de método)

WithScenario("Test Scenario")
    .Given(*AConfiguration*) // each method
    .When(*MyMethodToTestIsCalledWith*, variable_A, variable_B, variable_C) // in the
    .Then(*ExpectVariableAEquals*, expected_value_1) // Scenario will
        .And(*ExpectVariableBEquals*, expected_value_2) // indicate if it has
        .And(*ExpectVariableCEquals*, expected_value_3) // passed or failed execution.
    .Execute();

Ahora que la sintaxis BDD es más larga y más amplia, y estos ejemplos son terriblemente artificiales, sin embargo, para situaciones de prueba muy complejas en las que muchas cosas están cambiando en un sistema como resultado de un comportamiento determinado del sistema, la sintaxis BDD le ofrece una clara descripción sobre lo que está probando y cómo se ha definido su configuración de prueba, y puede mostrar este código a un no programador y ellos comprenderán instantáneamente lo que está sucediendo. Además, si "variable_A" falla la prueba en ambos casos, el ejemplo Asserts no se ejecutará más allá de la primera afirmación hasta que haya solucionado el problema, mientras que la API de BDD ejecutará todos los métodos llamados en la cadena, a su vez, e indicará qué partes individuales de la declaración estaban en error.

Personalmente, creo que este enfoque funciona mucho mejor que los marcos de xUnit más tradicionales en el sentido de que el lenguaje de prueba es el mismo lenguaje que sus clientes hablarán de sus requisitos lógicos. Aun así, me las arreglé para usar los marcos xUnit en un estilo similar sin necesidad de inventar una API de prueba completa para respaldar mis esfuerzos, y aunque las afirmaciones aún se cortocircuitarán, leerán más limpiamente. Por ejemplo:

Usando Nunit :

[Test]
void TestMyMethod()
{
    const int theExpectedValue = someValue;

    GivenASetupToTestMyMethod();

    var theActualValue = WhenIExecuteMyMethodToTest();

    Assert.That(theActualValue, Is.EqualTo(theExpectedValue)); // nice, but it's not BDD
}

Si decide explorar utilizando una API de prueba unitaria, mi consejo es experimentar con un gran número de API diferentes durante un tiempo, y mantener una actitud abierta sobre su enfoque. Si bien defiendo personalmente BDD, sus propias necesidades comerciales pueden requerir algo diferente para las circunstancias de su equipo. Sin embargo, la clave es evitar adivinar su sistema existente. Siempre puede admitir sus pruebas existentes con algunas pruebas usando otra API si es necesario, pero ciertamente no recomendaría una gran reescritura de prueba solo para hacer que todo sea igual. A medida que el código heredado deja de usarse, puede reemplazarlo fácilmente y sus pruebas con un código nuevo, y pruebas usando una API alternativa, y esto sin necesidad de invertir en un esfuerzo importante que no necesariamente le dará ningún valor comercial real. En cuanto al uso de una API de prueba unitaria,

S.Robins
fuente
1

Lo que tienes es simple y hace el trabajo. Si te funciona, genial. No necesita un marco de prueba de unidad convencional, y dudaría en llevar a cabo el trabajo de portar una biblioteca existente de pruebas de unidad a un nuevo marco. Creo que el mayor valor de los marcos de pruebas unitarias es reducir la barrera de entrada; simplemente comienza a escribir pruebas, porque el marco ya está en su lugar. Ya pasó ese punto, por lo que no obtendrá ese beneficio.

El otro beneficio de usar un marco general, y es un beneficio menor, en mi opinión, es que los nuevos desarrolladores ya pueden estar al día en cualquier marco que esté utilizando, por lo que requerirán menos capacitación. En la práctica, con un enfoque directo como el que ha descrito, esto no debería ser un gran problema.

Además, la mayoría de los frameworks convencionales tienen ciertas características que su framework puede o no tener. Estas características reducen el código de plomería y hacen que sea más rápido y fácil escribir casos de prueba:

  • Ejecución automática de casos de prueba, mediante convenciones de nomenclatura, anotaciones / atributos, etc.
  • Varias afirmaciones más específicas, para que no tenga que escribir lógica condicional para todas sus aserciones o capturar excepciones para afirmar su tipo.
  • Categorización de casos de prueba, para que pueda ejecutar fácilmente subconjuntos de ellos.
Adam Jaskiewicz
fuente