• No se han encontrado resultados

A veces, estamos testeando un componente que tiene dependencias con apis de        terceros o incluso se conecta a una base de datos para recuperar cierta        información. Pero, lo que realmente queremos testear es el comportamiento del        componente. Imaginemos que siempre que existen dependencias, realizamos una        conexión con la base de datos, en un proyecto pequeño el rendimiento de los        tests podría ser inapreciable, pero en un proyecto grande con cientos, incluso        miles de tests, la duración de todos ellos podría ser inmensa. Aquí es cuando        entran en juego los dobles de tests para simular dicho comportamiento que nos        permita centrarnos y testar solo lo que realmente necesitamos. Permiten        “engañar” al código para que se crea que colabora correctamente con otras clases,        es como si fueran los dobles de las películas para las escenas peligrosas.   

Existen los siguientes tipos de dobles ordenados de menor a mayor complejidad: 

dummy​: se usa cuando no nos importa cómo se colabora con este objeto. Por       

ejemplo, cuando sabemos que no se va a usar en absoluto. Lo necesitamos porque        nos interesa su interfaz pero no su implementación. La implementación de los        métodos de estos dobles no hacen nada y devuelven null. Normalmente, se usa        para rellenar una lista de parámetros.

class​ ​DummyRepositoryClass​ ​implements​ ​RepositoryClass​ {

@Override

​public​ String ​getHelloWorld​()​ {

​throw​ ​new​ RuntimeException(​"Not expected to be called"​); }

}

class​ ServiceTest​{

@Test

​public​ ​void​ ​example_dummy_test​()​ {

DummyRepositoryClas​s dummy = ​new​ ​DummyRepositoryClas​s();

ServiceClass​ myService = ​new​ ServiceClass​(dummy);

   

 

Se debe tener en cuenta que el uso de un framework de mocks también es una        alternativa al ejemplo anterior y suele ser más común. Si usamos, por ejemplo,        mockito, se haría de la siguiente forma: 

 

stub: es como un dummy pero que devuelve valores fijos distintos de null. Por       

ejemplo, un método de autenticación devolvería siempre true y así podríamos usar        este doble para probar todos los escenarios donde la autenticación ha sido        correcta, sin necesidad de hacer la llamada real. En el ejemplo anterior, el método       

getHelloWorld()​, podría devolver siempre la misma string, esto se consideraría       

un stub. Como vimos antes, esto también se podría hacer con un mock, donde        podremos especificar el valor que queremos que devuelva siempre. 

spy: es como un stub pero que espía a quien lo llama. Esto permite luego,       

comprobar el número de veces que se ha llamado al método, el número de        argumentos que se le pasan, etc. Estos dobles son peligrosos porque acoplan el        test con la implementación concreta, lo que provocará que si se cambia la        implementación, aunque no cambie el comportamiento, el test fallará. Son tests        frágiles, por lo que debemos evitarlos. 

Mockito nos ofrece el método verify(), que comprueba que se llama al método e        incluso el número de veces que ha debido ser invocado. Por ejemplo:  

  }

DummyRepositoryClass ​dummy​ = mock(​DummyRepositoryClass​.class); 

private​ ​final​ AuthenticationService spyAs = mock(AuthenticationService.​class​); ...

​when​(spyAs.isAuthenticated()).thenReturn(​true​); ...

   

En el código anterior se comprueba que efectivamente se ha llamado al método        isAuthenticated() una sola vez (el valor por defecto). Si quisiéramos comprobar        que se ha llamado tres veces: 

verify(spyAs, times(3)).isAuthenticated(); 

mock: es como un spy que sabe lo que está probando exactamente. Así, al propio       

mock, en la sección de aserciones, se le preguntará si ha ido bien o mal el test. El        mock sabe el comportamiento de cómo se debe llamar al doble, cuántas veces se        le ha llamado, con qué parámetros, etc. Es una de las formas más conocidas y        usadas hoy en día por los desarrolladores ya que ofrece múltiples opciones para        probar nuestro código. 

Vamos a ver con Mockito dos ejemplos sobre cómo especificar el resultado que        queremos que nos devuelva nuestro mock. Imaginemos que tenemos un servicio        que devuelve una lista de productos de una tienda. Esa lista es del tipo       

List<Product>. ​La primera forma que veremos a continuación es ​t​ype safe​, esto              quiere decir que tiene en cuenta el tipo devuelto y por tanto, nos saldría un error        en tiempo de compilación indicandonos que se espera una lista de productos y se        está devolviendo una string: 

 

La segunda forma no es ​type safe​, esto quiere decir que no tiene en cuenta el tipo              devuelto y por tanto, no nos saldría ningún error en tiempo de compilación pero sí        al ejecutar el test, provocando su fallo: 

 

fake: es un tipo totalmente distinto a los anteriores. Un fake implementa los       

métodos con lógica de negocio, es como un simulador que puede ser muy sencillo        o extremadamente complicado. Por ejemplo, si usamos una base de datos en        memoria para simular una base de datos real, esta base de datos en memoria se       

when​(productService.​getProducts​())​.​thenReturn​(​"This should be a list of products, not a string"​) ​//Shows an error 

doReturn​(​"​This should be a list of products, not a

   

considera un fake. 

De forma coloquial, también es muy común denominar a todos estos dobles como        “mocks”. 

Recomendaciones 

El principal objetivo de los tests es comprobar que todas las partes implicadas de  una aplicación queden libres de errores de forma unitaria e integrada para 

prevenir problemas en sucesivas fases del ciclo de vida del proyecto. 

FIRST 

Si bien los propios tests deben perseguir también un buen diseño, para evitar que        la propia infraestructura de tests se convierta en un problema, debería cumplir        con el principio FIRST: 

F​

ast: los tests deben ser de rápida ejecución, por eso debemos poner especial       

énfasis en implementar tests unitarios y, solo test de integración en aquellos        casos en los que realmente necesitemos el contexto de un sistema externo para        ser ejecutados. Si nombramos correctamente los tests de integración, podemos        definir una fase concreta para la ejecución de los mismos dentro del ciclo de vida        de Maven, pudiendo ahorrar la ejecución de tal fase en una build normal y        recopilar estadísticas de cobertura independientes distinguiendo entre tests        unitarios y de integración. 

I​

ndependent: para facilitarnos la tarea de detección de errores es muy importante       

que los tests sean independientes los unos de los otros. Para lograrlo debemos        evitar que las salidas de unos tests sean utilizadas como entradas de otros y no        debería importar el orden en el cual se vayan a ejecutar los tests, ya que cada        ejecución debe ser independiente de la otra. Si tenemos una batería de tests de        integración contra base de datos, debemos mantener la transaccionalidad en las        operaciones, de modo que el entorno siempre quede consistente tras su       

   

ejecución. 

R​

epeatable: deben soportar su ejecución más de una vez sin cambiar el resultado       

ni el estado del sistema independientemente de su entorno o contexto. 

S​

elf-validating: deben ser autoevaluables, es decir, que el propio test identifique       

si el test ha funcionado correctamente o no. Esta autoevaluación se realiza        mediante aserciones (asserts). 

T​

imely: deben escribirse en el momento oportuno, es decir antes del código de       

producción, y el motivo es muy simple: es más fácil hacer tests para un código        que todavía no está escrito que para uno que ya ha sido creado, del mismo modo        que es más fácil hacer crecer recto un árbol que todavía no ha brotado con una        guía, que enderezar uno que tiene varios metros de altura.  

 

Documento similar