{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!--\n",
    "5/11\n",
    "Cortes de control\n",
    "Pruebas Unitarias(Andres)\n",
    "CLASE DE LABORATORIO \n",
    "-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<!--\n",
    "# Corte de control\n",
    "\n",
    "TODO: Hacer\n",
    "-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Testing\n",
    "\n",
    "Cuando estamos desarrollando un programa, si hicimos las cosas con responsabilidad y ejecutamos el programa, hicimos algunas pruebas, etc, podemos afirmar que lo que hicimos, con mayor o menor seguridad, funciona. A partir de ese momento y hasta la próxima vez que se modifique el código, o que alguien encuentre una falla en el sistema (bug) podemos seguir manteniendo esa afirmación. <br>\n",
    "Supongamos que después de tiempo viene el usuario y nos pide un cambio en el funcionamiento del programa... una vez que modificamos el código para incorporar ese requerimiento del usuario tenemos que probar nuevamente todo el programa para asegurarnos que con esa modificación no rompimos nada. En programas chicos esto no tiene un gran impacto, pero si pensamos en un programa que actualmente se está usando y se estuvo desarrollando por años, un pequeño cambio podría requerir semanas de pruebas. Y es por eso que no todo el código que escribimos en nuestra vida profesional está destinado a ser ejecutado por el usuario final, gran parte, está destinado a asegurar que nuestro código funciona como nosotros decimos que funciona. <br>\n",
    "Existen varias formas de probar automáticamente nuestro código:\n",
    "* Pruebas unitarias\n",
    "* Pruebas de integración\n",
    "* Pruebas funcionales\n",
    "* Pruebas de regresión\n",
    "\n",
    "Pero por el momento nos centraremos en lo que son las pruebas unitarias. \n",
    "\n",
    "## Pruebas unitarias\n",
    "\n",
    "Las pruebas unitarias son catalogadas de caja blanca, ya que es necesario conocer el código para poder escribirlas. <br>\n",
    "Estas pruebas tienen que ser:\n",
    "\n",
    "* *Automatizables:* tienen que poder correrse todas las pruebas automáticamente sin intervención del usuario\n",
    "* *Repetibles:* se tienen que poder correrse varias veces y dar los mismos resultados\n",
    "* *Independientes:* la ejecución de una prueba no debe afectar al resto\n",
    "* *Aisladas:* la ejecución de una prueba no debe fallar por una falla ajena al código que estamos probando\n",
    "* *Con un único objetivo:* cada prueba debe tener como objetivo probar una única cosa\n",
    "\n",
    "Las ventajas de realizar pruebas unitarias son:\n",
    "\n",
    "* *Documentan el código:* al leer las pruebas puede entender qué es lo que intenta hacer el código\n",
    "* *Dan seguridad al momento hacer cambios:* al momento de incorporar nuevos requerimientos del usuario, o al realizar un *refactor*, se pueden correr las pruebas y asegurarse rápidamente si el código sigue funcionando como se esperaba o no.\n",
    "* *Los errores son más fáciles de encontrar:* al momento de correr las pruebas y encontrar que falla un test, sólo tenemos que ir a la función que prueba ese test y fijarnos si lo que está mal es el test o la función.\n",
    "* *Diseñan:* si las pruebas se escriben antes que el código que deben probar ayudan a comprender lo que tiene que hacer esa función y en cierto sentido ayudan a diseñar la solución a implementar\n",
    "\n",
    "### Etapas de una prueba\n",
    "\n",
    "Las etapas de una prueba unitaria son:\n",
    "\n",
    "1. *Setup*: Preparar el contexto para poder ejecutar el código a testear\n",
    "2. *Exercise*: Ejecución de la función a testear\n",
    "3. *Verify*: Verificar que el resultado obtenido es igual al resultado esperado\n",
    "\n",
    "### Cómo realizar las pruebas\n",
    "\n",
    "Al momento de desarrollarlas:\n",
    "\n",
    "1. se elige una función a probar\n",
    "2. se elige cuáles son sus parámetros de entrada\n",
    "3. en función de los parámetros de entrada, se determina cuáles son los parámetros de salida de esa función\n",
    "4. al definir los parámetros de entrada, podemos anticipar cual será el flujo de sentencias que se ejecutarán dentro de la función, por lo que podríamos iterar varias veces los puntos 2 y 3 hasta lograr pasar por todas las líneas de la función (nos ayuda a identificar código muerto), o, por lo menos, hasta alcanzar el nivel de cobertura deseado.\n",
    "\n",
    "Es importante tener en cuenta no sólo los casos felices, sino también los que tienen que mostrar algún mensaje de error para asegurarnos que el código maneje esos errores como corresponde.\n",
    "\n",
    "### En python\n",
    "\n",
    "En la versión 2.1 de python se incluyó el módulo [unittest](https://docs.python.org/2/library/unittest.html) que es uno de los que usualmente se utiliza para implementar estas pruebas. <br> \n",
    "La estructura de un archivo que contenga las pruebas debe ser:\n",
    "```Python\n",
    "# encoding: utf-8\n",
    "import unittest  # Importar el módulo unittest\n",
    "\n",
    "\n",
    "# Crear una clase que herede de unittest.TestCase\n",
    "class TestStringMethods(unittest.TestCase):\n",
    "\n",
    "    # Definir un método que comience con test\n",
    "    def test_upper_of_foo_will_return_foo_in_uppercase(self):\n",
    "        # Setup\n",
    "        target = 'foo'\n",
    "        expected_result = 'FOO'\n",
    "        \n",
    "        # Exercise\n",
    "        result = target.upper()\n",
    "    \n",
    "        # Verify\n",
    "        self.assertEqual(result, expected_result)\n",
    "\n",
    "    # Definir un método que comience con test\n",
    "    def test_isupper_of_upper_target_will_return_true(self):\n",
    "        # Setup\n",
    "        target = 'FOO'\n",
    "        \n",
    "        # Exercise\n",
    "        result = target.isupper()\n",
    "    \n",
    "        # Verify\n",
    "        self.assertTrue(result)\n",
    "\n",
    "    # Definir un método que comience con test\n",
    "    def test_isupper_of_capitalize_target_will_return_false(self):\n",
    "        # Setup\n",
    "        target = 'Foo'\n",
    "        \n",
    "        # Exercise\n",
    "        result = target.isupper()\n",
    "    \n",
    "        # Verify\n",
    "        self.assertFalse(result)\n",
    "\n",
    "\n",
    "# Esto es opcional, pero si se quiere ejecutar los tests\n",
    "# como python test_de_strings.py es necesario.\n",
    "if __name__ == '__main__':\n",
    "    unittest.main()\n",
    "```\n",
    "\n",
    "Y si suponemos que el archivo se llama `test_de_strings.py` y lo ejecutamos con el comando `python test_de_strings.py` nos mostrará en la consola:\n",
    "\n",
    "```\n",
    "...\n",
    "----------------------------------------------------------------------\n",
    "Ran 3 tests in 0.000s\n",
    "\n",
    "OK\n",
    "```\n",
    "\n",
    "Y si a la clase `TestStringMethods` le agregamos el siguiente método:\n",
    "\n",
    "```Python\n",
    "    def test_isupper_of_lower_target_will_return_true(self):\n",
    "        # Setup\n",
    "        target = 'foo'\n",
    "        \n",
    "        # Exercise\n",
    "        result = target.isupper()\n",
    "    \n",
    "        # Verify\n",
    "        self.assertTrue(result)\n",
    "```\n",
    "\n",
    "Va a fallar este nuevo test, ya que `result` valdrá *False*.\n",
    "\n",
    "```\n",
    ".F..\n",
    "======================================================================\n",
    "FAIL: test_isupper_of_lower_target_will_return_true (__main__.TestStringMethods)\n",
    "----------------------------------------------------------------------\n",
    "Traceback (most recent call last):\n",
    "  File \"test_de_strings.py\", line 50, in test_isupper_of_lower_target_will_return_true\n",
    "    self.assertTrue(result)\n",
    "AssertionError: False is not true\n",
    "\n",
    "----------------------------------------------------------------------\n",
    "Ran 4 tests in 0.001s\n",
    "\n",
    "FAILED (failures=1)\n",
    "```\n",
    "\n",
    "#### Descubriendo los tests\n",
    "Otra forma de ejecutar estos tests es, parado en la misma carpeta donde se encuentra el archivo, ejecutando el comando `python -m unittest discover` y en ese caso no es necesario poner al final del archivo las líneas:\n",
    "```Python\n",
    "if __name__ == '__main__':\n",
    "    unittest.main()\n",
    "```\n",
    "\n",
    "En realidad, no es necesario que se encuentren en la misma carpeta, lo que tiene que pasar es que se encuentre dentro del mismo [paquete](https://docs.python.org/2/tutorial/modules.html#packages). Y eso en python para eso se usan los archivos `__init__.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejemplo\n",
    "\n",
    "Supongamos que tenemos que hacer una función que parsea una línea de un archivo de texto sabiendo que es un archivo CSV (por lo que cada campo estará separado por una coma) y el formato es:\n",
    "```\n",
    "numero_de_partido,goles_local,goles_visitante # comentario\n",
    "```\n",
    "\n",
    "Donde:\n",
    "\n",
    "* `numero_de_partido`: es un número entero mayor a 1 (no tiene límite superior)\n",
    "* `goles_local` y `goles_visitante`: son los goles convertidos por cada uno de los equipos\n",
    "* A continuación de los goles del equipo visitante pueden venir, opcionalmente, una cantidad no determinada de espacios e, incluso, un comentario anteponiendo el caracter #.\n",
    "\n",
    "Dicha función tiene que retornar un diccionario con los campos provistos por el archivo. <br>\n",
    "\n",
    "Si pudieramos asumir que el archivo siempre tendrá líneas válidas, una posible solución podría ser:\n",
    "\n",
    "```Python\n",
    "def parsear_linea_prode(linea):\n",
    "    '''Función que no parsea una línea de un archivo\n",
    "    CSV con los resultados de un partido.\n",
    "    return: Diccionario con las claves numero_de_partido,\n",
    "    goles_local y goles_visitante.\n",
    "    '''\n",
    "\n",
    "    sin_comentario = linea.partition('#')[0]\n",
    "    sin_espacios = sin_comentario.strip()\n",
    "    id_partido, goles_loc, goles_vis = sin_espacios.split(',')\n",
    "    resultado = {\n",
    "       'numero_partido': int(id_partido),\n",
    "       'goles_local': int(goles_loc),\n",
    "       'goles_visitante': int(goles_vis)\n",
    "    }\n",
    "\n",
    "    return resultado\n",
    "```\n",
    "\n",
    "Para asegurarnos que nuestro código funciona correctamente podríamos agregar los siguientes test:\n",
    "\n",
    "```Python\n",
    "#!/usr/bin/env python\n",
    "# -*- coding: utf-8 -*-\n",
    "import unittest\n",
    "from prode import parsear_linea_prode\n",
    "\n",
    "\n",
    "class TestParsearLineasFixture(unittest.TestCase):\n",
    "\n",
    "    def test_parsear_linea_prode_parsea_bien_la_primer_linea(self):\n",
    "        # Setup\n",
    "        linea = '1,0,0'\n",
    "        resultado_esperado = {\n",
    "            'numero_partido': 1,\n",
    "            'goles_local': 0,\n",
    "            'goles_visitante': 0\n",
    "        }\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "    \n",
    "    def test_parsear_linea_prode_ignora_el_comentario_despues_del_numeral(self):\n",
    "        # Setup\n",
    "        linea = '1,0,0   # Chile vs Ecuador'\n",
    "        resultado_esperado = {\n",
    "            'numero_partido': 1,\n",
    "            'goles_local': 0,\n",
    "            'goles_visitante': 0\n",
    "        }\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_prode_ignora_el_enter_al_final_de_la_linea(self):\n",
    "        # Setup\n",
    "        linea = '1,0,0   # Chile vs Ecuador\\n'\n",
    "        resultado_esperado = {\n",
    "            'numero_partido': 1,\n",
    "            'goles_local': 0,\n",
    "            'goles_visitante': 0\n",
    "        }\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_para_valores_mayores_a_10_tambien_funciona(self):\n",
    "        # Setup\n",
    "        linea = '999,123,432   # Chile vs Ecuador\\n'\n",
    "        resultado_esperado = {\n",
    "            'numero_partido': 999,\n",
    "            'goles_local': 123,\n",
    "            'goles_visitante': 432\n",
    "        }\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    unittest.main()\n",
    "```\n",
    "\n",
    "Pero qué pasa si después nos agregan un requerimiento en el que dicen que, en \n",
    "\n",
    "```Python\n",
    "    def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_una_linea_vacia(self):\n",
    "        # Setup\n",
    "        linea = ''\n",
    "        resultado_esperado = {}\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_4_valores(self):\n",
    "        # Setup\n",
    "        linea = '1,2,3,4'\n",
    "        resultado_esperado = {}\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_2_valores(self):\n",
    "        # Setup\n",
    "        linea = '1,2'\n",
    "        resultado_esperado = {}\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_comentario_sin_numeral(self):\n",
    "        # Setup\n",
    "        linea = '1,2,3 comentario'\n",
    "        resultado_esperado = {}\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "\n",
    "    def test_parsear_linea_prode_retorna_un_diccionario_vacio_cuando_le_pasan_una_letra(self):\n",
    "        # Setup\n",
    "        linea = '1,a,3'\n",
    "        resultado_esperado = {}\n",
    "\n",
    "        # Exercise\n",
    "        resultado = parsear_linea_prode(linea)\n",
    "\n",
    "        # Verify\n",
    "        self.assertEquals(resultado, resultado_esperado)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "<!--\n",
    "## TDD (Test Driven Development)\n",
    "\n",
    "TDD es una metodología de desarrollo que se basa fuertemente en las pruebas unitarias. De hecho, sus dos principales reglas son:\n",
    "\n",
    "1. Primero se escriben los test, y luego, el código que cumple con esos test\n",
    "2. No se escribe código más allá del que es necesario para cumplir con la batería de test que se tengan por el momento\n",
    "-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Otros frameworks\n",
    "\n",
    "[Nosetest](https://nose.readthedocs.org/en/latest/) y [py.test](http://pytest.org/latest/). <br>\n",
    "Para más información se puede ver: http://docs.python-guide.org/en/latest/writing/tests/\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 2",
   "language": "python",
   "name": "python2"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
