Ir al contenido principal

Unit Testing

Como programador, he aprehendido la importancia de los test unitarios por la vía del dolor. Por la vía de los mantenimientos de software que se van volviendo más y más inmantenibles día tras día, por la vía de corregir un error con urgencia, subirlo a un entorno de producción habiendo hecho las pruebas funcionales justas, y totalmente orientadas a probar la parte que se ha modificado, para darme cuenta -también dolorosamente- que la corrección del error produce otro error en algún otro punto de la solución, que obliga a revertir el cambio, corregir de nuevo con la presión del negocio en aumento, y subir la corrección de la corrección con los dedos cruzados y las rodillas temblorosas, a sabiendas de que ningún mecanismo me garantiza que no vuelva a pasar lo mismo. Sencillamente no se pueden repetir manualmente todas las pruebas, todas las casuísticas que pudieran haberse visto afectadas por el cambio.

Cuesta un poco entender el test unitario como una inversión, pero cuando por fin lo haces entiendes su potencia: sí, lleva tiempo; hay que dedicarle tiempo y cuidados a los test, tanto como a las implementaciones. ¿Cuál es el beneficio que retorna esta inversión? Podríamos mencionar los dos quizá más importantes: tiempo, y solidez. Tiempo porque una batería de test bien hechos nos debería ahorrar mucho tiempo de depuración. Mucho tiempo de búsqueda de bugs recónditos. Y solidez, porque con cada prueba que hagamos (cada vez que lanzamos la batería de test) lo probamos TODO. O casi todo, eso depende de la cobertura de tus test. 

Luego está TDD. Una metodología de desarrollo en la que los test unitarios se vuelven los protagonistas de nuestro proyecto. Un prodigio que he venido conociendo de la mano de este estupendo libro Diseño Ágil con TDD . Quien lo escribe está haciendo cosas re-interesantes desde las Islas Canarias que os animo a curiosear.

Abordaremos TDD más adelante si tenemos ocasión. De momento vamos a incorporar test unitarios en nuestra plataforma 

Lo primero que debemos elegir es una framework de entre los disponibles, xUnit, nUnit o MsTest. Francamente, no aprecio diferencias sustanciales entre ellos, si bien esta apreciación podría perfectamente ser rebatida. Voy a elegir MsTest como punto de partida, haciendo caso omiso de este interesante artículo, Why xUnit, and not nUnit or MsTest?

Testearemos la clase Business_Data_Functions del proyecto ZMLabsBusiness:


Fijémonos en el método TestMasterDB. Recibe como parámetro el nombre de un servidor SQL Server. El método comprueba si el servidor es válido y accesible, creando una conexión contra la base de datos master. Si la conexión se establece devuelve un valor true, y sino, un valor false. 

Debemos pensar en qué circunstancias el método funcionaría bien, y en cuales no. Bueno, lo más básico a priori es pasar un nombre de servidor que sí funcione, y uno inventado, con lo que tendríamos 2 pruebas básicas pero efectivas.

    [TestClass]
    public class DataFunctionsTest
    {
        [TestMethod]
        public void TestMasterDB_Fail_IncorrectServer()
        {
            Business_Data_Functions _datafunctionobject = new Business_Data_Functions();

            bool res = _datafunctionobject.TestMasterDB(@"XXX");

            Assert.IsFalse(res);
        }

        [TestMethod]
        public void TestMasterDB_OK()
        {
            Business_Data_Functions _datafunctionobject = new Business_Data_Functions();

            bool res = _datafunctionobject.TestMasterDB(@"DESKTOP-8R1RIRR\sqlserveri17");

            Assert.IsTrue(res);
        }
    }

Los adornos [TestClass] y [TestMethod] son suficientes para que Visual Studio identifique estas clases y métodos como pruebas unitarias, y las muestre en el explorador de pruebas, desde donde podemos ejecutar una, varias o todas, así como lanzar depuraciones.


La prueba, como prueba unitaria que es, debe tener un propósito concreto. Si aprecias que tu prueba tiene dos o más propósitos tal vez debas pensar en dividirla en más pruebas, más simples. 

Además idealmente cada uno de los métodos de prueba debe funcionar de forma aislada, sin depender unas del resultado de otras.

La estructura de la prueba suele constar de tres bloques: (i) la preparación de los datos necesarios para la prueba, (ii) la ejecución en si de la prueba y (iii) las comprobaciones.

Veamos el siguiente método de la clase Business_Data_Functions


Este método da un poco más de juego. Le pasamos un nombre de servidor SQL Server, y nos devuelve la ruta donde residen los ficheros de la base de datos master, el fichero de datos y el de log.

Podemos implementar una prueba correcta para un servidor activo, y depurarla para ver cuál es la respuesta esperada, en función de la cual estableceremos las mejores comprobaciones para la prueba:


El método funcionando bien devolverá dos entradas en la colección, una de tipo log y otra de tipo data. De cara a los resultados, podemos comprobar que el conteo de los elementos de la lista es 2, pero mejor será la prueba si nos decidimos por ser más específicos. 

        [TestMethod]
        public void GetFilesPath_OK()
        {
            Business_Data_Functions _datafunctionobject = new Business_Data_Functions();

            List<DataDomain> res = _datafunctionobject.GetFilesPath(@"DESKTOP-8R1RIRR\sqlserveri17");

            Assert.AreEqual(res.Count, 2);
            Assert.AreEqual(res.Where(d => d.FileType == DataDomain.enumFileType.data).ToList().Count, 1);
            Assert.AreEqual(res.Where(d => d.FileType == DataDomain.enumFileType.log).ToList().Count, 1);
        }

        [TestMethod]
        public void GetFilesPath_Fail_IncorrectServer()
        {
            Business_Data_Functions _datafunctionobject = new Business_Data_Functions();

            List<DataDomain> res = _datafunctionobject.GetFilesPath(@"XXX");

            Assert.AreEqual(res.Count, 0);
        }

Inmediatamente después, la siguiente prueba obvia es aquella en que pasamos como parámetro un servidor incorrecto.

No es baladí esta segunda prueba. Consideren que, si el método devolviera una excepción en lugar de una lista vacía -que es nuestro resultado esperado- el test no pasaría y nos advertiría así que algo no funciona como estaba previsto. ¿En qué parte de nuestro código tendrá repercusión este mal funcionamiento? A saber; nos da igual. No necesitamos rebuscar entre el código ni lanzarnos a depurar nada. Tan solo debemos corregir el comportamiento del método cuya prueba falló. Damos por sentado que en todas partes donde usemos ese código, esperaremos como resultado una lista vacía si el servidor pasado por parámetro es incorrecto o no está funcionando.

Un punto importante a tener en cuenta al hacer test unitarios en la cobertura de código. Cuanta más mejor, claro está. Cuando hacemos test unitarios en distintas capas veremos que se produce cierta redundancia. No se preocupen. Al fin y al cabo estamos probando una misma funcionalidad desde distintas perspectivas.  

Para darle mayor cobertura a nuestros test unitarios, voy a proceder a implementar algún test para nuestra capa de datos, que en algunos casos parecerán redundantes con respecto a los test de la capa de negocio. Vayamos por ejemplo a la clase labs_repos del proyecto ZMLabsData.

El siguiente código prueba el método getCategories desde la perspectiva del acceso a datos:

    [TestClass]
    public class data_tests_info_Test
    {
        private string cnx_str_OK = @"Persist Security Info=False;Integrated Security = true; Initial Catalog = EnigmaLABS_EF; Server=DESKTOP-8R1RIRR\sqlserveri17";
        private string cnx_str_Fail = "XXX";

        [TestMethod]
        public void getCategories_OK()
        {
            labs_repos _repo = new labs_repos(cnx_str_OK);

            var res = _repo.getCategories();

            Assert.IsTrue(res.Count > 0);
            Assert.AreEqual(res[0].Categorie, "SQL Server Tips");
        }

        [TestMethod]
        public void getCategories_BadCnxStr()
        {
            labs_repos _repo = new labs_repos(cnx_str_Fail);

            var res = _repo.getCategories();

            Assert.AreEqual(res.Count, 0);
        }
    }

El método getCategories es invocado desde la capa de negocio desde un método homónimo de la clase Business_Test_Functions_EF.


El test unitario para el método getCategories de la capa de negocio parece ser casi idéntico al que anteriormente hemos desarrollado. 

    [TestClass]
    public class EFFunctionsTest
    {
        [TestMethod]
        public void getCategoriesTest_CNX_Fail()
        {
            labs_repos repos = new labs_repos("");
            Business_Test_Functions_EF clsObj = new Business_Test_Functions_EF(repos);

            List<CategoriesDomain> res = clsObj.getCategories();

            Assert.IsTrue(res.Count == 0);
        }

        [TestMethod]
        public void getCategoriesTest_CNX_OK()
        {
            labs_repos repos = new labs_repos(@"Persist Security Info=False;Integrated Security=true;Initial Catalog=EnigmaLABS_EF;Server=DESKTOP-8R1RIRR\sqlserveri17");
            Business_Test_Functions_EF clsObj = new Business_Test_Functions_EF(repos);

            List<CategoriesDomain> res = clsObj.getCategories();

            Assert.IsTrue(res.Count > 0);
        }
    }

Parece claro que si el test en la capa de datos falla, fallará igualmente el test en la capa de negocio. Sin embargo, si en la capa de negocio el código evoluciona, e incorporamos nuevas reglas, entonces veremos cuan interesante resulta disponer de una buena cobertura de test unitarios en cada una de las capas de la solución..

Amigos programadores, hasta aquí la publicación de hoy. 

Voy a despedirme con un reto para todos Uds. Una interesante práctica para mejorar nuestra técnica con los Unit Test. ¿Recuerdan el conjunto de funciones fibo_functions, parte_horas_functions y quijote_functions


...son parte esencial del buen funcionamiento de nuestra plataforma, y están pidiendo test unitarios a gritos. ¡Anímense, respondan a la llamada! El próximo reto será crear test unitarios para las clases test1_, test2_, etc. 

Sigan disfrutando aprendiendo mis queridos lectores. Hasta la próxima.








Comentarios

Entradas populares de este blog

Test 3: ¿Son eficientes los ORMs?

Bienvenidos amigos. Me complace anunciar que por fin estrenamos la categoría " SQL Server Tips ", y lo hacemos por todo lo alto, entrando de lleno en un aspecto altamente polémico entre programadores. ¿Es eficiente un ORM en los accesos a datos? Ya conocen la filosofía de nuestros Tests, no vamos a teorizar demasiado, pero sí una pequeña base va a ser necesaria para conseguir una buena respuesta a nuestra pregunta. He leído un interesante  artículo de nuestros súper amigos de Deloitte ( cuando usar ORM ) argumentando que el uso o no de un ORM hay que decidirlo en relación a la complejidad de nuestro modelo de datos, y al rendimiento que requeriremos en nuestras soluciones, pero, ¿cuándo no deseamos el mejor rendimiento para nuestro software? Lo cierto es que, como ya hemos visto, el ORM facilita mucho las cosas, aporta claridad al código, de eso no cabe duda, pero, ¿es eficiente? He ahí la cuestión. Sobre este asunto vamos a poner a funcionar nuestros apreciados Test. C

Proyecto GEOW. Implementando el patrón CQRS. 2ª parte

En el anterior post ( Proyecto GEOW. Implementando el patrón CQRS ) nos adentramos en el funcionamiento del proyecto GEOW que nos va a servir de base para implementar un patrón arquitectónico, CQRS , pensado para dar respuesta a sistemas con alta exigencia de lecturas y escrituras simultáneas. Para ello hemos creado una interfaz gráfica con una serie de figuras geométricas en movimiento. Ahora vamos a ir a la parte de EL DATO. Cada vez que uno de estos cuadrados cambia de posición envía una trama con sus propias características, y sus nuevas coordenadas. Cada uno de los cuadrados realiza un movimiento cada 300 milisegundos, y he llegado a probar con hasta unas 700 figuras. En estos niveles el software empieza a sufrir, pero más la parte gráfica. Aparentemente el sistema de grabación de coordenadas se mantiene en buena forma. Vamos a ver, precisamente, este sistema de grabación: Grabando lotes de coordenadas en BBDD En el objeto PointObj que representa cada una de las figuras, en el

Primeros pasos en Enigma Software Labs

En pocas palabras: vamos a probar y estresar distintas soluciones de software para un mismo problema, en busca de realidades matemáticas. Para ello, lo primero que necesitamos en averiguar el modo de publicar código aquí en Blogger . Para ello voy a usar un sistema súper sencillo y eficiente. Ahí va un ejemplo de código C# private void frmMonitor_Load ( object sender, EventArgs e) { TreeNode _tr = new TreeNode( "SQL Server Tips" ); TreeNode _tr2 = new TreeNode( "C# Code" ); treeCatalogo.Nodes.Add(_tr); treeCatalogo.Nodes.Add(_tr2); //splitContainer.Panel1.Focus(); //lstMonitor.Focus(); } Hilarante ¿verdad?   hilite.me  lo hace posible. Bien, paso 2, fácil y sencillo: vamos a crear una plataforma de testeo para C# en una bonita solución que compartiré en el repositorio GIT, para disfrute de todos. Enigma Software - ZM LABS Como posteriormente me dive