¿Por qué usamos burlas para pruebas unitarias? y el uso de test-dobles - Actualizado 2022

¿Por qué usamos burlas para pruebas unitarias? y el uso de test-dobles - Actualizado 2022

Para comprender cómo y por qué usamos simulacros para las pruebas, debemos comprender los diferentes tipos de pruebas dobles (implementaciones utilizadas para las pruebas) y qué son las pruebas unitarias. Comenzaremos con pruebas unitarias y luego pasaremos a diferentes tipos de pruebas dobles, con ejemplos.

En la forma más pura, las pruebas unitarias son pruebas para una unidad, qué tan grande o pequeña es una unidad, está en debate. A menudo se considera una clase, pero también podría considerarse solo un método. Sin embargo, en la programación orientada a objetos, a menudo usamos una clase, ya que una clase puede tener estado, para que podamos encontrar fallas en la clase, es posible que tengamos que llamar a varios métodos uno tras otro. Por ejemplo, para probar una clase de Lista, primero tendrá que agregar algo a la lista, antes de poder probar la funcionalidad de eliminación. Cuando se escriben pruebas unitarias, es importante no probar varias unidades a la vez, lo que significa que las unidades que trabajan juntas o están estrechamente acopladas deben estar fuera de consideración. Estas son pruebas de integración, no pruebas unitarias, las pruebas de integración tienen como objetivo probar varios componentes juntos, mientras que las pruebas unitarias son pruebas de una unidad aislada. El aislamiento puede ser de otras clases, pero también puede ser de IO, Datases, llamadas API, etc. Las pruebas unitarias a menudo se desarrollan utilizando el desarrollo basado en pruebas (TDD) o algunos componentes de este. Esta publicación no cubrirá TDD. Recomiendo el libro de Kent Beck si te interesa este tema:

Dobles de prueba

Para probar nuestras unidades de forma aislada, necesitamos desacoplarlas (aislarlas). El desacoplamiento a menudo se logra mediante algún tipo de inyección de dependencia. Por ejemplo, el uso simple y antiguo de constructores, u otra forma de "establecer" una dependencia. Lo mejor de esto es que podemos crear implementaciones específicas de prueba (dobles de prueba). Con este enfoque, las dependencias se vuelven abstractas y hacen lo que se les indica en la prueba dada.

A continuación se muestra un ejemplo de una implementación de stub. Para mis ejemplos, uso el lenguaje C# y el marco de pruebas unitarias Nunit, pero serán fáciles de leer si tiene experiencia en C++ o Java. Mi objetivo era simplificar mis ejemplos para que cualquier persona con experiencia en programación orientada a objetos pudiera leerlos. A continuación voy a crear una implementación muy pequeña de un juego de mesa:

public class BoardGame : IBoardGame
{
    private IDice _dice;

    public BoardGame(IDice dice)
    {
        _dice = dice;
    }

    public int RollDice()
    {
        return _dice.Roll();
    }
}

Hasta ahora lo único que puedes hacer en el BoardGame es tirar los dados. Esto se basa en una dependencia inyectada a través del BoardGame constructor. Para probar esto, hacemos una pequeña prueba para asegurarnos de que nuestro BoardGame devuelve cualquiera que sea el resultado de los dados:

[Test]
public void BoardGameReturns6WhenDiceReturns6()
{
    var boardGame = new BoardGame(new Always6DiceStub());
    Assert.AreEqual(6, boardGame.RollDice());
}

private class Always6DiceStub : IDice
{
    public int Roll()
    {
        return 6;
    }
}

En mi prueba anterior creo un new BoardGame objeto, luego inyecto un Always6DiceStub implementación (un doble de prueba de stub). Los stubs son pequeñas implementaciones que devuelven una respuesta codificada (enlatada), lo que los hace excelentes para esto. Si hubiera realizado una implementación que realmente arrojara un número aleatorio, habría tenido que afirmar un rango o mi prueba se volvería inestable debido a la aleatoriedad. El talón se asegura de que siempre recupere el número 6. No tengo ninguna otra implementación de mis dados que no sea el talón, puedo probar completamente mi BoardGame class sin implementaciones reales hasta el momento.

El siguiente método para mi BoardGame será el MovePlayer() método. Este método tomará un número como parámetro:el número obtenido y, para simplificar, lo moveremos tan lejos en el juego. Para esto presento el BoardMap , que hará un seguimiento de en qué posición se encuentran los diferentes jugadores. Pero por ahora solo hay un jugador:

private IDice _dice;
private IBoardMap _boardmap;

public BoardGame(IDice dice, IBoardMap boardmap)
{
    _dice = dice;
    _boardmap = boardmap;
}

public void MovePlayer(int spaces)
{
    _boardmap.MovePlayer(spaces);
}

Lo anterior es lo mismo BoardGame como antes. Pero con un nuevo método y dependencia para el BoardMap . Probablemente hayas notado que el MovePlayer() El método no devuelve nada. Entonces, ¿cómo probamos esto? Aquí es donde entra en juego el doble de la prueba de espionaje:

[Test]
public void BoardGameCanMoveSpaces()
{
    var boardMapSpy = new BoardMapSpy();
    var boardGame = new BoardGame(new DiceDummy(), boardMapSpy);
    boardGame.MovePlayer(2);
    boardGame.MovePlayer(5);
    boardGame.MovePlayer(3);
    Assert.AreEqual(10, boardMapSpy.SpacesMoved);
}

private class BoardMapSpy : IBoardMap
{
    public int SpacesMoved = 0;

    public void MovePlayer(int spaces)
    {
        SpacesMoved += spaces;
    }
}

private class DiceDummy : IDice
{
    public int Roll()
    {
        throw new NotImplementedException("Dummy implementation");
    }
}

Arriba, he creado un doble de prueba de espía para registrar lo que se envía al espía. Una prueba de espionaje registra dos veces la entrada y al final puede dar un informe sobre esto. Cada vez que me muevo, agrego al SpacesMoved variable y afirmar que la suma es correcta.

Todavía tengo un dado que necesita ser inyectado en el constructor. Para esto podría haber usado el valor null . Pero como no me gusta null Se podría haber requerido que los valores y la dependencia estuvieran allí, en lugar de usar null Creo una implementación ficticia. Que es otro doble de prueba. Este tipo de prueba doble no hace más que asegurarse de que cumplo con los contratos de mi código.

Así que ahora hemos usado tres tipos diferentes de dobles de prueba. El título de esta publicación tiene Mock en él. Cubriremos esto a continuación.

simulacros

A menudo uso el término "burla" en lugar de dobles de prueba. ¿Por qué? Porque uso un marco de simulación para casi todos mis dobles de prueba. Con un marco de simulación sólido, no tiene que crear los dobles de prueba anteriores. Un marco de simulación le permite crear simulacros, que es un tipo especial de prueba doble. Para esto usaré el framework NSubstitute, este es mi favorito pero hay muchos otros que pueden hacer lo mismo.

Revisaré los ejemplos anteriores y, en lugar de usar dobles de prueba, usaré simulacros:

[Test]
public void BoardGameReturns6WhenDiceReturns6WithMocks()
{
    var dice = Substitute.For<IDice>();
    dice.Roll().Returns(6);
    var boardGame = new BoardGame(dice);
    Assert.AreEqual(6, boardGame.RollDice());
}

Arriba está el mismo ejemplo que mi primera prueba. Sin embargo, en lugar de usar un código auxiliar, usamos un simulacro que actúa como código auxiliar. Se crea un simulacro (o un sustituto, como le gusta llamarlo a NSubstitute framework), luego se le indica que siempre devuelva seis cuando Roll() se llama, al igual que el stub anterior. A continuación, un nuevo BoardGame se crea y se inyecta el Mock de dados. Como antes del boardGame.Rolldice() Se llama al método y se afirma que devuelve seis. Ese fue un ejemplo de hacer un stub usando un marco de trabajo simulado, el siguiente es nuestro doble de prueba de espionaje:

[Test]
public void BoardGameCanMoveSpacesMock()
{
    var dice = Substitute.For<IDice>();
    var boardMap = Substitute.For<IBoardMap>();
    var boardGame = new BoardGame(new DiceDummy(), boardMap);
    boardGame.MovePlayer(2);
    boardGame.MovePlayer(5);
    boardGame.MovePlayer(3);
    boardMap.Received().MovePlayer(2);
    boardMap.Received().MovePlayer(5);
    boardMap.Received().MovePlayer(3);
}

Arriba está nuestra prueba usando un espía. Usando NSubstitute creo un simulacro del IBoardMap y luego proceder a darle los mismos valores que antes, y al final afirmar que recibió estas llamadas. También creo un sustituto de los dados para usar un maniquí, que no hace más que asegurarme de que puedo completar el constructor.

Así que ahora hemos reemplazado todos nuestros otros dobles de prueba con una contraparte simulada. ¿El código mejoró o empeoró? eso depende de la persona que escribe el código, a algunos les gustan los simulacros, a otros las implementaciones reales. Repasaré algunas ventajas y desventajas de la simulación frente a la creación de implementaciones específicas para la prueba.

Al usar simulacros, tendrá menos implementaciones en su base de código. Puede leer directamente en su prueba lo que hace su implementación. Pero, ¿esto realmente causa menos código? Puede guardar algunos corchetes, pero aún deberá definir qué se debe devolver o espiar para cada prueba. Algunos dicen que usar implementaciones reales se siente más nativo. Hay una curva de aprendizaje al introducir un marco de simulación. Si trabaja en un entorno de equipo, todo el equipo deberá poder comprender el marco (al menos tiene que ser legible). Esta es una inversión, como cualquier otra inversión en un marco determinado.

La burla es una herramienta poderosa y puedes hacer muchas cosas con ella. Muchos marcos son inmensos en características. Pero recuerda que siempre puedes hacer lo mismo usando una implementación real. He estado usando simulacros durante muchos años y todavía es lo que prefiero. Pero esto es solo cuando se trabaja con C#. Cuando codifico Java, por ejemplo, no conozco ninguna biblioteca simulada, por lo tanto, uso los otros tipos de dobles de prueba.

Tipos de dobles de prueba

Aquí repasaré los diferentes tipos de dobles de prueba y haré un breve resumen. Estos son los componentes básicos para crear excelentes pruebas unitarias. Algunas pruebas unitarias no necesitan pruebas dobles, por supuesto, ¡pero la mayoría sí! El término doble Test fue creado por Gerard Meszaros; puede leer más sobre él en su propio artículo. Esta es mi opinión al respecto:

  • Ficticio: Una implementación utilizada solo para cumplir un contrato. Como un constructor o método. En el caso de prueba dado, no se llama a la implementación ficticia.
  • Trozo: Una implementación con una respuesta integrada. A menudo se usa para probar un valor devuelto específico de una dependencia. Esto hace que sea más fácil evitar la aleatoriedad o tal vez obtener un código de error específico (que puede ser difícil de activar).
  • Espía: El espía registra todo lo que se le envía para que luego podamos asegurarnos de que hicimos las llamadas correctas. Esto se hace a menudo para asegurarse de que la dependencia se llama correctamente y en las condiciones adecuadas. El espía también puede hacer un informe sobre cómo se llamó. Lo que hace que el informe sea asertivo. A menudo se usa para métodos de anulación.
  • simulacro: Un simulacro se basa en un marco de burla. En lugar de crear implementaciones de Dummies, Stubs y Spies, podemos usar un simulacro. Por lo tanto, un simulacro puede ser cualquiera de los 3. Con algunos marcos, también puede hacer la mayoría de los dobles de prueba falsos. Pero en sí mismo, el simulacro también es un doble de prueba.
  • Falso: Una falsificación es una implementación parcial, y no se trató en mis ejemplos. A menudo se utiliza para simular sistemas de archivos, bases de datos, solicitudes y respuestas http, etc. No es un stub ya que tiene más lógica. Puede mantener el estado de lo que se le envía (insertado en la base de datos) y devolverlo a pedido.

Notas de cierre

Espero que ahora tenga una mejor comprensión de los simulacros y qué son los dobles de prueba. Los ejemplos que he dado en esta publicación son, por supuesto, muy simples . Pero creo que esta publicación muestra cómo se relacionan los simulacros y otros dobles de prueba.

Las pruebas unitarias que utilizan dobles de prueba nos permiten probar nuestro código de forma aislada, en condiciones que controlamos. Podemos abstraer cualquier estado, IO, bases de datos o similares usando dobles de prueba. Otra cosa con la que nos ayudan las pruebas unitarias es desacoplar nuestro código. Separando la responsabilidad de nuestras diferentes clases. Si desea leer más, le recomiendo los siguientes libros:

Divulgación :Tenga en cuenta que algunos de los enlaces en esta publicación son enlaces de afiliados y si los visita para realizar una compra, ganaré una comisión. Tenga en cuenta que vinculo estas empresas y sus productos por su calidad. La decisión es tuya, y si decides o no comprar algo depende completamente de ti.

Espero que te haya gustado la publicación, ¡déjame saber lo que piensas en los comentarios a continuación!