… segunda Parte de TDD is dead? … o yo y el TDD
Este es el segundo y último artículo de la serie que completa el anterior post TDD is dead? … o yo y el TDD.
Trataré de explicar como defino y codifico mis propios objetos ‘mock‘, y de como los uso tanto para pruebas unitarias, como también en pruebas de integración entre clases.
Y siendo más especifico mostrar como se pueden codificar tres tipos de mock utilizados habitualmente:
- Simular la capa de persistencia a base de datos.
- El que se utiliza para comprobar que se ha llamado a la interfaz que representa desde el objeto que nos interesa probar.
- El utilizado simplemente para poder pasar las pruebas y que no nos interesa su comportamiento.
Y como dijo el maestro:
“… hablar es barato, vamos a ver un poco de código…”
Demo
Para ilustrar el ejemplo utilizaré una aplicación web que mediante un formulario html sencillo llamará con ajax a un servicio REST. Consiste en dar de alta códigos de promoción de una web.
Los datos que se piden son la promoción, el nombre, el correo electrónico y el código de promoción. Se validará que el correo electrónico no esté ya utilizado y si todo es correcto se enviará el correo a la persona y se mostrarán los datos de los códigos ya dados de alta.La demo es completamente operativa y se puede usar. Se puede descargar el código aquí:
MoksExample en GitHub
Los detalles de como funciona y como se puede probar están en el archivo README.md
La parte de servicios REST es Java, montada sobre Spring. En el ecosistema de Spring, Spring-Data es el mediador con la capa de persistencia de datos. En el ejemplo se usará como interfaz con la base de datos. Como framework de tests unitarios usaré Junit. Maven será la herramienta de compilación, ejecución de test y construcción del proyecto. Como es una demo para estos fines se usa Hsqldb, base de datos en memoria.
Simular la capa de persistencia de BBDD
En la demo existen dos entidades PROMOTION y PROMOTION_CODE. La primera es el maestro de promociones y en la segunda se almacenarán todos los códigos de promoción relacionados con las primeras. Los objetos de negocio que las definen son los beans: Promotion y PromotionCode
Spring-Data se basa en el patrón repositorio. A grandes rasgos y simplificando, Spring-Data implementa la interfaz de las operaciones CRUD y el resto de consultas que se pueden realizar para cada entidad de la capa de persistencia (usualmente una tabla de una base de datos).
Existen por tanto dos interfaces definidos en el pakage com.logicaalternativa.ejemplomock.repository cuya misión es definir las operaciones CRUD de las entidades
- PromotionRepository definido para la entidad PROMOTION
- PromotionCodeRepository definido para la entidad PROMOTION_CODE
En este caso los dos extienden del interfaz de Spring-Data JpaRepository.
Con Spring-Data sólo tienes que definir la interfaz con las diferentes operaciones de consulta, inserción y modificación que se van a realizar de la entidad y este se encarga de implementar la interfaz. Esto hace que resulte muy fácil codificar tus propios mocks. Después para los test se inyectarán por inversión de control al objeto de negocio.
Para los test he implementado los dos mock de cada uno de los interfaces. Están en el package com.logicaalternativa.ejemplomock.repository.mock .
Las dos clases son muy similares y siguen la misma línea. Sólo se ha implementado las operaciones necesarias para realizar los test. La idea es esa codificar sólo lo estrictamente necesario. Se añadirá o se modificará código cuando toque, si para un futuro test es necesario implementar algún otro método más.
Centrándome por ejemplo en el mock PromotionCodeRepositoryMock, el ‘quid de la cuestión‘ está en que he definido un atributo lista de tipo PromotionCode.
1 public class PromotionCodeRepositoryMock implements PromotionCodeRepository {2 private List<PromotionCode> promotionCode;3 ...4 }
Este es accesible mediante los métodos públicos:
1 public class PromotionCodeRepositoryMock implements PromotionCodeRepository {2 ...3 public List<PromotionCode> getPromotionCode()4 ...5 public void setPromotionCode(PromotionCode...promotionCode)6 ...7 }
He codificado los métodos CRUD para que operen con esta lista. Por ejemplo, así he codificado el método que sirve para obtener un código de promoción a través de su clave primaria.
01 public class PromotionCodeRepositoryMock implements PromotionCodeRepository {02 ...03 public PromotionCode findOne( final String email ) {04 int index = findIndex( email );05 if ( index != -1 ) {06 return clone( getPromotionCode().get( index ) );07 }
08 return null;09 }
10 ...11 }
Que lo que hace es encontrar el índice de la lista de códigos de promoción con el mismo email. Si lo encuentra devuelve una copia de ese objeto. Para entrar más en detalle te invito a revisar los métodos findIndex, clone y el resto de métodos que he implementado de PromotionCodeRepositoryMock
Otro ejemplo es el método saveAndFlush:
01 public class PromotionCodeRepositoryMock implements PromotionCodeRepository {02 ...03 public <S extends PromotionCode> S saveAndFlush(S promotionCode ) {04 if ( getPromotionCode() == null ) {05 this.promotionCode = new ArrayList<PromotionCode>();06 getPromotionCode().add( promotionCode );07 return promotionCode ;08 }
09 final String email = promotionCode != null ? promotionCode.getEmail() : null;10 int index = findIndex( email ) ;11 if ( index == -1 ) {12 getPromotionCode().add( promotionCode );13 } else {14 getPromotionCode().set( index, promotionCode );15 }
16 return promotionCode;17 }
18 ...19 }
Que busca en la lista el objeto con el mismo email. Si lo encuentra actualiza el objeto de la lista y sino, lo añade a la lista.
¿Cómo se usa?
El test AddCodeBusinessImpTest prueba el objeto de negocio AddCodeBusinessImp.
La clase de negocio (AddCodeBusinessImp) comprueba que existe el código de promoción en la BBDD y utiliza para ello PromotionRepository. En el método setUp() del test se inicializa un objeto PromotionRepositoryMock con el objeto Promotion deseado y este mock se inserta por inversión de control a la instancia de AddCodeBussinessImp.
01 public class AddCodeBusinessImpTest {02 ...03 private AddCodeBusinessImp addCodeBusinessImp;04 ...05 private PromotionRepositoryMock promotionRepositoryMock;06 private PromotionCodeRepositoryMock promotionCodeRepositoryMock;07 private Promotion promotion;08 ...09 @Before10 public void setUp() throws Exception {11 promotion = new Promotion();12 promotion.setIdPromotion( 1 );13 ...14 promotionRepositoryMock = new PromotionRepositoryMock();15 promotionRepositoryMock.setPromotions( promotion );16 ...17 addCodeBusinessImp = new AddCodeBusinessImp();18 addCodeBusinessImp.setPromotionRepository( promotionRepositoryMock );19 ...20 }
21 ...22 }
promotion, promotionRepositoryMock, addCodeBusinessImp son atributos privados de AddCodeBusinessImpTest.
Internamente AddCodeBusinessImp llama al método findOne de PromotionRepository con el identificador de la promoción (el del maestro de promociones) del nuevo código que se quiere insertar.
Si queremos testear que ocurre si no encuentra la promoción en la BBDD, simplemente se hace lo que en el método testPromotionNotBbDd de AddCodeBusinessImpTest:
01 public class AddCodeBusinessImpTest {02 ...03 @Test04 public void testPromotionNotBbDd() {05 try {06 promotionRepositoryMock.setPromotions( ( Promotion[] ) null );07 addCodeBusinessImp.validateAndAdd( promotionCode );08 ...09 }
10 ...11 }
Si la lista de promociones en PromotionRepositoryMock es nula no se podrá encontrar la promoción (El método findOne devolverá nulo).
En este mismo test también se utiliza PromotionCodeRepositoryMock. Inyectado también por inversión de control a la instancia de AddCodeBusinessImp, en este caso, sirve para comprobar que se ha insertado el código de promoción. Se hace en el test testOk
01 public class AddCodeBusinessImpTest {02 ...03 @Test04 public void testOk() {05 try {06 addCodeBusinessImp.validateAndAdd( promotionCode );07 final List<PromotionCode> promotionCodeBbDCodes = promotionCodeRepositoryMock.getPromotionCode();08 boolean result = promotionCodeBbDCodes != null09 && promotionCodeBbDCodes.size() == 110 && promotionCode.equals( promotionCodeBbDCodes.get( 0 ) )11 ;
12 ...13 }
14 ...15 }
Para tener una visión más clara de cual es la idea, lo mejor es revisar cada uno de los test Junit de AddCodeBusinessImpTest Creo que puede ser de gran ayuda para tener una visión global de lo que quiero plantear . He intentado que sean claros y autoexplicativos.
Mock de comprobación de llamada a interfaz y datos correctos.
Lo que interesa en este caso es comprobar que un objeto de negocio hace la llamada a un interfaz con los datos correctos. Lo que se hace es ‘mockear‘ esa interfaz capturando los datos de la llamada para comprobar después de que estos son correctos.
En la demo existe un objeto de negocio, SendMailCodePromotionImp, que a partir de una dirección de correo manda el código de promoción por email. Este objeto de negocio realiza internamente una llamada a la interfaz JavaMailSender que implementa Spring (JavaMailSenderImpl) para enviar el correo electrónico.
JavaMailServerMock, que implementa también el interfaz JavaMailServer, es el mock que va a recoger los datos de la llamada. En este caso sólo me interesaba saber la dirección de correo a la que se envía el correo electrónico. Para ello he implementado el método send:
01 public class JavaMailServerMock implements JavaMailSender {02 ...03 private String sendTo;04 ...05 @Override06 public void send( MimeMessage mimeMessage ) throws MailException {07 String[] to = null;08 try {09 to = mimeMessage.getHeader("To");10 } catch (MessagingException e) {11 logger.error( "[send] ".concat( e.getMessage() ), e );12 to = null;13 }
14 setSendTo( to != null && to.length > 0 ? to[0] : null );15 }
16 ...17 public String getSendTo() {18 return sendTo;19 }
20 public void setSendTo(String sendTo) {21 this.sendTo = sendTo;22 }
23 }
Se recoge del argumento de entrada la cabecera “To” para almacenarla en un atributo de la clase.
¿Cómo se usa?
En el test SendMailCodePromotionImpTest se utiliza este Mock. En el método setUp(), se instancia y se inserta por inversión de control al objeto SendMailCodePromotionImp.
01 public class SendMailCodePromotionImpTest {02 ...03 private SendMailCodePromotionImp sendMailCodePromotionImp;04 ...05 private JavaMailServerMock javaMailServerMock;06 ...07 @Before08 public void setUp() throws Exception {09 ...10 javaMailServerMock = new JavaMailServerMock();11 ...12 sendMailCodePromotionImp = new SendMailCodePromotionImp();13 ...14 sendMailCodePromotionImp.setJavaMailSender( javaMailServerMock );15 ...16 }
17 ...18 }
El test testOk de SendMailCodePromotionImpTest sirve como ejemplo para comprobar si se ha realizado correctamente la llamada:
01 public class SendMailCodePromotionImpTest {02 ...03 public void testOk() {04 try {05 sendMailCodePromotionImp.sendMailCodePromotion( email, locale );06 final String sendTo = javaMailServerMock.getSendTo();07 boolean res = sendTo != null08 && sendTo.equals( nameUser.toUpperCase() + " <" + email +">" );09 ...10 }
11 ...12 }
Donde después de hacer la llamada al método sendMailCodePromotion del objeto de negocio se obtiene el atributo de sendTo del mock para comprobar que se envía a la misma dirección de correo electrónico.
En este mock sólo se comprueba la cabecera ‘To‘. También se podría comprobar el texto del mensaje o cualquier otro dato enviado desde el objeto de negocio.
Mock para pasar las pruebas
En la demo he codificado MessageSourceMock que implementa el interfaz MessageSource, utilidad que tiene Spring para la internacionalización. Este mock sólo me interesa para poder probar otras funcionalidades. Ha sido necesario codificado porque el objeto de negocio lo necesitaba, pero, en este caso, el resultado de su llamada era irrelevante. Solamente se ha codificado el método getMessage :
1 public class MessageSourceMock implements MessageSource {2 ...3 @Test4 public String getMessage(String arg0, Object[] arg1, Locale arg2) throws NoSuchMessageException {5 return arg0;6 }
7 ...8 }
Que como puedes comprobar, devuelve directamente la entrada.
¿Cómo se usa?
En el test SendMailCodePromotionImpTest se utiliza este Mock. En el método setUp()Se instancia y se inserta por inversión de control al objeto
SendMailCodePromotionImp.
01 public class SendMailCodePromotionImpTest {02 ...03 private SendMailCodePromotionImp sendMailCodePromotionImp;04 ...05 private MessageSourceMock messageSourceMock;06 ...07 @Before08 public void setUp() throws Exception {09 ...10 messageSourceMock = new MessageSourceMock();11 sendMailCodePromotionImp = new SendMailCodePromotionImp();12 ...13 sendMailCodePromotionImp.setMessageSource( messageSourceMock );14 }
15 ...16 }
Ya ahora al hacer la llamada getSendTo del objeto de negocio no aparece la tan temida NullPointerException 😉
Conclusiones
Como se puede ver, con un poco de ingenio es relativamente sencillo crearte tus propios objetos mock sin tener que recurrir a Frameworks. La creación de los mock me ayudan a comprender mejor como funciona la aplicación y ver las clases involucradas como verdaderas cajas negras. Esta es la gracia del paradigma de la orientación a objetos.
No necesariamente tiene que llevar demasiado tiempo el codificarlos. Sólo se debería ‘picar’ lo que necesitas cuando lo necesitas y mucho es reaprobechable. Al final del proyecto seguro que tienes un buen juego de objetos mock para pruebas.
Otra ventaja, es que no hay reflexión, no existe bbdd en memoria para los test, ni ha sido necesario levantar un entorno para pruebas… el resultado es que los test van ‘volaos‘.
P.D.: Para esta demo, de esto que te vas liando y liando… y poco a poco me ‘currado’ un framework MVC hecho en JavaScript. Cuando lo organice un poco será material para próximos post.