¿Archivos únicos o múltiples para la unidad que prueba una sola clase?

20

Al investigar las mejores prácticas de pruebas unitarias para ayudar a elaborar pautas para mi organización, me he preguntado si es mejor o útil separar los accesorios de prueba (clases de prueba) o mantener todas las pruebas para una sola clase en un archivo.

Fwiw, me refiero a las "pruebas unitarias" en el sentido puro de que son pruebas de caja blanca dirigidas a una sola clase, una afirmación por prueba, todas las dependencias burladas, etc.

Un escenario de ejemplo es una clase (llámela Documento) que tiene dos métodos: CheckIn y CheckOut. Cada método implementa varias reglas, etc. que controlan su comportamiento. Siguiendo la regla de una afirmación por prueba, tendré varias pruebas para cada método. Puedo colocar todas las pruebas en una sola DocumentTestsclase con nombres como CheckInShouldThrowExceptionWhenUserIsUnauthorizedy CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

O podría tener dos clases de prueba separadas: CheckInShouldy CheckOutShould. En este caso, mis nombres de prueba se acortarían, pero se organizarían para que todas las pruebas de un comportamiento (método) específico estén juntas.

Estoy seguro de que hay ventajas y desventajas para abordar y me pregunto si alguien ha seguido la ruta con varios archivos y, de ser así, ¿por qué? O, si ha optado por el enfoque de archivo único, ¿por qué cree que es mejor?

Hijo de pirata
fuente
2
Los nombres de sus métodos de prueba son demasiado largos. Simplifíquelos, incluso si eso significa que un método de prueba contendría múltiples aserciones.
Bernard
12
@Bernard: se considera una mala práctica tener múltiples afirmaciones por método; sin embargo, los nombres largos de métodos de prueba NO se consideran una mala práctica. Usualmente documentamos lo que el método está probando en el nombre mismo. Por ejemplo constructor_nullSomeList (), setUserName_UserNameIsNotValid () etc ...
c_maker
44
@Bernard: también uso nombres largos de prueba (¡y solo allí!). Los amo, porque está muy claro lo que no estaba funcionando (si sus nombres son buenas opciones;)).
Sebastian Bauer
Las afirmaciones múltiples no son una mala práctica, si todas prueban el mismo resultado. P.ej. no lo hubieras hecho testResponseContainsSuccessTrue(), testResponseContainsMyData()y testResponseStatusCodeIsOk(). Como quieren que ellos en una sola testResponse()que tiene tres afirma: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)yassertEquals(true, response.success)
Juha Untinen

Respuestas:

18

Es raro, pero a veces tiene sentido tener múltiples clases de prueba para una clase determinada bajo prueba. Por lo general, haría esto cuando se requieran diferentes configuraciones y se compartan en un subconjunto de las pruebas.

Carl Manaster
fuente
3
Buen ejemplo de por qué ese enfoque me fue presentado en primer lugar. Por ejemplo, cuando quiero probar el método CheckIn, siempre quiero que el objeto bajo prueba se configure de una manera, pero cuando pruebo el método CheckOut, necesito una configuración diferente. Buen punto.
SonOfPirate
14

Realmente no puedo ver ninguna razón convincente por la que dividiría una prueba para una sola clase en varias clases de prueba. Dado que la idea de conducir debe ser mantener la cohesión a nivel de clase, también debes esforzarte por lograrlo a nivel de prueba. Solo algunas razones aleatorias:

  1. No es necesario duplicar (y mantener múltiples versiones de) el código de configuración
  2. Es más fácil ejecutar todas las pruebas para una clase desde un IDE si están agrupadas en una clase de prueba
  3. Solución de problemas más fácil con mapeo uno a uno entre clases de prueba y clases.
papilla
fuente
Buenos puntos. Si la clase bajo prueba es un error terrible, no descartaría varias clases de prueba, donde algunas de ellas contienen lógica auxiliar. Simplemente no me gustan las reglas muy rígidas. Aparte de eso, grandes puntos.
Trabajo
1
Eliminaría el código de configuración duplicado al tener una clase base común para los dispositivos de prueba que funcionan en una sola clase. No estoy seguro de estar de acuerdo con los otros dos puntos.
SonOfPirate
En JUnit, por ejemplo, es posible que desee probar cosas usando diferentes Runners, lo que lleva a la necesidad de diferentes clases.
Joachim Nilsson
Mientras escribo una clase con una gran cantidad de métodos con matemáticas complejas en cada uno, encuentro que tengo 85 exámenes y hasta ahora solo he escrito exámenes para el 24% de los métodos. Me encuentro aquí porque me preguntaba si es una locura dividir esta cantidad masiva de pruebas en categorías separadas de alguna manera para poder ejecutar subconjuntos.
Mooing Duck
7

Si se ve obligado a dividir las pruebas unitarias para una clase en varios archivos, eso puede ser una indicación de que la clase en sí está mal diseñada. No puedo pensar en ningún escenario en el que sería más beneficioso dividir las pruebas unitarias para una clase que se adhiera razonablemente al Principio de Responsabilidad Única y otras mejores prácticas de programación.

Además, tener nombres de métodos más largos en las pruebas unitarias es aceptable, pero si le molesta, siempre puede repensar la convención de nomenclatura de la prueba unitaria para acortar los nombres.

PescadoCanasta
fuente
Por desgracia, en el mundo real, no todas las clases se adhieren a SRP et al ... y esto se convierte en un problema cuando se está probando un código heredado.
Péter Török
3
No estoy seguro de cómo SRP se relaciona aquí. Tengo varios métodos en mi clase y necesito escribir pruebas unitarias para todos los diferentes requisitos que impulsan su comportamiento. ¿Es mejor tener una clase de prueba con 50 métodos de prueba o 5 clases de prueba con 10 pruebas cada una que se relacionen con un método específico en la clase bajo prueba?
SonOfPirate
2
@SonOfPirate: Si necesita tantas pruebas, eso podría significar que sus métodos están haciendo demasiado. Entonces SRP es muy relevante aquí. Si aplica SRP a clases y métodos, tendrá muchas clases pequeñas y métodos que son fáciles de probar y no necesitará tantos métodos de prueba. (Pero estoy de acuerdo con Péter Török en que cuando prueba el código heredado, las buenas prácticas están fuera de la puerta y solo tiene que hacer lo mejor que pueda con lo que se le da ...)
c_maker
El mejor ejemplo que puedo dar es el método CheckIn, donde tenemos reglas comerciales que determinan quién tiene permiso para realizar la acción (autorización), si el objeto está en el estado adecuado para ser registrado, como si está desprotegido y si hay cambios. ha sido hecho. Tendremos pruebas unitarias que verifiquen que se aplican las reglas de autorización, así como pruebas para verificar que el método se comporta correctamente si se llama a CheckIn cuando el objeto no se desprotege, no se cambia, etc. Es por eso que terminamos con muchos pruebas ¿Estás sugiriendo que esto está mal?
SonOfPirate
No creo que haya respuestas difíciles y rápidas, correctas o incorrectas sobre este tema. Si lo que estás haciendo funciona para ti, eso es genial. Esta es solo mi perspectiva sobre una guía general.
FishBasketGordo
2

Uno de mis argumentos en contra de separar las pruebas en varias clases es que se hace más difícil para otros desarrolladores en el equipo (especialmente aquellos que no son expertos en pruebas) localizar las pruebas existentes ( Gee, me pregunto si ya hay una prueba para esto ¿Me pregunto dónde estaría? ) y también dónde poner nuevas pruebas ( voy a escribir una prueba para este método en esta clase, pero no estoy muy seguro de si debo ponerlas en un archivo nuevo o en uno existente uno? )

En el caso de que diferentes pruebas requieran configuraciones drásticamente diferentes, he visto a algunos practicantes de TDD colocar el "accesorio" o la "configuración" en diferentes clases / archivos, pero no las pruebas en sí.

rally25rs
fuente
1

Desearía poder recordar / encontrar el enlace donde se demostró por primera vez la técnica que he elegido adoptar. Esencialmente creo una sola clase abstracta para cada clase bajo prueba que contiene accesorios de prueba anidados (clases) para cada miembro que se está probando. Esto proporciona la separación originalmente deseada pero mantiene todas las pruebas en el mismo archivo para cada ubicación. Además, esto genera un nombre de clase que permite agrupar y ordenar fácilmente en el corredor de prueba.

Aquí hay un ejemplo de cómo se aplica este enfoque a mi escenario original:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Esto resulta en las siguientes pruebas que aparecen en la lista de pruebas:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

A medida que se agregan más pruebas, se agrupan y clasifican fácilmente por nombre de clase, lo que también mantiene todas las pruebas para la clase bajo prueba listadas juntas.

Otra ventaja de este enfoque que he aprendido a aprovechar es la naturaleza orientada a objetos de esta estructura, lo que significa que puedo definir el código dentro de los métodos de inicio y limpieza de la clase base DocumentTests que serán compartidos por todas las clases anidadas, así como dentro de cada clase anidada para las pruebas que contiene.

Hijo de pirata
fuente
1

Intenta pensar al revés por un minuto.

¿Por qué tendrías solo una clase de prueba por clase? ¿Estás haciendo una prueba de clase o una prueba de unidad? ¿Incluso depende de las clases ?

Se supone que una prueba unitaria está probando un comportamiento específico, en un contexto específico. El hecho de que su clase ya tenga un CheckInmedio significa que debe haber un comportamiento que lo necesite en primer lugar.

¿Cómo pensarías sobre este pseudocódigo?

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Ahora no estás probando directamente el checkInmétodo. En su lugar, está probando un comportamiento (que es afortunadamente;)), y si necesita refactorizarlo Documenty dividirlo en diferentes clases, o fusionar otra clase en él, sus archivos de prueba siguen siendo coherentes, todavía tienen sentido ya que nunca estás cambiando la lógica al refactorizar, solo la estructura.

Un archivo de prueba para una clase simplemente hace que sea más difícil refactorizar cada vez que lo necesite, y las pruebas también tienen menos sentido en términos de dominio / lógica del código en sí.

Steve Chamaillard
fuente
0

En general, recomendaría pensar en las pruebas como pruebas de un comportamiento de la clase en lugar de un método. Es decir, algunas de sus pruebas pueden necesitar llamar a ambos métodos en la clase para evaluar el comportamiento esperado.

Por lo general, comienzo con una clase de prueba unitaria por clase de producción, pero eventualmente puedo dividir esa clase de prueba unitaria en varias clases de prueba según el comportamiento que prueban. En otras palabras, recomendaría no dividir la clase de prueba en CheckInShould y CheckOutShould, sino dividir por el comportamiento de la unidad bajo prueba.

Christian Horsdal
fuente
0

1. Considere qué es probable que salga mal

Durante el desarrollo inicial, usted sabe bastante bien lo que está haciendo y ambas soluciones probablemente funcionarán bien.

Se vuelve más interesante cuando una prueba falla después de un cambio mucho más tarde. Hay dos posibilidades:

  1. Te has roto gravemente CheckIn(o CheckOut). Nuevamente, tanto la solución de un solo archivo como la de dos archivos están bien en ese caso.
  2. Has modificado ambos CheckIny CheckOut(y quizás sus pruebas) de una manera que tenga sentido para cada uno de ellos, pero no para los dos juntos. Has roto la coherencia de la pareja. En ese caso, dividir las pruebas en dos archivos dificultará la comprensión del problema.

2. Considere para qué pruebas se utilizan

Las pruebas tienen dos propósitos principales:

  1. Compruebe automáticamente que el programa aún funciona correctamente. Si las pruebas están en uno o varios archivos no importa para este propósito.
  2. Ayuda a un lector humano a entender el programa. Esto será mucho más fácil si las pruebas de funcionalidad estrechamente relacionadas se mantienen juntas, porque ver su coherencia es un camino rápido hacia la comprensión.

¿Entonces?

Ambas perspectivas sugieren que mantener las pruebas juntas puede ayudar, pero no perjudicará.

Lutz Prechelt
fuente