¿Es posible dividir un nombre de función largo en varias líneas?

81

Nuestro equipo de desarrollo utiliza un linter PEP8 que requiere una longitud máxima de línea de 80 caracteres .

Cuando escribo pruebas unitarias en Python, me gusta tener nombres de métodos descriptivos para describir lo que hace cada prueba. Sin embargo, esto a menudo me lleva a exceder el límite de caracteres.

Aquí hay un ejemplo de una función que es demasiado larga ...

class ClientConnectionTest(unittest.TestCase):

    def test_that_client_event_listener_receives_connection_refused_error_without_server(self):
        self.given_server_is_offline()
        self.given_client_connection()
        self.when_client_connection_starts()
        self.then_client_receives_connection_refused_error()

Mis Opciones:

  • ¡Podrías escribir nombres de métodos más cortos!

    Lo sé, pero no quiero perder el carácter descriptivo de los nombres de las pruebas.

  • ¡Puede escribir comentarios de varias líneas encima de cada prueba en lugar de usar nombres largos!

    Esta es una idea decente, pero luego no podré ver los nombres de las pruebas cuando las ejecute dentro de mi IDE (PyCharm).

  • Quizás pueda continuar las líneas con una barra invertida (un carácter de continuación de línea lógica).

    Desafortunadamente, esta no es una opción en Python, como se menciona en la respuesta de Dan.

  • Podría dejar de hacer sus pruebas.

    Esto tiene sentido de alguna manera, pero es bueno fomentar un conjunto de pruebas bien formateado.

  • Podría aumentar el límite de longitud de la línea.

    A nuestro equipo le gusta tener el límite porque ayuda a mantener el código legible en pantallas estrechas, por lo que esta no es la mejor opción.

  • Puede eliminar testdesde el principio de sus métodos.

    Esto no es una opción. El ejecutor de pruebas de Python necesita todos los métodos de prueba para empezar testo no los recogerá.

    Editar: algunos corredores de prueba le permiten especificar una expresión regular al buscar funciones de prueba, aunque prefiero no hacerlo porque es una configuración adicional para todos los que trabajan en el proyecto.

  • Puede separar EventListener en su propia clase y probarlo por separado.

    El Event Listener está en su propia clase (y está probado). Es solo una interfaz que se activa por eventos que ocurren dentro de ClientConnection. Este tipo de sugerencia parece tener una buena intención, pero está mal dirigida y no ayuda a responder la pregunta original.

  • Podrías usar un BDD Framework como Behave . Está diseñado para pruebas expresivas.

    Esto es cierto y espero utilizar más en el futuro. Aunque todavía me gustaría saber cómo dividir los nombres de las funciones entre líneas.

Por último...

¿Hay alguna forma en Python de dividir una declaración de función larga en varias líneas ?

Por ejemplo...

def test_that_client_event_listener_receives_
  connection_refused_error_without_server(self):
    self.given_server_is_offline()
    self.given_client_connection()
    self.when_client_connection_starts()
    self.then_client_receives_connection_refused_error()

¿O tendré que morder la bala y acortarlo yo mismo?

byxor
fuente
8
¿Por qué no utilizar una cadena de documentos de función descriptiva? Entonces podría imprimirlo confunc.__doc__
jakub
62
Deja de poner pelusa en tus pruebas unitarias.
John Kugelman
55
Entonces apaga esta regla. Es una locura menor que esté tratando de evitar esta regla de pelusa en lugar de simplemente desactivarla.
John Kugelman
13
Vuelva a visitar PEP8 python.org/dev/peps/pep-0008 , buenas razones para ignorar las pautas: When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this PEP.en su caso, usaría un nombre de función más corto.
Akavall
56
Hay dos problemas difíciles en informática, invalidación de caché, nombrar cosas y errores de uno por uno.
Surt

Respuestas:

78

No, esto no es posible.

En la mayoría de los casos, un nombre tan largo no sería deseable desde el punto de vista de la legibilidad y usabilidad de la función, aunque su caso de uso para los nombres de prueba parece bastante razonable.

Las reglas léxicas de Python no permiten que un solo token (en este caso, un identificador) se divida en varias líneas. El carácter de continuación de línea lógica ( \al final de una línea) puede unir varias líneas físicas en una sola línea lógica, pero no puede unir un solo token en varias líneas.

Dan Lenski
fuente
2
Es una pena. Aunque todavía siento que podría haber una solución mágica de alguna manera. --- Debo mencionar que probé la barra invertida en mi publicación, por si alguien me la menciona.
byxor
6
Lo mejor que puede hacer es utilizar su nombre descriptivo como msg kwarg arg en un método self.assert *. Si pasa la prueba, no lo verá. Pero si la prueba falla, su cadena descriptiva estará disponible en el objeto de resultado de la prueba.
B Rad C
11
Vale la pena señalar que no es exactamente una situación donde el uso del carácter de continuación de línea es aceptable: largas withdeclaraciones: with expr1 as x, \<newline> expr2 as y .... En todos los demás casos, por favor, sólo colocar la expresión entre paréntesis: (a_very_long <newline> + expression)funciona bien, y es mucho más fácil de leer y robusta entonces a_very_long \<newline> + expression... este último se rompe apenas añadiendo un solo espacio después de la barra invertida!
Bakuriu
3
@Bakuriu - ¡Guau! No sabía que no se podía envolver una withdeclaración en parens.
mattmc3
2
@ mattmc3 La razón es simple: no es una expresión. AFAIK, es literalmente el único caso en el que usar paréntesis para continuar en una nueva línea simplemente no es una opción.
Bakuriu
52

También podrías escribir un decorador que mute .__name__para el método.

def test_name(name):
    def wrapper(f):
        f.__name__ = name
        return f
    return wrapper

Entonces podrías escribir:

class ClientConnectionTest(unittest.TestCase):
    @test_name("test_that_client_event_listener_"
    "receives_connection_refused_error_without_server")
    def test_client_offline_behavior(self):
        self.given_server_is_offline()
        self.given_client_connection()
        self.when_client_connection_starts()
        self.then_client_receives_connection_refused_error()

confiando en el hecho de que Python concatena literales de cadena adyacentes a la fuente.

Sean Vieira
fuente
3
Esta es una muy buena idea. También parece muy legible. Lo intentaré ahora y veré si mi IDE muestra los nombres de funciones más largos.
byxor
2
Desafortunadamente, el decorador no se aplica antes de que se ejecute la prueba en PyCharm, lo que significa que no puedo ver los nombres descriptivos de mi corredor de pruebas.
byxor
2
Creo que querrás decorar wrappercon @functools.wraps(f).
2
Esta es la mejor solución para tener su pastel y comerlo también; combina todas las características que @BrandonIbbotson estaba buscando. Lástima que PyCharm aún no lo asimile.
Dan Lenski
3
Aún mejor, modifique el decorador para generar un nombre descriptivo a partir de la cadena de documentación de la función.
Nick Sweeting
33

Según la respuesta a esta pregunta: ¿Cómo deshabilitar un error pep8 en un archivo específico? , use el comentario final # nopep8o # noqapara desactivar PEP-8 para una línea larga. Es importante saber cuándo romper las reglas. Por supuesto, el Zen de Python le diría que "los casos especiales no son lo suficientemente especiales como para romper las reglas".

mattmc3
fuente
5
En realidad, es una idea fantástica porque me permite eliminar el resto de los archivos de prueba. Lo acabo de probar y funciona. También puedo conservar todos los beneficios de los nombres largos de métodos. --- Mi única preocupación es que al equipo no le gustará ver los # nopep8comentarios esparcidos durante las pruebas;)
byxor
8

Podemos aplicar el decorador a la clase en lugar del método, ya que unittestobtenemos el nombre de los métodos dir(class).

El decorador decorate_methodrevisará los métodos de clase y cambiará el nombre del método según el func_mappingdiccionario.

Pensé en esto después de ver la respuesta del decorador de @Sean Vieira, +1 de mí

import unittest, inspect

# dictionary map short to long function names
func_mapping = {}
func_mapping['test_client'] = ("test_that_client_event_listener_receives_"
                               "connection_refused_error_without_server")     
# continue added more funtion name mapping to the dict

def decorate_method(func_map, prefix='test_'):
    def decorate_class(cls):
        for (name, m) in inspect.getmembers(cls, inspect.ismethod):
            if name in func_map and name.startswith(prefix):
                setattr(cls, func_map.get(name), m) # set func name with new name from mapping dict
                delattr(cls, name) # delete the original short name class attribute
        return cls
    return decorate_class

@decorate_method(func_mapping)
class ClientConnectionTest(unittest.TestCase):     
    def test_client(self):
        # dummy print for testing
        print('i am test_client')
        # self.given_server_is_offline()
        # self.given_client_connection()
        # self.when_client_connection_starts()
        # self.then_client_receives_connection_refused_error()

La ejecución de prueba con unittestcomo se muestra a continuación mostró el nombre completo de la función descriptiva, cree que podría funcionar para su caso, aunque puede que no suene tan elegante y legible desde la implementación

>>> unittest.main(verbosity=2)
test_that_client_event_listener_receives_connection_refused_error_without_server (__main__.ClientConnectionTest) ... i am client_test
ok
Skycc
fuente
7

Una especie de enfoque específico del contexto del problema. El caso de prueba que ha presentado en realidad se parece mucho a un formato de lenguaje natural que describe los pasos necesarios para realizar un caso de prueba.

Vea si usar el behavemarco de estilo de desarrollo del controlador de comportamiento tendría más sentido aquí. Su "función" podría parecerse (ver cómo el given, when, thenreflejar lo que tenía):

Feature: Connect error testing

  Scenario: Client event listener receives connection refused error without server
     Given server is offline
      when client connect starts
      then client receives connection refused error

También hay un pyspecspaquete relevante , uso de muestra de una respuesta reciente sobre un tema relacionado:

Alecxe
fuente
Estaba pensando en mencionar que sabía que había opciones de BDD como behave. Sin embargo, no quería distraer demasiado a la gente en mi pregunta. Parece un marco realmente agradable y probablemente lo usaré en el futuro. De hecho, le pregunté a mi equipo si podía usarlo en este proyecto, pero no querían que las pruebas se vieran "raras";) --- Nunca había visto pyspecs antes. Gracias por la sugerencia.
byxor
1
@BrandonIbbotson te entendí, entiendo por qué no quisiste mencionarlo, tiene mucho sentido. pyspecs, por cierto, podría ser más fácil de integrar en su base de código de prueba - una forma más "python" de hacer BDD - no se necesitan estos archivos de características. ¡Gracias!
alecxe
5

La necesidad de este tipo de nombres puede insinuar otros olores.

class ClientConnectionTest(unittest.TestCase):
   def test_that_client_event_listener_receives_connection_refused_error_without_server(self):
       ...

ClientConnectionTestsuena bastante amplio (y en absoluto como una unidad comprobable), y es probable que sea una clase grande con muchas pruebas en su interior que podrían reenfocarse. Me gusta esto:

class ClientEventListenerTest(unittest.TestCase):
  def receives_connection_refused_without_server(self):
      ...

"Prueba" no es útil en el nombre porque está implícito.

Con todo el código que me ha dado, mi consejo final es: refactorice su código de prueba, luego revise su problema (si todavía está ahí).

BM
fuente
El detector de eventos es una interfaz. Los métodos en él se activan por cosas que suceden con ClientConnection. Ya se ha probado el detector de eventos por sí solo. Personalmente, creo que ClientConnection sigue SRP bastante bien, pero podría estar sesgado (y no puedes verlo). --- Los nombres de las pruebas de Python deben comenzar con testo el ejecutor de pruebas no los recoge.
byxor
1
@BrandonIbbotson Ah, lo entiendo ahora, estás probando que la conexión del cliente activa algo en el detector de eventos. Eso habría sido más obvio con un nombre como "test_that_connection_without_server_triggers_connection_refused_event". El requisito de la parte de "prueba" es terrible porque te obliga a elegir nombres raros o nombres llenos de pegamento inútil.
BM
Ese es un mejor nombre de método. Podría cambiar el nombre de un par de esos métodos como lo sugirió. Aunque probablemente todavía tenga muchos métodos con más de 80 caracteres
byxor
Por lo que veo, puedes anidar clases en Python. ¿El corredor de pruebas maneja eso? Tal vez pueda dividir el interior de ClientConnectionTest en temas que son clases anidadas que contienen pruebas relacionadas. De esa manera, la clase del tema lleva la parte del nombre que no necesita escribir en cada prueba.
BM
1
Sí, pensé que ese podría ser el caso. No estoy seguro de qué más sugerir entonces. Tal vez demos una oportunidad para extender el límite de caracteres de todos modos, lo hicimos nosotros mismos y al final nos dimos cuenta de que no era un gran problema y que todos tenían espacio para recibir más de 80 líneas de caracteres. ¡Buena suerte!
BM
4

La solución de nombre de función más corto tiene mucho mérito. Piense en lo que realmente se necesita en el nombre de su función real y lo que ya se proporciona.

test_that_client_event_listener_receives_connection_refused_error_without_server(self):

¿Seguro que ya sabes que es una prueba cuando la ejecutas? ¿Realmente necesitas usar guiones bajos? ¿Son realmente necesarias palabras como 'eso' para que se entienda el nombre? ¿Sería igual de legible el caso del camello? ¿Qué tal el primer ejemplo a continuación como una reescritura de lo anterior (número de caracteres = 79): Aceptar una convención para usar abreviaturas para una pequeña colección de palabras comunes es aún más efectivo, por ejemplo, Conexión = Conexión, Error = Err. Al usar abreviaturas, debe tener en cuenta el contexto y solo usarlas cuando no haya posibilidad de confusión - Segundo ejemplo a continuación. Si acepta que no hay una necesidad real de mencionar al cliente como sujeto de prueba en el nombre del método, ya que esa información está en el nombre de la clase, entonces el tercer ejemplo puede ser apropiado. (54) caracteres.

ClientEventListenerReceivesConnectionRefusedErrorWithoutServer (self):

ClientEventListenerReceivesConnRefusedErrWithoutServer (auto):

EventListenerReceiveConnRefusedErrWithoutServer (auto):

También estoy de acuerdo con la sugerencia de B Rad C "use un nombre descriptivo como msg kwarg arg en un self.assert" Solo debería estar interesado en ver el resultado de las pruebas fallidas cuando se ejecuta el conjunto de pruebas. La verificación de que tiene todas las pruebas necesarias escritas no debería depender de tener los nombres de los métodos tan detallados.

PD: Probablemente también eliminaría 'WithoutServer' como superfluo. ¿No debería el controlador de eventos del cliente recibir el evento en el caso de que no se pueda contactar con el servidor por algún motivo? (aunque tbh, creo que sería mejor que si el cliente no puede conectarse a un servidor, recibe algún tipo de 'conexión no disponible', la conexión rechazada sugiere que se puede encontrar el servidor pero rechaza la conexión en sí).

Charemer
fuente
1
TL; DR: compare la longitud de su respuesta con otra respuesta.
MarianD
3
MarianD: Lo siento, pero la respuesta fue dada para el OP, quien puede molestarse en tomarse un minuto para leerlo y abordó varias estrategias para acortar el nombre con ejemplos constructivos y fundamentos. Si desea la versión corta ... "Evite las palabras y la puntuación innecesarias y acorte las palabras comunes de manera consistente", ¿es lo suficientemente breve?
Charemer
3
Con la biblioteca unittest de python, cada método de prueba tiene que empezar, de lo testcontrario, el ejecutor de la prueba no lo capta.
byxor
1
@BrandonIbbotsontest_EventListenerReceiveConnRefusedErrWithoutServer(self):
Hendry
1
Me gusta CamelCase, pero creo que parece una violación de PEP 8: "Use las reglas de nomenclatura de funciones: minúsculas con palabras separadas por guiones bajos según sea necesario para mejorar la legibilidad".
Scooter