¿Es una mala práctica que las pruebas unitarias dependan unas de otras?

9

Digamos que tengo algún tipo de pruebas unitarias como esta:

let myApi = new Api();

describe('api', () => {

  describe('set()', () => {
    it('should return true when setting a value', () => {
      assert.equal(myApi.set('foo', 'bar'), true);
    });
  });

  describe('get()', () => {
    it('should return the value when getting the value', () => {
      assert.equal(myApi.get('foo'), 'bar');
    });
  });

});

Así que ahora tengo 2 pruebas unitarias. Uno establece un valor en una API. El otro prueba para asegurarse de que se devuelve el valor adecuado. Sin embargo, la segunda prueba depende de la primera. ¿Debo agregar un .set()método en la segunda prueba antes get()con el único propósito de asegurarme de que la segunda prueba no dependa de nada más?

Además, en este ejemplo, ¿debería crear instancias myApipara cada prueba en lugar de hacerlo una vez antes de las pruebas?

Jake Wilson
fuente

Respuestas:

15

Sí, es una mala práctica. Las pruebas unitarias deben ejecutarse independientemente unas de otras, por las mismas razones por las que necesita cualquier otra función para ejecutarse independientemente: puede tratarla como una unidad independiente.

¿Debo agregar un método .set () en la segunda prueba antes de get () con el único propósito de asegurarme de que la segunda prueba no dependa de nada más?

Si. Sin embargo, si estos son solo métodos de captación y configuración, no contienen ningún comportamiento, y realmente no debería necesitar probarlos, a menos que tenga la reputación de manipular cosas gordas de tal manera que el captador / setter compila pero establece u obtiene el campo incorrecto.

Robert Harvey
fuente
En mi ejemplo, digamos que myApies un objeto instanciado. ¿Debo reinstalar myApien cada prueba unitaria? O es que reutilizarlo entre pruebas tiene el potencial de causar que la prueba dé falsos positivos, etc. Y sí, mi ejemplo es una cosa simplificada de captador / definidor, pero en realidad obviamente sería mucho más complicado.
Jake Wilson el
1
@JakeWilson Hay un libro llamado Pragmatic Unit Testing que analiza los conceptos básicos de las pruebas unitarias, como cómo evitar que las pruebas interfieran entre sí, etc.
rwong
Otro ejemplo: si usa JUnit, el orden de su función no está definido por el orden en que las escribió en la clase. @JakeWilson Si su api no tiene estado, puede reutilizarla, si no la reinstala.
Walfrat
2

Intente seguir la estructura de organizar-actuar-afirmar para cada prueba.

  1. Organice sus objetos, etc. y póngalos en un estado conocido (un accesorio de prueba). A veces, esta fase incluye afirmaciones para mostrar que de hecho estás en el estado en el que crees que estás.
  2. Actúa, es decir: realiza el comportamiento que estás probando.
  3. Afirma que obtuviste el resultado esperado.

Sus pruebas no se molestan en crear un estado conocido primero, por lo que no tienen sentido de forma aislada.

Además, las pruebas unitarias no necesariamente prueban un solo método: las pruebas unitarias deberían probar una unidad. Por lo general, esta unidad es una clase. Algunos métodos como get()solo tienen sentido en combinación con otro.

Probar getters y setters es sensato, en particular en lenguajes dinámicos (solo para asegurarse de que realmente están allí). Para probar un captador, primero debemos proporcionar un valor conocido. Esto puede suceder a través del constructor o de un instalador. O visto de otra manera: probar el getter está implícito en las pruebas del setter y del constructor. Y el setter, ¿siempre regresa true, o solo cuando se cambió el valor? Esto podría llevar a las siguientes pruebas (pseudocódigo):

describe Api:

  it has a default value:
    // arrange
    api = new Api()
    // act & assert
    assert api.get() === expected default value

  it can take custom values:
    // arrange & act
    api = new Api(42)
    // act & assert
    assert api.get() === 42

  describe set:

    it can set new values:
      // arrange
      api = new Api(7)
      // act
      ok = api.set(13)
      // assert
      assert ok === true:
      assert api.get() === 13

    it returns false when value is unchanged:
      // arrange
      api = new Api(57)
      // act
      ok = api.set(57)
      // assert
      assert ok === false
      assert api.get() === 57

Reutilizar el estado de una prueba anterior haría que nuestras pruebas fueran bastante frágiles. Reordenar las pruebas o cambiar el valor exacto en una prueba puede hacer que las pruebas aparentemente no relacionadas fallen. Asumir un estado específico también puede ocultar errores si hace que se pasen pruebas que en realidad deberían fallar. Para evitar esto, algunos corredores de prueba tienen opciones para ejecutar los casos de prueba en un orden aleatorio.

Sin embargo, hay casos en los que reutilizamos el estado proporcionado por la prueba anterior. En particular, cuando crear un dispositivo de prueba lleva mucho tiempo, combinamos muchos casos de prueba en un conjunto de pruebas. Si bien estas pruebas son más frágiles, aún podrían ser más valiosas ahora porque pueden realizarse más rápidamente y con mayor frecuencia. En la práctica, la combinación de pruebas es deseable cuando las pruebas involucran un componente manual, cuando se necesita una gran base de datos o cuando se prueban máquinas de estado.

amon
fuente