Una deuda pendiente durante mi carrera profesional dentro de la Industria del Software ha sido dedicarle tiempo y esfuerzo a profundizar en temas de Calidad de Software. Por eso he decidido investigar sobre testing de aplicaciones .NET con xUnit y Moq. El objetivo es obtener los conocimientos necesarios para contribuir una estrategia de pruebas de un proyecto.
Intro a las pruebas de código
- Dos aspectos a tener en cuenta cuando creas tus pruebas de código son: Organización y Optimización.
- Prácticas de diseño de software que impactan en la mejora de las pruebas: SRP, DIP
- Las pruebas van en un proyecto independiente del código que se quiere probar.
- Con las pruebas de código podemos intentar asegurar que tu proyecto hace lo que se espera de él.
- Organización de las pruebas: Separar totalmente el código de producción del código de pruebas.
- Carpeta /src para proyectos de código para producción
- Carpeta /test para proyectos de código para pruebas
- Cómo implementar las pruebas: AAA
- Arrange (Aprovisiona/Organiza): Para crear escenario de prueba
- Act (Actúa): Ejecutamos el código que queremos probar
- Assert (Afirma): Comprobar los resultados obtenidos vs los esperados.
- Las pruebas se lanzan a través de:
- El explorador de pruebas
- La terminal --> dotnet test
- Live Testing (ejeución en segundo plano) --> Con Visual Studio edición Enterprise
- Convención para los nombres: NombreDelMétodo_QueDeberíaDevolver_Condiciones
- Sumar_ShouldBe5_IfA3AndB2()
- Par_ShouldBeTrue_IfA2()
- Dividir_ShouldBe4_IfA8AndB2()
- Dividir_ShouldThrowDivideByZeroException_IfA8AndB0()
xUnit.net
- Con xUnit.net podemos realizar pruebas de código mediante hechos y teorías.
- Fact (Hecho): Una prueba de código única que te permite probar unas condiciones concretas.
- Theory (Teoría): Pruebas de código múltiples. Cada ejecución de una teoría se lleva a cabo de manera independiente.
- Para que las pruebas sean útiles, no solo hay que probar el resultado, sino todos los caminos lógicos que pueda seguir el código. Esto incluye las excepciones y los retornos nulos.
- Una aserción no es más que una comprobación que, en caso de no cumplirse, lanza una excepción. xUnit.net trae un conjunto de asersiones, pero también se puede agregar nuevas a través de librerías.
- Alguna veces es necesario preparar el entorno para las pruebas, por ejemplo incluir ciertos archivos en una carpeta. Podemos hacer esto a través del propio constructor de la clase para preparar las pruebas, e implementar IDisposable para limpiar los recursos una vez que hayamos terminado. Todo este código auxiliar que estamos usando en el constructor lo podemos sacar a una clase independiente a través de los Fixture (Accesorios).
- In xUnit, a test fixture is all the things we need to have in place in order to run a test and expect a particular outcome. Existen 2 tipos de Fixture:
- De clase --> IClassFixture<T>
- De colección --> ICollectionFixture<T>
- Con el accesorio de clase (ClassFixture) vamos a poder compartir funcionalidad entre todas las ejecuciones de las pruebas de una clase.
- La forma de indicar a mi clase de prueba que necesita un accesorio es la siguiente:
- Para tener un código auxiliar que sea transversal a la ejecución de varias clases de prueba usamos un accesorio de colección (ICollectionFixture<T>). Con una colección agrupamos diferentes clases de prueba dentro de una misma ejecución.
- xUnit.net permite agrupar/organizar pruebas. Esto lo logramos a través de los rasgos, es decir, a través del uso del atributo [Trait].
- Los rasgos nos van a permitir filtrar dentro del explorador de pruebas
- Tambien podemos crear rasgos personalizados. Con estos podemos agrupar bugs, agrupar historias de trabajo, tipos de prueba, funcionalidad, etc.
- xUnit.net ofrece la posibilidad de registrar los logs que nos interese, y se añadirán al resultado de la prueba, independientemente de si esta pasa o no.
Simulaciones con Moq
- Un Mock es un objeto simulado: fake object, dummy object, simulated object...
- Moq es un framework de mocking.
- Los mock (objetos simulados) nos van a permitir sustituir elementos de nuestro código por otros cuya respuesta controlamos. Esto lo conseguimos reimplementamos la interfaz con lo que a nosotros nos interese.
- El hecho de ineyectar las dependencias nos permite redefinir comportamientos para poder hacer pruebas.
- Podemos simular código creando código auxiliar para poder simular los objetos en las pruebas, pero esto tiene limitaciones cuando la aplicación escala en tamaño y funcionalidades (mucho código extra para mantener).
- Frameworks para crear objetos simulados: NSubstitute, Moq, RhinoMocks, Foq, etc
- Cómo usar Moq: 1) crear un objeto tipo Mock<T>, 2) configurar el mock, 3) Reemplazar un objeto desde un mock.
- Lo más habitual es crear estos mock en el constructor de la clase de pruebas o en un accesorio, ya que así los vamos a poder utilizar en todas las pruebas aunque, si tienes la seguridad de que sólo vas a necesitarlo en una prueba en concreto, puede ir dentro de ella, en el Arrange.
Temas varios
- Tipos de pruebas
- Unitarias: Probar una única clase. Reciben un mock de las cosas que necesitan para poder funcionar.
- De integración: Verifican que la integración de las diferentes clases y servicios funcionan correctamente. En este tipo de pruebas es posible utilizar mocks para algunos niveles que todavía no se pueden probar, como por ejemplo un servicio de terceros.
- Funcionales: Probar el funcionamiento completo del sistema en base a los requisitos que tiene.
- TDD
- Es una forma de desarrollo ligada a las pruebas de código.
- Pasos
- Escribir las pruebas sobre un requisito concreto
- Hacer el código que haga pasar la prueba
- Por último, se hace una refactorización para dejar el código limpio
- TDD permite desarrollar funcionalidades de una manera muy sólida.
- Cobertura de código
- Es una métrica porcentual que nos da información sobre qué código se ha probado y cuál no.
- Los criterios (tipos) de cobertura del código más usados en un proyecto son: a) Cobertura de líneas (line coverage), b) Cobertura de ramas (branch coverage).
- Herramientas para medir la cobertura de código: Visual Studio Enterprise, extensión Resharper, Rider IDE, etc.
- Hay diferencias a la hora de generar un informe de cobertura de código en .NET Clásico y .NET
- Probar código Legacy
- Si se añade nueva funcionalidad, agregarle sus respectivo código de prueba
- Si se tiene que modificar compartamiento de un método viejo, evaluar si es viable crear un conjunto de pruebas para asegurar que no se rompe nada de ese código en concreto.
- Consejo: Al trabajar con código heredado se deben contener esas ansias de conseguir la perfección que nos caracteriza a los desarrolladores.
Integración Continua y testing
- Detectar que nuestro código funciona en otros servidores además de nuestro entorno local.
- La integración continua es la parte del desarrollo de software que se encarga de integrar cambios con mucha frecuencia para detectar fallos de manera rápida.
- Con la mayor frecuencia posible se compile el código desde 0 y se pasen todas las pruebas que haya en la solución.
- Problemas derivados del desarrollo en equipo:
- El código no compile
- Se haya roto alguna funcionalidad
- Con la Integración Continua podemos detectar errores relativos a dependencias.
- Gracias a la CI podemos tener siempre a mano una versión compilada con todos los cambios del equipo de desarrollo unidos.
- Herramientas para hacer CI:
- Online: Azure DevOps, GitHub Actions
- On-Premise: Jenkins
- La integración continua de nuestro proyecto se puede iniciar de diversas maneras: a través de un commit + push de Git, con un Webhook, programada a determinadas horas cada día...
- Proceso general de Integración Continua:
No comments:
Post a Comment