Ist es möglich, einen langen Funktionsnamen über mehrere Zeilen zu teilen?

81

Unser Entwicklungsteam verwendet einen PEP8-Linter, der eine maximale Zeilenlänge von 80 Zeichen erfordert .

Wenn ich Unit-Tests in Python schreibe, möchte ich beschreibende Methodennamen haben, um zu beschreiben, was jeder Test tut. Dies führt jedoch oft dazu, dass ich die Zeichenbeschränkung überschreite.

Hier ist ein Beispiel für eine Funktion, die zu lang ist ...

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()

Meine Optionen:

  • Sie könnten einfach kürzere Methodennamen schreiben!

    Ich weiß, aber ich möchte die Beschreiblichkeit der Testnamen nicht verlieren.

  • Sie können über jeden Test mehrzeilige Kommentare schreiben, anstatt lange Namen zu verwenden!

    Dies ist eine anständige Idee, aber dann kann ich die Testnamen nicht sehen, wenn ich die Tests in meiner IDE (PyCharm) ausführe.

  • Vielleicht können Sie die Zeilen mit einem Backslash (einem logischen Zeilenfortsetzungszeichen) fortsetzen.

    Leider ist dies in Python keine Option, wie in Dans Antwort erwähnt.

  • Sie könnten aufhören, Ihre Tests zu färben.

    Dies ist in gewisser Hinsicht sinnvoll, aber es ist schön, eine gut formatierte Testsuite zu fördern.

  • Sie können die Zeilenlängenbeschränkung erhöhen.

    Unser Team mag das Limit, weil es hilft, den Code auf schmalen Displays lesbar zu halten, daher ist dies nicht die beste Option.

  • Sie können testvom Anfang Ihrer Methoden entfernen .

    Dies ist keine Option. Der Testläufer von Python benötigt zunächst alle Testmethoden, sonst werden testsie nicht erfasst.

    Bearbeiten: Bei einigen Testläufern können Sie bei der Suche nach Testfunktionen einen regulären Ausdruck angeben, obwohl ich dies lieber nicht tun möchte, da dies ein zusätzliches Setup für alle ist, die an dem Projekt arbeiten.

  • Sie können den EventListener in eine eigene Klasse aufteilen und separat testen.

    Der Event Listener ist in einer eigenen Klasse (und wird getestet). Es ist nur eine Schnittstelle, die durch Ereignisse in ClientConnection ausgelöst wird. Diese Art von Vorschlag scheint eine gute Absicht zu haben, ist jedoch fehlgeleitet und hilft nicht bei der Beantwortung der ursprünglichen Frage.

  • Sie können ein BDD-Framework wie Behave verwenden . Es ist für Ausdruckstests konzipiert.

    Dies ist wahr, und ich hoffe, dass ich in Zukunft mehr davon verwenden kann. Obwohl ich immer noch gerne wissen möchte, wie man Funktionsnamen über Zeilen verteilt.

Letzten Endes...

Gibt es in Python eine Möglichkeit, eine lange Funktionsdeklaration auf mehrere Zeilen aufzuteilen ?

Zum Beispiel...

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()

Oder muss ich die Kugel beißen und selbst kürzen?

byxor
quelle
8
Warum nicht eine beschreibende Funktion docstring verwenden? Dann könnten Sie es mitfunc.__doc__
jakub
62
Hören Sie auf, Ihre Unit-Tests zu färben.
John Kugelman
55
Schalten Sie dann diese Regel aus. Es ist ein kleiner Wahnsinn, dass Sie sich so sehr bemühen, diese Flusenregel zu umgehen, anstatt sie nur zu deaktivieren.
John Kugelman
13
Besuchen Sie PEP8 python.org/dev/peps/pep-0008 erneut . Gute Gründe, Richtlinien zu ignorieren: When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this PEP.In Ihrem Fall würde dies einen kürzeren Funktionsnamen verwenden.
Akavall
56
In der Informatik gibt es zwei schwierige Probleme: die Ungültigmachung des Caches, das Benennen von Dingen und Fehler nacheinander.
Surt

Antworten:

78

Nein das ist nicht möglich.

In den meisten Fällen wäre ein derart langer Name unter dem Gesichtspunkt der Lesbarkeit und Verwendbarkeit der Funktion unerwünscht, obwohl Ihr Anwendungsfall für Testnamen ziemlich vernünftig erscheint.

Die lexikalischen Regeln von Python erlauben nicht, dass ein einzelnes Token (in diesem Fall ein Bezeichner) auf mehrere Zeilen aufgeteilt wird. Das logische Zeilenfortsetzungszeichen ( \am Ende einer Zeile) kann mehrere physische Zeilen zu einer einzigen logischen Zeile verbinden, jedoch nicht ein einzelnes Token über mehrere Zeilen hinweg.

Dan Lenski
quelle
2
Das ist eine Schande. Ich habe immer noch das Gefühl, dass es irgendwie eine magische Lösung geben könnte. --- Ich sollte erwähnen, dass ich den Backslash in meinem Beitrag ausprobiert habe, nur für den Fall, dass jemand es mir gegenüber erwähnt.
Bis zum
6
Am besten verwenden Sie Ihren beschreibenden Namen als msg kwarg arg in einer self.assert * -Methode. Wenn der Test erfolgreich ist, wird er nicht angezeigt. Wenn der Test jedoch fehlschlägt, ist Ihre beschreibende Zeichenfolge für das Testergebnisobjekt verfügbar.
B Rad C
11
Es ist erwähnenswert, dass es genau eine Situation gibt, in der die Verwendung des Zeilenfortsetzungszeichens akzeptabel ist: lange withAnweisungen : with expr1 as x, \<newline> expr2 as y .... In allen anderen Fällen bitte, wickeln Sie einfach den Ausdruck in Klammern: (a_very_long <newline> + expression)funktioniert gut, und ist viel besser lesbar und robust dann a_very_long \<newline> + expression... die letzteren Pausen durch nur einen einzigen Raum nach dem umgekehrten Schrägstrich addieren!
Bakuriu
3
@ Bakuriu - Whoa! Ich wusste nicht, dass Sie eine withAussage nicht in Parens einwickeln können .
Mattmc3
2
@ mattmc3 Der Grund ist einfach: Es ist kein Ausdruck. AFAIK ist buchstäblich der einzige Fall, in dem die Verwendung von Klammern für die Fortsetzung einer neuen Zeile einfach keine Option ist.
Bakuriu
52

Sie können auch einen Dekorator schreiben, der .__name__für die Methode mutiert .

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

Dann könnten Sie schreiben:

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()

unter Berufung auf die Tatsache, dass Python an Quellen angrenzende String-Literale verkettet.

Sean Vieira
quelle
3
Das ist eine sehr gute Idee. Es sieht auch sehr gut lesbar aus. Ich werde es jetzt versuchen und sehen, ob meine IDE die längeren Funktionsnamen anzeigt.
Byxor
2
Leider wird der Dekorateur nicht angewendet, bevor der Test in PyCharm ausgeführt wird, was bedeutet, dass ich die beschreibenden Namen meines Testläufers nicht sehen kann.
Byxor
2
Ich denke , Sie wollen dekorieren wrappermit @functools.wraps(f).
2
Dies ist die beste Lösung, um Kuchen zu essen und zu essen. Es kombiniert alle Funktionen, nach denen @BrandonIbbotson gesucht hat. Schade, dass PyCharm es noch nicht ganz verstanden hat.
Dan Lenski
3
Ändern Sie den Dekorator noch besser, um einen beschreibenden Namen aus der Dokumentzeichenfolge der Funktion zu generieren.
Nick Sweeting
33

Gemäß der Antwort auf diese Frage: Wie deaktiviere ich einen pep8-Fehler in einer bestimmten Datei? Verwenden Sie den # nopep8oder den # noqanachfolgenden Kommentar, um PEP-8 für eine lange Zeile zu deaktivieren. Es ist wichtig zu wissen, wann die Regeln gebrochen werden müssen. Natürlich würde das Zen von Python Ihnen sagen, dass "Sonderfälle nicht speziell genug sind, um die Regeln zu brechen."

mattmc3
quelle
5
Das ist eigentlich eine fantastische Idee, weil ich damit den Rest der Testdateien fusseln kann. Ich habe es gerade getestet und es funktioniert. Ich kann auch alle Vorteile der langen Methodennamen behalten. --- Meine einzige Sorge ist, dass das Team den # nopep8Kommentar während der Tests nicht gerne sehen würde ;)
bis zum
8

Wir können den Dekorator anstelle der Methode auf die Klasse anwenden , da wir unittestden Methodennamen von erhalten dir(class).

Der Dekorateur decorate_methoddurchläuft die Klassenmethoden und benennt den Namen der Methode basierend auf dem func_mappingWörterbuch um.

Ich dachte daran, nachdem ich gesehen hatte, wie der Dekorateur von @Sean Vieira antwortete, +1 von mir

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()

Der Testlauf mit dem folgenden unittestNamen hat den vollständigen Namen der beschreibenden Funktion angezeigt. Er ist der Ansicht, dass er für Ihren Fall möglicherweise funktioniert, obwohl er in der Implementierung möglicherweise nicht so elegant und lesbar klingt

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

Eine Art kontextspezifischer Ansatz für das Problem. Der von Ihnen vorgestellte Testfall ähnelt tatsächlich einem Format in natürlicher Sprache, in dem die erforderlichen Schritte für einen Testfall beschrieben werden.

Prüfen Sie, ob die Verwendung des behaveFrameworks für den Entwicklungstyp des Verhaltenstreibers hier sinnvoller ist. Ihre „Feature“ aussehen könnte (wie die given, when, thenreflektieren , was man hatte):

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

Es gibt auch relevante pyspecsPaket- und Verwendungsbeispiele aus einer aktuellen Antwort zu einem verwandten Thema:

Alecxe
quelle
Ich dachte daran zu erwähnen, dass ich wusste, dass es BDD-Optionen wie gibt behave. Allerdings wollte ich die Leute in meiner Frage nicht zu sehr ablenken. Es sieht aus wie ein wirklich schönes Framework und ich werde es wahrscheinlich in Zukunft verwenden. Ich habe mein Team tatsächlich gefragt, ob ich es in diesem Projekt verwenden könnte, aber sie wollten nicht testen, ob es "komisch" aussieht;) --- Ich habe noch nie Pyspecs gesehen. Danke für den Vorschlag.
Bis zum
1
@BrandonIbbotson gotcha, ich verstehe, warum du es nicht erwähnen wolltest - macht vollkommen Sinn. pyspecsÜbrigens ist die Integration in Ihre Testcodebasis möglicherweise einfacher - eine "Python" -Methode für BDD -, aber diese Feature-Dateien werden nicht benötigt. Vielen Dank!
Alecxe
5

Die Notwendigkeit dieser Art von Namen kann auf andere Gerüche hinweisen.

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

ClientConnectionTestklingt ziemlich breit (und überhaupt nicht wie eine testbare Einheit) und ist wahrscheinlich eine große Klasse mit vielen Tests, die neu ausgerichtet werden könnten. So was:

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

"Test" ist im Namen nicht nützlich, weil es impliziert ist.

Bei all dem Code, den Sie mir gegeben haben, lautet mein letzter Rat: Überarbeiten Sie Ihren Testcode und wiederholen Sie Ihr Problem (falls er noch vorhanden ist).

BM
quelle
Der Ereignis-Listener ist eine Schnittstelle. Die darin enthaltenen Methoden werden durch Ereignisse mit ClientConnection ausgelöst. Das Testen des Ereignis-Listeners selbst wurde bereits durchgeführt. Persönlich denke ich, dass ClientConnection SRP ziemlich gut folgt, aber ich könnte voreingenommen sein (und Sie können es nicht sehen). --- Python-Testnamen müssen beginnen testoder der Testläufer nimmt sie nicht auf.
Bis zum
1
@BrandonIbbotson Ah, ich verstehe jetzt, Sie testen, ob die Clientverbindung etwas im Ereignis-Listener auslöst. Das wäre bei einem Namen wie "test_that_connection_without_server_triggers_connection_refused_event" offensichtlicher gewesen. Die "Test" -Teilanforderung ist schrecklich, weil sie Sie zwingt, entweder mit unangenehmen Namen oder Namen voller nutzlosen Klebstoffs zu arbeiten.
BM
Das ist ein besserer Methodenname. Ich könnte einige dieser Methoden umbenennen, wie Sie vorgeschlagen haben. Obwohl ich wahrscheinlich immer noch viele Methoden über 80 Zeichen haben werde
bis zum
Soweit ich weiß, können Sie Klassen in Python verschachteln. Behandelt der Testläufer das? Möglicherweise können Sie die Innenseiten von ClientConnectionTest in Themen aufteilen, bei denen es sich um verschachtelte Klassen handelt, die verwandte Tests enthalten. Auf diese Weise trägt die Klasse des Themas den Teil des Namens, den Sie nicht bei jedem Test schreiben müssen.
BM
1
Ja, ich dachte, das könnte der Fall sein. Ich bin mir nicht sicher, was ich sonst noch vorschlagen soll. Vielleicht versuchen Sie es trotzdem mit einer Erweiterung des Zeichenlimits. Wir haben das selbst gemacht und am Ende festgestellt, dass es keine so große Sache ist und jeder Platz hatte, um mehr als 80 Zeichenzeilen zu begrüßen. Viel Glück!
BM
4

Die Lösung mit kürzeren Funktionsnamen hat viele Vorteile. Überlegen Sie, was in Ihrem tatsächlichen Funktionsnamen wirklich benötigt wird und was bereits geliefert wird.

test_that_client_event_listener_receives_connection_refused_error_without_server(self):

Sicher wissen Sie bereits, dass es ein Test ist, wenn Sie ihn ausführen? Müssen Sie wirklich Unterstriche verwenden? Sind solche Wörter wirklich erforderlich, damit der Name verstanden wird? Wäre Kamelkoffer genauso lesbar? Wie wäre es mit dem ersten Beispiel unten als Umschreibung des Obigen (Zeichenanzahl = 79): Das Akzeptieren einer Konvention zur Verwendung von Abkürzungen für eine kleine Sammlung gebräuchlicher Wörter ist noch effektiver, z. B. Verbindung = Verbindung, Fehler = Fehler. Wenn Sie Abkürzungen verwenden, müssen Sie den Kontext berücksichtigen und sie nur verwenden, wenn keine Verwechslungsgefahr besteht - zweites Beispiel unten. Wenn Sie akzeptieren, dass der Client im Methodennamen nicht als Testobjekt angegeben werden muss, da sich diese Informationen im Klassennamen befinden, ist das dritte Beispiel möglicherweise angemessen. (54) Zeichen.

ClientEventListenerReceivesConnectionRefusedErrorWithoutServer (self):

ClientEventListenerReceivesConnRefusedErrWithoutServer (self):

EventListenerReceiveConnRefusedErrWithoutServer (self):

Ich würde auch dem Vorschlag von B Rad C zustimmen: "Verwenden Sie einen beschreibenden Namen als msg kwarg arg in in einem self.assert." Sie sollten nur daran interessiert sein, die Ausgabe von fehlgeschlagenen Tests zu sehen, wenn die Testsuite ausgeführt wird. Die Überprüfung, ob Sie alle erforderlichen Tests geschrieben haben, sollte nicht davon abhängen, ob die Methodennamen so detailliert sind.

PS Ich würde wahrscheinlich auch 'WithoutServer' als überflüssig entfernen. Sollte der Client-Ereignishandler das Ereignis nicht empfangen, falls der Server aus irgendeinem Grund nicht erreichbar ist? (Obwohl ich denke, dass es besser wäre, wenn der Client keine Verbindung zu einem Server herstellen kann, erhält er eine Art "Verbindung nicht verfügbar". Die verweigerte Verbindung deutet darauf hin, dass der Server gefunden werden kann, lehnt jedoch die Verbindung selbst ab.)

Charemer
quelle
1
TL; DR - Bitte vergleichen Sie die Länge Ihrer Antwort mit einer anderen Antwort.
MarianD
3
MarianD: Tut mir leid, aber die Antwort wurde für das OP gegeben, das sich möglicherweise die Mühe macht, sich eine Minute Zeit zu nehmen, um es zu lesen, und verschiedene Strategien zur Verkürzung des Namens mit konstruktiven Beispielen und Begründungen angesprochen. Wenn Sie die Kurzversion wollen ... "Vermeiden Sie unnötige Wörter und Satzzeichen und kürzen Sie gebräuchliche Wörter konsequent" - ist das kurz genug?
Charemer
3
Bei der unittest-Bibliothek von Python muss jede Testmethode mit beginnen, testsonst nimmt der Testläufer sie nicht auf.
Byxor
1
@BrandonIbbotsontest_EventListenerReceiveConnRefusedErrWithoutServer(self):
Hendry
1
Ich mag CamelCase, aber ich denke, es scheint eine Verletzung von PEP 8 zu sein: "Verwenden Sie die Regeln für die Benennung von Funktionen: Kleinbuchstaben mit durch Unterstriche getrennten Wörtern, um die Lesbarkeit zu verbessern."
Scooter