Curso de Testing Javascript Moderno. Parte 5: tests unitarios en métodos privados

25 Oct 2013

Introducción

Continuamos con nuestra serie de tutoriales sobre Testing Moderno en Javascript para abordar en esta ocasión una cuestión interesante: la cobertura de aquellos métodos o funciones que estructuramos en nuestras aplicaciones para que sean ‘privados’ siguiendo la filosofía de otros lenguajes de programación orientados a objetos.

Por definición, estos trozos de código deberían ser inaccesibles desde fuera del contexto en el que se han declarado, por lo que a priori, los tests que lanzamos no son capaces de analizarlos. Para solventar este problema no existe una fórmula ‘universalmente aceptada’, por lo que tenemos que recurrir a la que mejor se adapte a nuestra forma de programar.

Veamos en detalle el problema y las soluciones propuestas.

Métodos privados

En Javascript, es habitual contar con métodos privados dentro de nuestros objetos cuyo fin habitual es el de no contaminar el entorno global. Definimos así a estas porciones de código como aquellas no accesibles desde fuera del ámbito (scopes) en que han sido declaradas. Curiosamente, como Javascript no maneja de un modo nativo este tipo de comportamiento como si lo hace por ejemplo Java o PHP dentro de su arquitectura de clases, se ha escrito mucho sobre cómo conseguir ‘emularlo’. En este mismo blog, cuando hemos tratado el tema de los patrones de diseño, hemos ido analizando diversas formas para aislar funciones. La más conocidas por todos, es seguramente la que se deriva del frecuentemente utilizado patrón módulo que ya tratamos en detalle.

Un ejemplo sencillo de esta arquitectura quedaría como sigue:

var myModule = ( function() {
 
    function foo () {
        // private function `foo` inside closure
        return "foo";
    }
 
    return {
        bar: function () {
            // public function `bar` returned from closure
            return "bar";
        }
    }
} () );

Existen muchas variantes de este patrón, cada una con sus particularidades, pero esta resulta clara tras un simple vistazo: vemos una función autoejecutable que devuelve un objeto al contexto global. Dicho objeto tiene como objetivo exponer funciones que serán accesibles de forma directa (desde el contexto global) como en este caso la función ‘bar‘. Esta función ‘bar’ puede sin embargo necesitar dentro de su lógica interna recurrir a otras funciones que no nos interesa que sean accesibles desde fuera, como por ejemplo en este caso, ‘foo‘. La idea es que ‘bar’ puede acceder a ‘foo’ porque están declaradas dentro del mismo contexto, pero cuando se ha evaluado todo, la única función que se devuelve al contexto global es ‘bar’, mientras que ‘foo‘ permanece ‘enterrada’ y por tanto inaccesible.

Este planteamiento puede resultar muy interesante en nuestras aplicaciones porque nos permite ‘ocultar’ parte de la lógica al contexto global. La Programación Orientada a Objetos utiliza esta característica constantemente en sus arquitecturas. Sin embargo, desde la perspectiva del testing, esto nos presenta un problema: mientras que testear ‘bar’ resulta sencillo y trivial porque es accesible, ‘foo’ resulta imposible de alcanzar al no estar expuesto. Es decir, no podemos cubrir con tests la lógica que se encierre ‘foo’ y por tanto quedamos ahí sin cobertura.

NOTA: El tema de la cobertura no lo hemos tratado aún pero aquí puede imponerse por ahora el sentido común: una aplicación debe presentar la mayor cobertura de tests posible, incluyendo por supuesto el análisis de aquellos métodos privados y su correspondiente lógica interna. En un capítulo más adelante trataremos este concepto además de presentar algunas herramientas que nos indican de una forma visual el alcance de nuestros tests con respecto al código que tengamos escrito.

Estrategias agresivas para acceder a los métodos privados

En el mundo del TDD, suele esgrimirse un argumento frente a este escenario: un método privado no debería ser testeado ya que representa una funcionalidad mínima; si se considera que la lógica contenida en uno de estos métodos precisa de tests, tenemos dos posibles soluciones a adoptar: hacer dicha porción de código pública o extraerla a un módulo independiente sobre el que puedan aplicarse estas pruebas.

La primera opción resulta lógica: si un método privado aparece como complejo y requiere de una cobertura de tests, debería resultar natural modificar su ámbito de visibilidad volviéndolo público y así poder acceder desde la capa de tests. Sin embargo, esto conlleva un segundo problema de arquitectura del código: estamos condicionando nuestra organización en función de las pruebas y no del diseño.

La segunda opción, la extracción, es otra alternativa considerable: por lo general, si una funcionalidad dentro de un método privado conlleva un comportamiento complejo que precisa de cobertura, podemos estar ante una clara señal de que ese código debería funcionar como un módulo independiente siendo así en definitiva testeable. No estamos aquí haciendo público el método, sino cogiendo aquella parte más compleja para sacarla a un ámbito al que si podamos acceder con tests. Esta aproximación modular solucionaría la necesidad de realizar tests sobre métodos privados, no altera demasiado el diseño, pero como contrapartida cambia la estructura original de lo que ya hemos desarrollado.

Pese a que ambas soluciones propuestas son viables cuando tenemos que crear tests unitarios para funciones privadas, puede darse el caso en que no estemos en disposición de cambiar la estructura actual, bien por imposición del diseño, del equipo, o cualquier otro.

La solución híbrida

Bajo el supuesto anterior, podemos optar por una solución híbrida entre las dos propuestas anteriores: contar con funciones privadas que, durante el tiempo de desarrollo, se exponen al ámbito público para más tarde, en su paso a producción, convertirlas de nuevo en privadas de forma automática.

Conseguir esto es más sencillo de lo que puede parecer: únicamente debemos referenciar dentro de nuestro objeto a esos métodos privados como parte del API de forma temporal. Acotando esas declaraciones con comentarios o alguna señal visual, más adelante será sencillo eliminarlas manteniendo con ello la visibilidad original esperada. Veamos un ejemplo con el código anterior:

var myModule = ( function () {
 
    function foo () {
        // private function `foo` inside closure
        return "foo";
    }
 
    var api = {
        bar: function () {
            // public function `bar` returned from closure
            return "bar";
        }
    }
 
    /* test-code */
    api._foo = foo;
    /* end-test-code */
 
    return api;
} () );

Aquí podemos ver cómo el esquema básico no se ha modificado sustancialmente: tenemos una función autoejecutable que devuelve un objeto ‘api‘. Este objeto, la API propiamente dicha, lleva asociada el método ‘bar‘ como público. En el ámbito privado quedaría ‘foo‘, en principio inaccesible desde fuera de su contexto. Sin embargo, al asociar esta función directamente al objeto ‘api‘ tal y como vemos al final del código estamos permitiendo su acceso temporal desde fuera.

/* test-code */
api._foo = foo;
/* end-test-code */

El guión bajo inicial indicaría según las convenciones, que estamos ante un método privado susceptible de cambio.

En este contexto, tanto ‘bar‘, como ‘_foo‘ están expuestos como métodos públicos del objeto ‘api‘ y, por tanto, son ‘testeables’.

La estrategia final pasaría por eliminar este tipo de código en el paso final a producción bien a mano o, mucho mejor, mediante alguna herramienta de CI (Integración Continúa) como Grunt. Básicamente, en nuestro ejemplo, hemos modificado nuestro API dentro de un bloque de comentarios que nos sirven de delimitadores. Una vez completo el desarrollo, únicamente tendríamos que preocuparnos de indicar a nuestra herramienta automatizada que elimine dicho bloque salvaguardando así la estructura original de nuestro código.

Cuando tratemos el tema de la automatización, retomaremos este ejemplo para ver cómo podríamos eliminar ese ‘chivato’ que hemos colocado al final de nuestro objeto y que ‘revela’ las funciones que tendrían que ser privadas. De momento, la idea es disponer de la capacidad de acceder a ellas manteniendo diseño y código prácticamente inalterables.

Conclusión

Dentro de una dinámica TDD, es importante que cada línea de código efectivo que escribamos esté respaldada por un test que verifique su correcto comportamiento. Esto resulta relativamente sencillo de cumplir cuando todo el código es accesible desde cualquier punto de la aplicación, o lo que es lo mismo, cuando desde el contexto global se puede alcanzar a toda y cada una de las funciones que hemos programado. Sin embargo, en el mundo real, una aplicación moderna ‘esconde’ parte de su lógica dentro de estructuras y patrones de diseño que se han diseñado precisamente para cumplir esa labor: crear flujos lógicos donde solo quede finalmente expuesto de forma pública lo necesario mientras que toda la lógica interna queda oculta.

Este planteamiento, tan interesante desde el punto de vista de arquitectura del software, resulta sin embargo insalvable cuando queremos practicar los tests. Si un código es inaccesible para el API público de la aplicación, también lo es para el test. Por ello, hemos presentado dos estrategias (agresivas) y un híbrido final con el que salvar este punto, quedando a la elección del desarrollador (o equipo), aquel que mejor se adapte a su forma de trabajo.

Más:

{5} Comentarios.

  1. Tomás Corral

    Buenas, el tema de si testear o no métodos privados en JS es bastante peliagudo, aunque me gustaría dejar mi opinión al respecto.

    Si hacemos TDD como debemos, los métodos privados sólo aparecen durante el proceso de refactorizado del proceso, por lo cual esas lineas que pasamos a un método privado ya las hemos testeado en la fase de hacer pasar los tests.

    Mi opinión es que si tienes que testear un código privado es porque no se ha utilizado correctamente TDD o no se ha seguido el proceso correctamente.

    Saludos.

    • Carlos Benítez

      Efectivamente Tomás, esa es la línea más clásica dentro del TDD y la que suelo seguir de forma personal. Sin embargo, si que encuentro ahí el inconveniente en cuanto a la generación de informes de control: si además de la mera práctica llevamos reportes de la cobertura alcanzada con alguna herramienta externa (o varias) como puede ser Istanbul + Sonar, los métodos privados que pueda haber en la aplicación/biblioteca cuentan como líneas de código sin testear.

      Moralmente, puedo saber que todo, o casi, está bajo pruebas, pero para una auditoría que exige informes (últimamente habituales en grandes arquitecturas), el porcentaje que estos devuelven sobre cobertura será menor que el real.

      Por experiencia, cuando se trabaja en equipos de tamaño medio o grandes, esta solución híbrida que he comentado me ha resultado muy útil al permitir cubrir las especificaciones de cliente.

      Un saludo!

  2. Tomás Corral

    Precisamente sobre este tema he estado trabajando últimamente en mi último proyecto https://github.com/tcorral/JSONC donde he utilizado Karma con karma-coverage que hace uso de Istanbul y sin tener que testear ningún código privado, expresamente, tengo un 100% de cobertura.

    El cómo se ha hecho es como te decía haciendo pasar los test con el código mientras que no has hecho ninguna refactorización y por lo tanto el código es privado. En la fase de refactorización muevo el código succeptible de moverse a funciones privadas y vuelvo a pasar el test, la ejecución de los test hará que las lineas que antes se ejecutaban como código en la función pública, ahora se ejecuten desde la función privada.

    El problema de testear directamente una función privada es que es algo que el desarrollador pone como privado porque es succeptible de cambiar en un futuro por cualquier tipo de implementación diferente que implique una mejora pero donde el resultado sea el mismo al fin y al cabo, por lo que si dispones de una función que recibe ‘A’ y esperas que devuelva ‘B’ tiene que darte igual como se consiga, siempre y cuando sea lo que se espera.

    Prueba a descargarte mi proyecto como ejemplo y haz las pruebas que veas necesarias, si crees que sigo equivocado, pues podemos hablar sobre el tema, y ver cual es la mejor opción, y si tienes cualquier duda puedo echarte una mano cuando haga falta.

    Un abrazo.

    • Carlos Benítez

      Muy interesante;
      le echaré un vistazo sin falta y lo comentamos.

      Cualquier conclusión que saque la incorporaré a estos tutoriales a modo de ejemplo.

      Saludos y gracias por compartir tus experiencias!

  3. Leifsk8er

    Interesante, a la par que los comentarios! Estaré atento a las conclusiones también :D. Por cierto Tomas, muy buena pinta JSONC.

    Me ha gustado el concepto de que el mismo TDD es el que te lleva a crear los métodos privados en el proceso de refactorizado y en realidad es código ya testeado. Si directamente se puso privado no seguiste la metodología TDD. Me gusta.

    Bajo mi punto de vista, totalmente novato y aprendiendo, cuanto menos tenga que alterar el código mejor, sin importar de si llamo a través del testeo o no, y siempre que el código no sepa nada de los tests, ni condiciones ni nada. Las conclusiones a las que llegáis encaminan a ello. Mola! 😀

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *