1.2. Stubs

Los stubs son dobles de test que tienen un comportamiento prefijado que siempre será el mismo. Dicho de otra manera: los métodos de un stub devuelven una respuesta preprogramada y no calculada. Se podría decir que un dummie es un stub que devuelve null, aunque a veces sí queremos simular que un objeto devuelve null como respuesta válida.

Es decir, los stubs simulan comportamientos posibles de un colaborador de nuestra unidad bajo test. Estos comportamientos pueden ser:

  • Lanza una excepción, simulando que ha ocurrido algo malo.
  • Devuelve algún tipo de respuesta válida, simulando que todo ha ido bien y el colaborador puede entregar su respuesta.

Esto nos permite crear un test de nuestra unidad bajo test que verifique su comportamiento cuando el colaborador falla, lanzando una excepción (si debe relanzarla, si debe devolver una respuesta especificada, etc), y también cuando el colaborador devuelve una respuesta válida. Además, en este caso, podría devolver diversos tipos de respuestas válidas.

Pero es importante insistir en que la respuesta del stub no puede ni debe ser calculada porque introduciría un factor de indeterminación en el test.

Stub de una respuesta válida

Veamos aquí un ejemplo de un stub de un objeto, el cual hemos iniciado como dummy en el setUp que he mostrado más arriba:

1 /** @var Contract | MockObject $contract */
2 $contract = $this->createMock(Contract::class);
3 $this->contractRepository
4     ->method('byId')
5     ->willReturn($contract);

En este caso, hacemos un stub del método byId del colaborador ContractRepository de modo que devuelve un objeto Contract (en este caso un dummy).

Con esto podemos verificar el comportamiento de nuestra unidad bajo test cuando ContractRepository le devuelve el contrato solicitado mediante su id.

Stub de que se produce una excepción

Si queremos testear que la unidad bajo test reacciona de forma correcta en el caso de que no se encuentre el contrato, simularemos que se lanza una excepción:

1 $this->contractRepository
2     ->method('byId')
3     ->willThrowException(new ContractNotFoundException());

Stub de “ida y vuelta”

¿Y qué ocurre si el colaborador tiene que recibir datos de la unidad bajo test y devolverlos transformados? ¿No deberíamos tener algún modo de hacer que el stub devuelva una respuesta adecuada al dato recibido?

La respuesta a esta pregunta es un no rotundo.

Eso implicaría añadir alguna lógica en el doble y, como decíamos antes, se introduciría una indeterminación en el resultado del test: ¿cómo podemos asegurar que esa lógica para decidir la respuesta del doble no está afectando al comportamiento que queremos testear?

Además, eso introduce un acoplamiento del test a algo que está completamente fuera del código.

Entonces, ¿qué es lo que testeamos en ese caso? Pues en ese caso, estaríamos testeando la manera en que la unidad bajo test orquesta o coordina a sus colaboradores, y el comportamiento del colaborador estaría verificado por sus propios tests.

Supongamos que estamos programando un juego en el que el jugador puede utilizar objetos que aumentan su fuerza. Por tanto, la lógica del método obtiene un dato del propio jugador, lo pasa a un colaborador (el objeto que aumenta la fuerza) y este lo devuelve transformado. Algo así:

1 public function useObject(StrengthImprover $strengthImprover)
2 {
3     $this->strength = $strengthImprover->use($this->strength);
4 }

En el test:

 1 $player = new Player();
 2 
 3 $strengthImprover = $this->createMock(StrengthImprover);
 4 $strengthImprover
 5     ->method('use')
 6     ->willReturn(10);
 7 
 8 $player->useObject($strengthImprover);
 9 
10 $this->assertEquals(10, $player->strength());

Nos da exactamente igual la fuerza que tenga el jugador o la que reciba. Lo que testeamos aquí es (a) que se usa el objeto y (b) que el valor que devuelve es asignado a la propiedad adecuada. Si el objeto multiplicaba o añadía fuerza, es algo que estará en sus propios tests.