He usado este post como base para una sesión en el AOS2014 y he actualizado el post para tener en cuenta alguno de los comentarios recibidos.
La aplicación se desarrolla usando TDD excepto en ciertas partes que tenemos identificadas y que cubrimos de otra forma. Durante este proceso hemos ido evolucionando en la forma de hacer TDD.
Antes tests unitarios considerando aislamiento por clases y a veces incluso testeando por métodos de esas clases... Mucho mock. Requiere tener algo claro el diseño, pero requiere poco "poder" cerebral :-)
Ejemplos:
test_detect_bridge_profile
test_detect_catv
test_deprovision_raise_error_when_snmp_exception_was_raised
test_deprovision_service_ports_with_provisioned_ont_from_repository
test_provision_raise_error_when_snmp_exception_was_raised
test_provision_service_ports
A este tipo de tests les llamamos Test Unitarios de clase.
Hemos evolucionado a tests unitarios mas centrados en las funcionalidades de negocio y que pueden usar una o mas clases. Al igual que los Tests Unitarios de clase, estos tests son independientes unos de otros y no tienen ningún tipo de dependencia externa (BD, Red, Eventos, etc).
A este tipo de tests les llamamos Test Unitarios de funcionalidad.
Ejemplos:
CPE status
CPE Events History
Acknowledge CPE status events
✓ it acknowledges cpe status events
CPE Events with errors
when ask for all the cpe status events with error
✓ it returns all the events with error
when ask for the number of events with error of a cpe
✓ it returns the number of errors of the cpe
Remove CPE status event history
✓ it removes the status history of a cpe
history event recieved
✓ it registers the event
when ask for events history returns only event limit
✓ it returns events bases to limit option
when cpe goes offline
✓ it creates history events only when cpe status changed
✓ it removes cpe status
when voip changes status
✓ it creates history events only when voip status changed
...
A continuación podemos ver un extracto de la implementación de estos tests:
Como se puede ver intentan en todo momento reflejar términos de negocio, en nuestro caso referidos a telecomunicaciones y al estado de un CPE (Customer-premises equipment)
La entrada de este test es la lógica de negocio (cpe_status_service) que implementa el estado del CPE, para el que hemos usado dobles de pruebas sólo para los ports (Sistema de credenciales SIP (SipCredentials), Repositorios de persistencia (que en este caso están implementados en memoria (InMemoryCPEStatusRepository, InMemoryCPEStatusEventHistoryRepository)), y el reloj del sistema (Clock) que aunque no se suele tener en cuenta es claramente un port del sistema).
Por otro lado, existen otros tests, realizados también con TDD para cubrir implementaciones de los ports identificados (repositorios, SipCredentials, etc).
Actualmente todo el sistema esta cubierto por los siguientes test:
Tests unitarios
- Test Unitarios de clases 672 / 0.9s
- Test Unitarios de funcionalidad 135 / 0.45s
Test integracion:
- Total 166 12.89s
- De contrato 45 10s
Intentamos desarrollar siguiendo este orden
- Lógica de negocio
- Adaptadores
- Deliveries (servicio rest, interface cli, publicación/recepción de eventos y mensajes asíncronos)
Para desarrollar Lógica de negocio
- Identificamos ports
- TDD funcionalidades (dobles de pruebas/implementaciones simples para los ports)
- TDD sobre adaptadores (implementaciones de esos ports)
- Test de contrato de librerías / tecnologías (usados por esos adaptadores)
Evidentemente todo esto puede estar salpicados de spikes cuando queremos probar librerías, hacer bosquejos de adaptadores o cualquier otro tipo de prueba
Ahora mismo los deliveries no los probamos demasiado.
Conclusiones:
Ventajas de los Tests unitarios de funcionalidad con respecto a Test unitarios de clases
- Nos cuesta mucho menos mantener el código (refactors mucho mejores)
- Menor tendencia al sobre diseño
- El código refleja mejor el dominio
- Tenemos antes una solución completa que vamos refinando paulatinamente
Desventajas
- Algunas veces cuesta identificar los ports y cuesta un poco empezar una funcionalidad
Para paliar esta desventaja, algunas veces creamos mocks y tests de menor alcance que solo nos valen para ayudarnos a encajar las piezas (andamiaje) luego los borramos.
Podemos concluir que este cambio ha sido un gran avance para el equipo y ahora siempre que tenemos que añadir funcionalidades en una parte que tiene los tests unitarios por clases, lo primero que nos planteamos es si hacer nuevos tests por funcionalidades. A veces lo hacemos, otra veces no, depende del ROI.