Resultado inesperado ejecutando unittests

Gabriel Genellina gagsl-py2 en yahoo.com.ar
Dom Jul 29 18:52:30 CEST 2007


En Fri, 27 Jul 2007 20:10:57 -0300, Israel Fernández Cabrera  
<iferca en gmail.com> escribió:

> Estoy haciendo algunas pruebas para automatizar la corrida de unittest
> desde un IDE, brindando una GUI para ello pero en ese proyecto me
> encuentro con un problema que el código que a continuación adjunto
> ilustra:
>
> <código require="salvar en fichero de nombre import_tests.py">
> class PruebasDePrueba(unittest.TestCase):
>    def testUnTest(self):
>        a = 2
>        b = 1
>        self.assertEquals(a, b)
>
> def runTests():
>    loader = unittest.TestLoader()
>    result = unittest.TestResult()
>    suite = loader.loadTestsFromName("import_tests.PruebasDePrueba")
>    suite.run(result)
>    print "Errores: ", len(result.errors)
>    print "Fallos: ", len(result.failures)
>
> if __name__ == "__main__":
>    runTests()
>    raw_input("Modifique el test y presione ENTER para continuar")
>    runTests()
> </código>
>
> Notas: Ya se que eso de modificar un test para que corra no está
> correcto, es solo un ejemplo, no se prueba nada en absoluto, es solo
> un ejemplo.
>
> El objetivo de este código es ilustrar como corridas consecutivas de
> los unittest demuestran que a pesar de que la función "runTests()"
> recarga los tests utilizando unittest.TestLoader en la segunda corrida
> al parecer no se recarga nuevamente el tests sino que se utiliza la
> misma instancia que se cargó la primera vez.

El programa que esta ejecutandose, no se entera de que el fichero fuente  
fue modificado. En tu caso es mas dificil aun, porque esta todo en un  
mismo fuente. Supongamos, para hacerlo mas facil, que los tests estan en  
otro modulo, llamado modulo_tests.
El programa principal deberia hacer algo como `import modulo_tests` para  
poder usarlos. Una vez que un modulo fue importado correctamente, cuando  
se lo vuelve a importar, Python ya no intenta leer otra vez el fuente;  
simplemente se usa la referencia preexistente en sys.modules[]
Para obligarlo a que lo vuelva a leer, hay que usar la funcion reload.  
Pero reload no es una solucion magica: el codigo nuevo del modulo se  
carga, PERO NADA MAS SE MODIFICA. Suponiendo que el modulo definia clases,  
cualquier instancia de esas clases previamente construidas, siguen siendo  
de la clase "vieja". No van a "ver" los metodos nuevos o modificados.  
Cualquier referencia importada usando `from modulo import loquesea` va a  
seguir teniendo el valor viejo. Por eso en mi mensaje anterior te decia  
que es muy dificil -si no imposible- poder recargar un modulo arbitrario y  
hacer que todo sigua funcionando igual, sin conocerlo intimamente a él y a  
todos los que pudieran estar usandolo.

Un ejemplo. Supongamos que modulo.py tiene esto:
A = 1
lista = [1,2,3]

def mostrar_lista():
   print "lista=",lista

class MiClase:
   def nada(self):
     print "nada"

y hacemos lo siguiente:

py> import modulo
py> print modulo.A
1
py> print modulo.lista
[1, 2, 3]
py> from modulo import A, lista
py> print A
1
py> print lista
[1, 2, 3]
py> modulo.A is A
True
py> modulo.lista is lista
True
py> o1 = modulo.MiClase()
py> o1.nada()
nada
py> lista.append(4)
py> lista
[1, 2, 3, 4]
py> modulo.lista
[1, 2, 3, 4]

Ahora, modificamos externamente modulo.py para que quede asi:

A = 3
lista = [10,20,30]

def mostrar_lista():
   print "lista=",lista

class MiClase:
   def nada(self):
     print "algo"
   def nuevo(self):
     print "un metodo nuevo"

En el mismo interprete de Python anterior, veamos si se nota el cambio:

py> print modulo.A
1
py> print modulo.lista
[1, 2, 3]

No se enteró de las modificaciones. Usemos reload:

py> reload(modulo)
<module 'modulo' from 'modulo.py'>
py> modulo.A
3
py> modulo.lista
[10, 20, 30]

Parece que ahora sí tomó los cambios. Pero que pasa con los demas objetos?

py> lista
[1, 2, 3, 4]

Ooops... sigue con el valor anterior.

py> o1.nada()
nada
py> o1.nuevo()
Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
AttributeError: MiClase instance has no attribute 'nuevo'

o1 sigue viendo la definicion vieja del metodo "nada", y no se dio cuenta  
de que existe un metodo nuevo. Pero si construyo un objeto nuevo:

py> o2 = modulo.MiClase()
py> o2.nada()
algo
py> o2.nuevo()
un metodo nuevo

sí usa las nuevas definiciones. Y qué paso con la variable A?

py> modulo.A
3
py> A
1

Como se ve, no todo cambió. En parte se podria haber arreglado si hubiera  
re-ejecutado la linea `from module import A, lista` para que nuevamente  
"mi" A y "mi" lista coincidan con las del modulo. Pero en ese caso se  
pierde la modificacion que le hice a la lista. Pero eso no ayuda en el  
otro problema, que o1 sigue siendo de la clase "vieja".

En resumen: si se modifica el codigo fuente, Python no se entera a menos  
que usemos reload. Y aun usando reload, hay muchos detalles a tener en  
cuenta que hacen que no sirva en un caso generico como el que estas  
planteando (un framework de testeo donde se supone que el framework no  
sabe mucho sobre los detalles internos de los modulos que esta testeando).  
Asi que no hay una forma confiable de que un programa que se esta  
ejecutando "vea" las modificaciones hechas a un modulo, y se comporte  
igual que si dicha version modificada hubiese sido la que originalmente  
cargó.

> En la lista de python.org Gabriel (quien creo que también es
> suscriptor de python-es) me recomendó que ejecutara los tests desde un
> proceso diferente y que comunicara la GUI con este mediate IPC. Esa,
> claramente, podría ser una solución, pero me gustaría por ahora buscar
> una explicación a lo que me sucede y no un workaround para evitarlo.

(Si, soy el mismo!) Lo de ejecutar en un proceso separado, mas que un  
"workaround", me parece una "feature". De esa forma:
- los modulos testeados se ejecutan siempre en un entorno conocido y  
predecible
- no hay "remanentes" de ejecuciones anteriores que pudieran estorbar o  
alterar las pruebas
- el entorno de ejecucion de los tests esta aislado del entorno grafico  
del framework (que pasaria si los dos pretenden usar gtk.mainloop? o tu  
GUI usa gtk y algun test usa wxWindows, por ejemplo?)
- el entorno de ejecucion de los tests se parece mas al entorno  
definitivo; es decir, no esta "contaminado" con las clases y demas  
definiciones de la GUI de testeo.

La comunicacion entre ambos procesos no parece ser muy complicada. El  
framework solo deberia comunicar los tests que quiere que sean ejecutados;  
y por cada test, solo hay 3 respuestas posibles: pasa, falla, o dispara  
una excepcion inesperada. Y en principio eso es todo. Si quisieras poner  
una barra de progreso, por ejemplo, es cuestion de ir actualizandola a  
medida que llegan las respuestas de los tests.

-- 
Gabriel Genellina

------------ próxima parte ------------
_______________________________________________
Python-es mailing list
Python-es en aditel.org
http://listas.aditel.org/listinfo/python-es


Más información sobre la lista de distribución Python-es