2012-05-25 6 views
19

Cuando escribo pruebas unitarias, a veces corto y pego una prueba y no recuerdo cambiar el nombre del método. Esto da como resultado la sobrescritura de la prueba anterior, que la oculta de manera efectiva y evita que se ejecute. Por ejemplo;¿Cómo puedo detectar nombres de métodos duplicados en una clase de python?

class WidgetTestCase(unittest.TestCase): 

    def test_foo_should_do_some_behavior(self): 
    self.assertEquals(42, self.widget.foo()) 

    def test_foo_should_do_some_behavior(self): 
    self.widget.bar() 
    self.assertEquals(314, self.widget.foo()) 

En este caso, solo se llamaría a la última prueba. ¿Hay alguna forma de detectar este tipo de error programáticamente, sin analizar el código fuente en bruto directamente?

+1

Copiar y pegar es generalmente un signo de mala práctica de codificación; la mayoría de las veces significa que debe extraer la funcionalidad en otra función. Te ahorrará tiempo, esfuerzo si quieres cambiarlo más tarde, y problemas como estos. –

+8

Lattyware: en general, estoy de acuerdo. Sin embargo, este es el código de prueba unitaria, en el que es una buena práctica y se espera que tenga un conjunto de métodos cortos, la mayoría de los cuales se parecen bastante, excepto por las diferentes condiciones de configuración y aserciones. Entonces, yo diría que cortar y pegar es lo normal para el curso y no representa una mala práctica. –

+0

@Scotty: ¿Por qué no te haces el hábito de cambiar el nombre de un método como tu primera acción después de copiarlo? Por cierto, si el código de configuración y las aserciones difieren para cada método, entonces lo único que realmente está copiando es el nombre del método, que tal vez haya notado. –

Respuesta

23

Si ejecuta pylint por encima de su código, se le informará cuando haya sobrescrito otro método:

Por ejemplo, me encontré con esto:

class A(object): 
    def blah(self): 
     print("Hello World!") 

    def blah(self): 
     print("I give up!") 

En this online pylint checker. Además de todas las cadenas de documentación que falta y tal, me sale esto:

E: 5:A.blah: method already defined line 2 
+3

O, si quieres un linter que no sea tan molesto, hay pyflakes: https://launchpad.net/pyflakes – habnabit

4

No se puede detectar fácilmente/limpiamente durante el tiempo de ejecución ya que el método anterior simplemente se reemplaza y se debería usar un decorador en cada definición de función. El análisis estático (pylint, etc.) es la mejor manera de hacerlo.

Sin embargo, es posible que pueda crear una metaclase que implemente __setattr__ y pruebe si se sobrescribe un método. - Acabo de probarlo y __setattr__ de la metaclase no se llama para cosas definidas en el bloque de clase.

+1

Triste que '__setattr__' no es una solución viable - sonó como un enfoque inteligente :) –

+1

Seguimiento en esto: http://stackoverflow.com/q/10762088/298479 – ThiefMaster

+1

Solo una aclaración rápida para lectores futuros, no es que la metaclase '__setattr__' no se llama para métodos, sino que solo se llama cuando se configuran atributos en la clase ** object **, que aún no existe cuando se enlazan los nombres definidos en el bloque de clases. Más detalles en la pregunta de seguimiento de ThiefMaster. – Ben

6

Aquí es una opción para la forma de detectar esto en tiempo de ejecución usando decoradores y sin necesidad de ninguna herramienta de análisis:

def one_def_only(): 
    names = set() 
    def assert_first_def(func): 
    assert func.__name__ not in names, func.__name__ + ' defined twice' 
    names.add(func.__name__) 
    return func 
    return assert_first_def 

class WidgetTestCase(unittest.TestCase): 
    assert_first_def = one_def_only() 

    @assert_first_def 
    def test_foo_should_do_some_behavior(self): 
    self.assertEquals(42, self.widget.foo()) 

    @assert_first_def 
    def test_foo_should_do_some_behavior(self): 
    self.widget.bar() 
    self.assertEquals(314, self.widget.foo()) 

Ejemplo de una intento de importar o ejecutar:

>>> import testcases 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "testcases.py", line 13, in <module> 
    class WidgetTestCase(unittest.TestCase): 
    File "testcases.py", line 20, in WidgetTestCase 
    @assert_first_def 
    File "testcases.py", line 7, in assert_first_def 
    assert func.__name__ not in names, func.__name__ + ' defined twice' 
AssertionError: test_foo_should_do_some_behavior defined twice 
14

Lo que sigue es un hack horrible que utiliza las características de Python documentadas e indocumentadas. Nunca debe alguna vez hacer algo como esto.

Ha sido probado en Python 2.6.1 y 2.7.2; no parece funcionar con Python 3.2 como está escrito, pero luego, puedes do this right en Python 3.x de todos modos.

import sys 

class NoDupNames(object): 

    def __init__(self): 
     self.namespaces = [] 

    def __call__(self, frame, event, arg): 
     if event == "call": 
      if frame.f_code.co_flags == 66: 
       self.namespaces.append({}) 
     elif event in ("line", "return") and self.namespaces: 
      for key in frame.f_locals.iterkeys(): 
       if key in self.namespaces[-1]: 
        raise NameError("attribute '%s' already declared" % key) 
      self.namespaces[-1].update(frame.f_locals) 
      frame.f_locals.clear() 
      if event == "return": 
       frame.f_locals.update(self.namespaces.pop()) 
     return self 

    def __enter__(self): 
     self.oldtrace = sys.gettrace() 
     sys.settrace(self) 

    def __exit__(self, type, value, traceback): 
     sys.settrace(self.oldtrace) 

Uso:

with NoDupNames(): 
    class Foo(object): 
     num = None 
     num = 42 

Resultado:

NameError: attribute 'num' already declared 

Cómo funciona: Nos conectamos con el gancho de rastreo del sistema. Cada vez que Python está a punto de ejecutar una línea, se nos llama. Esto nos permite ver qué nombres fueron definidos por la última instrucción ejecutada. Para asegurarnos de que podamos capturar duplicados, en realidad mantenemos nuestro propio diccionario de variables locales y borramos Python después de cada línea. Al final de la definición de la clase, copiamos nuestros locales en Python. Algunas de las otras tonterías están ahí para manejar las definiciones de clases anidadas y para manejar múltiples asignaciones en una sola declaración.

Como un inconveniente, nuestro "¡despeja TODOS los lugareños!"Enfoque significa que no se puede hacer esto:

with NoDupNames(): 
    class Foo(object): 
     a = 6 
     b = 7 
     c = a * b 

Porque hasta donde sabe Python, no hay nombres a y b cuando se ejecuta c = a * b; quitamos los tan pronto como vimos 'em Además, si. se asigna la misma variable en dos ocasiones en una sola línea (por ejemplo, a = 0; a = 1) no va a coger eso. sin embargo, funciona para las definiciones de clases más típicas.

Además, no se debe poner nada, además de las definiciones de clases dentro de una NoDupNames contexto. No sé qué va a pasar, tal vez nada malo. Pero no lo he intentado, así que en teoría el el universo podría ser aspirado en su propio agujero.

Este es posiblemente el código más malvado que he escrito, ¡pero seguro que fue divertido!

Cuestiones relacionadas