Keyvan Akbary Seguir

Keyvan Akbary

Tech Lead en TransferWise

Autor del libro

Saber más

Test doubles

De forma análoga al uso de dobles en Hollywood, los test doubles son un término genérico que hace referencia a cualquier caso en el que se reemplaza un objeto de producción con otro con el único objetivo de probar el código.

Imaginemos que queremos probar una parte nuestro sistema que depende de la siguiente interfaz

interface Authorizer {
    public function authorize(string $user, string $pass): bool;
}

Dependiendo del contexto y la intención del test disponemos de una buena variedad de dobles para satisfacer dicha dependencia.

Dummy

class DummyAuthorizer implements Authorizer {
    public function authorize(string $user, string $pass): bool {
    }
}

Un objeto Dummy es algo que se utiliza para satisfacer dependencias, su uso en ejecución es completamente irrelevante.

class System {
    private $authorizer;

    public function __construct(Authorizer $authorizer) {
        $this->authorizer = $authorizer;
    }

    public function loginCount(): int {
        //some logic count calculation...
        return 0;
    }
}
class SystemTest extends TestCase {
    /**
     * @test
     */
    public function newlyCreatedSystemHasNoLoggedInUsers(): void {
        $system = new System(new DummyAuthorizer());

        $this->assertThat($system->loginCount(), $this->equalTo(0));
    }
}

Aunque en este test en concreto no se haga uso explícito de Authorizer, es necesario satisfacer la dependencia para poder construir System. El método authorize no se ejecutará dado que en este test nadie va a iniciar sesión. Por eso no es un problema que dicho método no devuelva nada. Si alguien lo utiliza, la ejecución se romperá y eso es lo que queremos, un Dummy no debería usarse en ejecución.

Stub

Imaginemos que ahora queremos probar una parte del sistema que requiere de haber iniciado sesión. No queremos utilizar la lógica de un autentificador real, nuestro propósito es probar únicamente la parte del sistema que utiliza el login, no el propio login. Hacerlo incrementaría el acoplamiento con el código y por tanto aumentaría la fragilidad de nuestro sistema. Un fallo en el login rompería el test aunque no hubiese cambiado la lógica de negocio. Además en muchos casos, las dependencias son complejas y no queremos depender de setups largos y lentos.

class AcceptingAuthorizerStub implements Authorizer {
    public function authorize(string $user, string $pass): bool {
        return true;
    }
}

El propósito de un Stub es el de proveer valores concretos para guiar al test en una determinada dirección.

De la misma forma, si queremos probar una parte del sistema a cargo de usuarios no autorizados podemos hacer que el Stub devuelva false.

Spy

Cuando quieras asegurarte de haber llamado a un método en tu sistema puedes utilizar un espía.

class AcceptingAuthorizerSpy implements Authorizer {
    public $authorizeWasCalled = false;

    public function authorize(string $user, string $pass): bool {
        $this->authorizeWasCalled = true;
        return true;
    }
}

Comprobarlo es tan sencillo como preguntar a nuestro espía si se ha llamado al método en cuestión en la fase de aserción de nuestro test.

Hay que tener cuidado, espiar al que te llama tiene un coste y se paga en forma de acoplamiento. Cuanto más espías, más te acoplas a la implementación y más frágiles serán tus tests.

Mock

class AcceptingAuthorizerVerificationMock implements Authorizer {
    public $authorizeWasCalled = false;

    public function authorize(string $user, string $pass): bool {
        $this->authorizeWasCalled = true;
        return true;
    }

    public function verify(): bool {
        return $this->authorizeWasCalled;
    }
}

Un mock conoce lo que se se está testeando. Si te fijas, se ha movido la fase de verificación del test al mock. Al contrario que un Stub, un Mock no está tan interesado en devolver valores concretos. Un mock esta más interesado en que métodos se han invocado, con que argumentos, cuando y con que frecuencia. Un mock siempre es un espía.

Fake

class AcceptingAuthorizerFake implements Authorizer {
    public function authorize(string $user, string $pass): bool {
        return $user === 'Bob';
    }
}

Únicamente los usuarios con nombre de usuario “Bob” serán autorizados. Puedes hacer que un Fake se comporte de forma diferente según los datos que envíes. Es una especie de simulador.

Un Fake no es un Stub dado que los Stubs no tienen lógica de negocio en ellos. Podríamos decir que en cierta manera un Mock es un espía, un espía es un tipo de Stub y un Stub es algo parecido a un Dummy. Un Fake no se parece a ninguno de ellos. Un Fake contiene cierto tipo lógica y puede complicarse hasta el punto de llegar a necesitar tests.

Un ejemplo típico de Fake son las InMemoryTestDatabase.

Librerías de Mocks

Los Dummies, Stubs y espías son sencillos de escribir, especialmente si cuentas con un IDE moderno. Por otro lado, escribir Mocks no lo es tanto. Librerías como PHPUnit o Mockery diluyen esta dificultad en un mar de adictivas DSLs. La facilidad con la que puedes escribir Mocks con ellas es un arma de doble filo que te puede hacer perder la visión del precio de añadirlos.

Añadir más aserciones o espiar en la llamada tiene un coste mas elevado que el de la propia implementación, comunmente lo denominamos acoplamiento. Cada uno de los test doubles tiene un propósito y un contexto, a más detalle, mayor coste, mayor fragilidad.

Referencias

¿Ves algo raro? ¡Editame!

Copyright © 2022