Czy można podzielić długą nazwę funkcji w wielu wierszach?

83

Nasz zespół programistów wykorzystuje linter PEP8, który wymaga maksymalnej długości linii 80 znaków .

Kiedy piszę testy jednostkowe w Pythonie, lubię mieć opisowe nazwy metod, aby opisać, co robi każdy test. Jednak często prowadzi to do przekroczenia limitu znaków.

Oto przykład funkcji, która jest zbyt długa ...

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

Moje opcje:

  • Możesz po prostu napisać krótsze nazwy metod!

    Wiem, ale nie chcę stracić opisowości nazw testów.

  • Możesz pisać wieloliniowe komentarze nad każdym testem zamiast używać długich nazw!

    To przyzwoity pomysł, ale wtedy nie będę mógł zobaczyć nazw testów podczas uruchamiania testów w moim IDE (PyCharm).

  • Być może możesz kontynuować wiersze za pomocą odwrotnego ukośnika (znak logicznej kontynuacji wiersza).

    Niestety nie jest to opcja w Pythonie, jak wspomniano w odpowiedzi Dana.

  • Możesz przestać lintować swoje testy.

    W pewnym sensie ma to sens, ale warto zachęcić do dobrze sformatowanego zestawu testów.

  • Możesz zwiększyć limit długości linii.

    Nasz zespół lubi mieć ten limit, ponieważ pomaga to zachować czytelność kodu na wąskich ekranach, więc nie jest to najlepsza opcja.

  • Możesz usunąć testz początku swoich metod.

    To nie jest opcja. Program uruchamiający testy Pythona potrzebuje na początek wszystkich metod testowych, w testprzeciwnym razie ich nie pobierze.

    Edycja: Niektóre programy uruchamiające testy umożliwiają określenie wyrażenia regularnego podczas wyszukiwania funkcji testowych, chociaż wolałbym tego nie robić, ponieważ jest to dodatkowa konfiguracja dla wszystkich pracujących nad projektem.

  • Możesz oddzielić EventListener do własnej klasy i przetestować ją oddzielnie.

    Odbiornik zdarzeń znajduje się we własnej klasie (i jest testowany). To tylko interfejs, który jest wyzwalany przez zdarzenia zachodzące w ClientConnection. Ten rodzaj sugestii wydaje się mieć dobre intencje, ale jest źle skierowany i nie pomaga w odpowiedzi na pierwotne pytanie.

  • Możesz użyć struktury BDD, takiej jak Behave . Jest przeznaczony do ekspresyjnych testów.

    To prawda i mam nadzieję, że w przyszłości wykorzystam ich więcej. Chociaż nadal chciałbym wiedzieć, jak podzielić nazwy funkcji na linie.

Ostatecznie...

Czy w Pythonie istnieje sposób na podzielenie długiej deklaracji funkcji na wiele wierszy ?

Na przykład...

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

A może sam będę musiał ugryźć kulę i skrócić ją?

byxor
źródło
8
Dlaczego nie użyć opisowej funkcji docstring? Wtedy możesz wydrukować to za pomocąfunc.__doc__
jakub
62
Przestań lintować testy jednostkowe.
John Kugelman
55
Następnie wyłącz tę regułę. To drobne szaleństwo, że tak bardzo starasz się obejść tę regułę kłaczków, a nie tylko ją wyłączyć.
John Kugelman
13
Ponownie odwiedź PEP8 python.org/dev/peps/pep-0008 , Dobre powody, aby zignorować wytyczne: w When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this PEP.twoim przypadku byłaby to krótsza nazwa funkcji.
Akavall
56
W informatyce są dwa poważne problemy: unieważnianie pamięci podręcznej, nazywanie rzeczy i błędy typu „off-by-one”.
Surt

Odpowiedzi:

79

Nie, to niemożliwe.

W większości przypadków tak długa nazwa byłaby niepożądana z punktu widzenia czytelności i użyteczności funkcji, chociaż Twój przypadek użycia nazw testów wydaje się całkiem rozsądny.

W leksykalne reguły Pythonie nie pozwalają pojedynczy znak (w tym przypadku identyfikator), aby być podzielony na kilka wierszy. Znak kontynuacji linii logicznej ( \na końcu linii) może łączyć wiele linii fizycznych w jedną linię logiczną, ale nie może łączyć pojedynczego tokenu w wielu liniach.

Dan Lenski
źródło
2
Jaka szkoda. Nadal czuję, że może istnieć magiczne rozwiązanie. --- Powinienem wspomnieć, że próbowałem użyć odwrotnego ukośnika w moim poście, na wypadek, gdyby ktoś mi o tym wspomniał.
byxor
6
Najlepszym rozwiązaniem jest użycie nazwy opisowej jako argumentu msg kwarg w metodzie self.assert *. Jeśli test się powiedzie, nie zobaczysz go. Ale jeśli test się nie powiedzie, opisowy ciąg będzie dostępny w obiekcie wyniku testu.
B Rad C
11
Warto zauważyć, że istnieje dokładnie jedna sytuacja, w której użycie znaku kontynuacji wiersza jest dopuszczalne: długie withwypowiedzi: with expr1 as x, \<newline> expr2 as y .... We wszystkich innych przypadkach, po prostu zawiń wyrażenie w nawias: (a_very_long <newline> + expression)działa dobrze, jest znacznie bardziej czytelne i solidniejsze niż a_very_long \<newline> + expression... to drugie przerywa, dodając pojedynczą spację po odwrotnym ukośniku!
Bakuriu,
3
@Bakuriu - Whoa! Nie wiedziałem, że nie możesz zawrzeć withoświadczenia w parens.
mattmc3
2
@ mattmc3 Powód jest prosty: to nie jest wyrażenie. AFAIK to dosłownie jedyny przypadek, w którym użycie nawiasów do kontynuacji w nowej linii po prostu nie jest opcją.
Bakuriu,
52

Państwo mogli również napisać dekorator że mutuje .__name__do tej metody.

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

Wtedy możesz napisać:

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

opierając się na fakcie, że Python konkatenuje sąsiadujące ze źródłem literały ciągów.

Sean Vieira
źródło
3
To bardzo dobry pomysł. Wygląda też bardzo czytelnie. Spróbuję teraz i zobaczę, czy moje IDE pokazuje dłuższe nazwy funkcji.
byxor
2
Niestety dekorator nie jest stosowany przed uruchomieniem testu w PyCharm, co oznacza, że ​​nie widzę opisowych nazw z mojego programu uruchamiającego testy.
byxor
2
Myślę, że chcesz ozdobić wrapperz @functools.wraps(f).
2
To najlepsze rozwiązanie, które pozwoli Ci zjeść ciastko i je zjeść; łączy w sobie wszystkie funkcje, których szukał @BrandonIbbotson. Szkoda, że ​​PyCharm jeszcze tego nie zrozumiał.
Dan Lenski
3
Co więcej, zmodyfikuj dekorator, aby wygenerował opisową nazwę z ciągu dokumentacyjnego funkcji.
Nick Sweeting,
33

Zgodnie z odpowiedzią na to pytanie: Jak wyłączyć błąd pep8 w określonym pliku? , użyj komentarza końcowego # nopep8lub, # noqaaby wyłączyć PEP-8 dla długiej linii. Ważne jest, aby wiedzieć, kiedy łamać zasady. Oczywiście Zen Pythona powiedziałby, że „Przypadki specjalne nie są na tyle wyjątkowe, aby łamać zasady”.

mattmc3
źródło
5
To naprawdę fantastyczny pomysł, ponieważ pozwala mi zszyć resztę plików testowych. Właśnie to przetestowałem i działa. Mogę również zachować wszystkie zalety długich nazw metod. --- Jedynym zmartwieniem jest to, że zespołowi nie spodoba się, gdy # nopep8komentarz zaśmieca się podczas testów;)
byxor
8

Możemy zastosować dekorator do klasy zamiast metody, ponieważ unittestpobierz nazwę metody z dir(class).

Dekorator decorate_methodprzejdzie przez metody klas i zmieni nazwę metody na podstawie func_mappingsłownika.

Pomyślałem o tym po zobaczeniu odpowiedzi dekoratora od @Sean Vieira, +1 ode mnie

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

test uruchomiony z unittestjak poniżej pokazał pełną, opisową nazwę funkcji, uważa, że ​​może działać w twoim przypadku, chociaż może nie brzmieć tak elegancko i czytelnie z implementacji

>>> unittest.main(verbosity=2)
test_that_client_event_listener_receives_connection_refused_error_without_server (__main__.ClientConnectionTest) ... i am client_test
ok
Skycc
źródło
7

Coś w rodzaju kontekstowego podejścia do problemu. Prezentowany przypadek testowy w rzeczywistości wygląda bardzo podobnie do formatu języka naturalnego opisującego kroki niezbędne do wykonania przypadku testowego.

Zobacz, czy użycie behavestruktury stylu programowania Behavior Driver miałoby większy sens w tym miejscu. Twój „cecha” może wyglądać następująco (patrz jak given, when, thenodzwierciedlają to, co trzeba było):

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

Istnieje również odpowiedni pyspecspakiet , przykładowe użycie z niedawnej odpowiedzi na pokrewny temat:

alecxe
źródło
Zastanawiałem się nad wspomnieniem, że wiedziałem, że istnieją opcje BDD behave. Jednak nie chciałem zbytnio rozpraszać ludzi swoim pytaniem. Wygląda na naprawdę fajny framework i prawdopodobnie użyję go w przyszłości. Właściwie zapytałem mój zespół, czy mógłbym go użyć w tym projekcie, ale nie chcieli, aby testy wyglądały „dziwnie”;) --- Nigdy wcześniej nie widziałem Pyspecs. Dzieki za sugestie.
byxor
1
@BrandonIbbotson rozumiem, rozumiem, dlaczego nie chciałeś o tym wspominać - ma sens. pyspecsNawiasem mówiąc, integracja z testowym kodem może być łatwiejsza - bardziej „pythonowy” sposób robienia BDD - te pliki funkcji nie są potrzebne. Dzięki!
alecxe
5

Potrzeba takich nazw może wskazywać na inne zapachy.

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

ClientConnectionTestbrzmi dość obszernie (i wcale nie jak jednostka testowalna) i prawdopodobnie jest dużą klasą z wieloma testami wewnątrz, które można by zmienić. Lubię to:

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

„Test” nie jest użyteczny w nazwie, ponieważ jest sugerowany.

Po całym kodzie, który mi dałeś, moja ostatnia rada brzmi: zrefaktoryzuj swój kod testowy, a następnie wróć do swojego problemu (jeśli nadal istnieje).

BM
źródło
Odbiornik zdarzeń to interfejs. Metody w nim zawarte są wyzwalane przez rzeczy, które dzieją się z ClientConnection. Przeprowadzono już samodzielne testowanie detektora zdarzeń. Osobiście uważam, że ClientConnection dość dobrze podąża za SRP, ale mogę być stronniczy (i nie widać tego). --- Nazwy testów w Pythonie muszą zaczynać się od testlub program uruchamiający testy ich nie odbiera.
byxor
1
@BrandonIbbotson Ach, rozumiem, testujesz, że połączenie klienta wyzwala coś w nasłuchiwaniu zdarzeń. Byłoby to bardziej oczywiste przy nazwie takiej jak „test_that_connection_without_server_triggers_connection_refused_event”. Wymóg dotyczący części „testowej” jest okropny, ponieważ zmusza cię do używania niezręcznych nazw lub nazw pełnych bezużytecznego kleju.
BM
To lepsza nazwa metody. Mogę zmienić nazwę kilku z tych metod, które zaproponowałeś. Chociaż prawdopodobnie nadal będę miał wiele metod ponad 80 znaków
byxor
Z tego, co widzę, możesz zagnieżdżać klasy w Pythonie. Czy biegacz testowy sobie z tym poradzi? Być może możesz podzielić wnętrze ClientConnectionTest na tematy, które są zagnieżdżonymi klasami zawierającymi powiązane testy. W ten sposób klasa tematu będzie zawierała część nazwy, której nie musisz pisać na każdym teście.
BM
1
Tak, pomyślałem, że tak może być. Nie wiem, co jeszcze zasugerować. Może i tak damy sobie radę z rozszerzeniem limitu znaków, zrobiliśmy to sami i ostatecznie zdaliśmy sobie sprawę, że to nie jest taka wielka sprawa i każdy miał miejsce na powitanie ponad 80 linii postaci. Powodzenia!
BM
4

Krótsza nazwa funkcji ma wiele zalet. Pomyśl o tym, co jest naprawdę potrzebne w twojej rzeczywistej nazwie funkcji i co jest już dostarczone.

test_that_client_event_listener_receives_connection_refused_error_without_server(self):

Na pewno już wiesz, że to test, kiedy go uruchomisz? Czy naprawdę musisz używać podkreśleń? czy słowa takie jak „to” są naprawdę potrzebne do zrozumienia nazwy? czy skrzynka wielbłąda byłaby równie czytelna? co powiesz na pierwszy przykład poniżej jako przepisanie powyższego (liczba znaków = 79): Jeszcze skuteczniejsze jest zaakceptowanie konwencji używania skrótów dla małego zbioru popularnych słów, np. Connection = Conn, Error = Err. Używając skrótów, należy uważać na kontekst i używać ich tylko wtedy, gdy nie ma możliwości pomylenia - drugi przykład poniżej. Jeśli zaakceptujesz, że nie ma potrzeby wspominania klienta jako podmiotu testowego w nazwie metody, ponieważ ta informacja znajduje się w nazwie klasy, trzeci przykład może być odpowiedni. (54) znaków.

ClientEventListenerReceivesConnectionRefusedErrorWithoutServer (self):

ClientEventListenerReceivesConnRefusedErrWithoutServer (self):

EventListenerReceiveConnRefusedErrWithoutServer (self):

Zgodziłbym się również z sugestią B Rad C „użyj nazwy opisowej jako argumentu msg kwarg w self.assert” Powinieneś być zainteresowany wyświetlaniem wyników testów zakończonych niepowodzeniem tylko wtedy, gdy uruchomiony jest zestaw testowy. Weryfikacja, że ​​masz napisane wszystkie niezbędne testy, nie powinna zależeć od posiadania tak szczegółowych nazw metod.

PS Prawdopodobnie również usunąłbym „WithoutServer” jako zbędny. Czy program obsługi zdarzeń klienta nie powinien odbierać zdarzenia w przypadku, gdy z jakiegokolwiek powodu nie można skontaktować się z serwerem? (chociaż myślę, że byłoby lepiej, gdyby klient nie mógł połączyć się z serwerem, otrzymywał pewnego rodzaju `` połączenie niedostępne '', odmowa połączenia sugeruje, że serwer można znaleźć, ale sam odmawia połączenia).

Charemer
źródło
1
TL; DR - porównaj długość swojej odpowiedzi z inną odpowiedzią.
MarianD
3
MarianD: Tak mi przykro, ale odpowiedź została udzielona OP, który może chcieć poświęcić chwilę na jej przeczytanie i odniósł się do kilku strategii skracania nazwy z konstruktywnymi przykładami i uzasadnieniem. Jeśli chcesz wersję skróconą… „Unikaj zbędnych słów i znaków interpunkcyjnych oraz konsekwentnie skracaj popularne słowa” - czy to wystarczająco krótkie?
Charemer
3
W przypadku biblioteki unittest w języku Python każda metoda testowa musi zaczynać się od, w testprzeciwnym razie program uruchamiający testy jej nie pobierze .
byxor
1
@BrandonIbbotsontest_EventListenerReceiveConnRefusedErrWithoutServer(self):
Hendry
1
Podoba mi się CamelCase, ale wydaje mi się, że jest to naruszenie PEP 8: „Użyj zasad nazewnictwa funkcji: małe litery ze słowami oddzielonymi podkreśleniami, jeśli to konieczne, aby poprawić czytelność.”
Scooter