Testing

Introducción al testing por unidades de prueba

Las test unit son trozos de código que se encargan de comprobar que bloques de funcionalidad (funciones, subsistemas, paquetes, etc.) funcionan como se espera. Para ello invocan a esos bloques de código con valores de entrada conocidos y comprueban que el comportamiento es el esperado.

De este modo cuando refactorizamos o cambiamos código nos aseguramos de que el código que antes funcionaba ahora sigue funcionando.

Además, fijada una interfaz es posible escribir test units *antes* de desarrollar el código, que no será completo hasta que pase todas esas test units.

Así, las test units deberían ser:

  • Exhaustivas en "código" (prueban todo el código que hay). Esto es automatizable de comprobar.
  • Exhaustivas en "rango" (comprueban todas las posibles entradas). Esto es mucho más difícil. No es lo mismo asegurarse que una función "multiplica" funciona para 3 * 4, que para nan (not a number) * MAX_INT. Deberían probar, al menos, todos los casos especiales (que pueden no tener código para tratarlas, que pueden derivarse de bugs, cuyo código para tratarlos no está bien escrito pero no ha fallado hasta ahora por no usarse, etc).

Una buena práctica es escribir una test unit por cada bug que se encuentra. Así, futuras revisiones del código no volverán a introducir bugs que ya existían por desconocimiento (por olvidarse un caso especial, por ejemplo, o por refactorizar a ciegas por no estar correctamente documentado).

Como inconveniente, son difíciles de escribir (al menos bien), y son código extra que debe mantenerse libre de errores y refactorizarse junto al código que prueban (así que necesitan mantenimiento además).

Testing en python

La test suite va en un archivo aparte a las clases del programa. Digamos que el criterio será test_NombreDeArchivoOriginal.

La filosofía separa la funcionalidad en:

"test fixture": preparación para un test. Base de datos temporal, datos de prueba, lo que sea. SE RESETEA ENTRE TESTS individuales.
"test case": lo que se prueba: salida esperada para una entrada (para una fixture en realidad) dada.
"test suite": varios test case o test suites. Recursivo :D
"test runner": lo que rula las test suites.
"test result": conceptualmente un "objeto resultado de los tests".

TestCase y FunctionTestCase implementan las test cases. La segunda es irrelevante a nuestros efectos. setUp() y tearDown() (sobrescribir, o pasar a FunctionTestCase) preparan y borran la fixture (¡ojo! solo la borran cuando el test ejecuta).

La suite de test queda implementada por TestSuite (duh)

Ejemplo:
import ModuloAProbar
import unittest

class nombreDelTest(unittest.TestCase): #heredamos de TestCase => esto es un testcase

    def setUp(self): 
        lo que haga falta

    def testCosaAProbarUno(self)
        self.assert_(true == true) #assert_(valor) casca si valor no es true

    def testCosaAProbarDos(self)
        a = true
        b = true #mola lo de no declarar variables :P

        self.assertEqual (a, b) #casca si a no es igual a b

    def testCosaAProbarTres(self)
        self.assertRaises(NombreExcepcion, huh?) # casca si no levantas la excepción esperada

    if __name__=='__main__'
        unittest.main() #maldad suma del ejemplo: así puedes correrlo desde la línea de comandos tb

La clave es que los test empiecen por "test"; así el testrunner sabe qué tests debe ejecutar… y con esos asserts (nótese que assert_() *NO* es lo mismo que assert() !! - ¡¡guión bajo!!. Debe ser para que no casque el testrunner con ese assert, seguramente lo reimplemente) se recopilan los resultados esperados. También hay funciones fail; bastaría mirarlo en la clase base unittest (la diferencia? ninguna. Creo que son renombrados solamente de los assert).

A nivel de resultados del testing, failure es si esperabas algo y te llega otra cosa distinta, mientras que un error es si te sale algo que *no* esperabas, tipo excepción o casque general.

La alternativa a todo eso es:

suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions) #decirle a TestLoader que cargue los tests que hay
unittest.TextTestRunner(verbosity=2).run(suite) #Correr en modo texto esta suite, con verbosidad = 2 (quite locuaz).

Y así va mostrando los test que va haciendo cuando se llama a esto desde otro lado (nótese el verbosity=2; aconsejo probar ambas opciones para ver la salida que generan, o meterse en el tutorial y verla).

Con esto ya tenemos para probar automáticamente

Metodología

Probar *todo* lo evidente. Aquí no importa ser bombero. Probar algo tipo 1==1 es absurdo (relativamente: puedes probar la igualdad con eso, así como que hay dos objetos creados…), pero probar algo del tipo hazTalCosa y luego assert(seHizoTalCosa) no lo es. por ejemplo: self.resize(150,100) y luego assert(window.size=(150,100)), o como sea la sintaxis; con eso se comprueba que el resize funcionó como debe.

Modo experto

Para el que quiera escribir menos y pensar más (pocos):

  • La clase principal del testing hereda de unittest.TestCase; así se puede implementar setUp en un único sitio para un conjunto de tests. Otra forma es heredar las clases de los tests de esa clase pincipal y sobrescribir sus métodos runTest . Pero para no acabar con cientos de clases de un solo método (el runTest), la alternativa es crear el objeto con métodos test*, y al construirlo entonces hay que decirle qué se quiere probar ( objetoTest('testPrimero'), nótese el string); eso usa el runTest por defecto y ya llama al método 'testPrimero'. ¡Para cada test, un nuevo objeto! con su nuevo setUp y tearDown.
  • tearDown se ejecuta si setUp se ejecutó (falle el runTest o no); para no ir dejando basurilla.
  • Agrupar cómodamente tests en conjuntos para enterarse luego es fácil:
def suite():
    suite = unittest.TestSuite() # instanciar una suite de tests
    suite.addTest(WidgetTestCase('testDefaultSize')) # añadir el test de nombre "testDefaultSize" de la clase WidgetTestCase
    suite.addTest(WidgetTestCase('testResize'))
    return suite #así sabe por dónde empezar

o incluso :

def suite():
    tests = ['testDefaultSize', 'testResize']

    return unittest.TestSuite(map(WidgetTestCase, tests)) #map mola, y si no preguntadle a Peña XD

y también vale

suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite([suite1, suite2])
  • y ya para muy comodones: suite = unittest.TestLoader().loadTestsFromTestCase(WidgetTestCase) busca por defecto -en el diccionario implícito de las clases, supongo… python tiene sus ventajas ;)- los métodos test* e instancia un self para cada función que encuentra.

Funciones útiles

http://docs.python.org/lib/testcase-objects.html

Los puntos suspensivos son solo para no copiar otra vez lo mismo; son los mismos parámetros. Donde no estén indicados, son los obvios.

assert_(...) / failUnless (expresion, [mensaje]) - el mensaje opcional, pero convendría
assertEqual(...) / failUnlessEqual (primero, segundo, [mensaje])
assertNotEqual / failIfEqual
assertAlmostEqual / failUnlessAlmostEqual (primo, secondo, [numero de decimales relevantes, [mensaje]]) - mi favorita XD
assertNotAl... os lo imaginais
assertRaises / failUnlessRaises(exception, callable, ...) - callable no es "que se calla", sino que significa que garantizas que se levanta una exception al llamar a la función "callable" con los parámetros que vienen detrás. 
failIf([msg]) - inversa de failUnless
fail([msg]) - cascar

Otros útiles:

failureException  - atributo de clase (este *NO* lleva parámetros) que devuelve la excepción que levantó test(). Por defecto es AssertionError. 
countTestCases() - imaginad. 
defaultTestReturn() - instancia de TestResult, que es un "objeto resultado". Quien quiera saber para qué vale, a leer la documentación. 
id() - string especificando el testCase actual
shortDescription() - duh.
  • Para acceder a TestResult (que se construyen solos): el objeto tiene estos atributos.
errors: lista de tuplas de 2 (duplas?): Instancias de TestCase, unidas a strings con las trazas (tracebacks), que dieron error. 
failures: Mismo pero para fallos
testsRun: nº de tests ejecutados. 
wasSuccesful()
stop() -> settear shouldStop a True. Por ejemplo, con un control C es lo que se hace en la implementación de TextTestRunner.

De lo que deducimos que TextTestRunner puede ser muy útil para backend, pero igual no tanto para gui…

Referencia

http://docs.python.org/lib/minimal-example.html

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License