Чи можна розбити довге ім'я функції на кілька рядків?


83

Наша команда розробників використовує ліній PEP8, який вимагає максимальної довжини рядка 80 символів .

Коли я пишу модульні тести на python, мені подобається мати описові імена методів, щоб описати, що робить кожен тест. Однак це часто призводить до того, що я перевищую ліміт символів.

Ось приклад занадто довгої функції ...

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

Мої варіанти:

  • Ви можете просто написати коротші назви методів!

    Я знаю, але я не хочу втрачати описовість назв тестів.

  • Ви можете писати багаторядкові коментарі над кожним тестом, а не використовувати довгі імена!

    Це гідна ідея, але тоді я не зможу побачити імена тестів під час запуску тестів у моїй IDE (PyCharm).

  • Можливо, ви можете продовжити рядки зворотною рискою рискою (логічний символ продовження рядка).

    На жаль, це не варіант у Python, як зазначено у відповіді Дана.

  • Ви можете припинити зв’язування тестів.

    Це певним чином має сенс, але приємно заохочувати добре відформатований набір тестів.

  • Ви можете збільшити обмеження довжини рядка.

    Нашій команді подобається мати обмеження, оскільки це допомагає тримати код читабельним на вузьких дисплеях, тому це не найкращий варіант.

  • Ви можете видалити testз початку ваші методи.

    Це не варіант. Для запуску тесту Python для початку потрібні всі методи тестування, testінакше він їх не підбере.

    Редагувати: Деякі тестові програми дозволяють вам вказати регулярний вираз під час пошуку тестових функцій, хоча я волів би не робити цього, оскільки це додаткове налаштування для всіх, хто працює над проектом.

  • Ви можете розділити EventListener на власний клас і протестувати його окремо.

    Слухач подій є у своєму власному класі (і тестується). Це просто інтерфейс, який запускається подіями, що відбуваються в ClientConnection. Цей тип пропозицій, схоже, має добрий намір, але неправильно спрямований і не допомагає відповісти на вихідне питання.

  • Ви можете використовувати BDD Framework, як Behave . Він призначений для виразних тестів.

    Це правда, і я сподіваюся використовувати їх у майбутньому. Хоча я все-таки хотів би знати, як розділити імена функцій на рядки.

Зрештою ...

Чи є в Python спосіб розділити довгу декларацію функції на кілька рядків ?

Наприклад...

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

Або мені доведеться сам кусати кулю і вкорочувати її?


8
Чому б не використовувати описову функцію docstring? Тоді ви могли б надрукувати його за допомогоюfunc.__doc__
jakub

62
Припиніть зв’язувати свої модульні тести.
Джон Кугельман,

55
Потім вимкніть це правило. Це незначне божевілля, що ви так стараєтесь обійти це правило ворсу, а не просто вимкнути його.
Джон Кугельман,

13
Перегляньте PEP8 python.org/dev/peps/pep-0008 , Вагомі причини для ігнорування вказівок: When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this PEP.у вашому випадку це буде використання коротшого імені функції.
Акавалл

56
Є дві важкі проблеми в галузі інформатики, анулювання кешу, іменування речей та поодинокі помилки.
Surt

Відповіді:


79

Ні, це неможливо.

У більшості випадків така довга назва буде небажаною з точки зору читабельності та зручності використання функції, хоча ваш варіант використання для імен тестів здається цілком обґрунтованим.

В лексичних правилах Python не дозволяють одному лексема (в даному випадку ідентифікатора) повинна бути розділена на кілька рядків. Символ продовження логічного рядка ( \в кінці рядка) може об’єднувати декілька фізичних рядків в один логічний рядок, але не може об’єднувати один маркер через кілька рядків.


2
Це ганьба. Я все ще відчуваю, що все-таки може бути чарівне рішення. --- Я повинен згадати, що я спробував зворотну скісну риску у своєму дописі, на всякий випадок, якщо хтось згадає це мені.
byxor

6
Найкраще - використовувати своє описове ім'я як аргумент msg kwarg у методі self.assert *. Якщо тест пройде, ви його не побачите. Але якщо тест не вдасться, ваш описовий рядок буде доступний на об'єкті результату тесту.
B Rad C

11
Варто відзначити , що існує рівно одна ситуації , коли використання символ продовження рядка прийнятно: довгі withвисловлювання: with expr1 as x, \<newline> expr2 as y .... У всіх інших випадках, будь ласка, просто оберніть вираз у дужках: (a_very_long <newline> + expression)чудово працює, набагато читабельніший і надійніший, ніж a_very_long \<newline> + expression... останній ламається, просто додавши один пробіл після зворотної скісної риски!
Бакуріу

3
@Bakuriu - Ого! Я не знав, що ти не withзможеш обговорити заяву в дужки.
mattmc3

2
@ mattmc3 Причина проста: це не вираз. AFAIK - це буквально єдиний випадок, коли використання дужок для продовження в новому рядку просто не є варіантом.
Бакуріу

52

Ви могли б також написати декоратор , що мутує .__name__для методу.

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

Тоді ви могли написати:

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

спираючись на той факт, що Python об'єднує сусідні рядкові літерали.


3
Це дуже гарна ідея. Це теж виглядає дуже читабельно. Я спробую це зараз і подивлюсь, чи мій IDE показує довші імена функцій.
byxor

2
На жаль, декоратор не застосовується до тестового запуску в PyCharm, тобто я не бачу описових імен від мого тестового бігуна.
byxor

2
Я думаю , що ви хочете прикрасити wrapperз @functools.wraps(f).

2
Це найкраще рішення «приготуй собі пиріг і з’їж його теж»; він поєднує в собі всі функції, які шукав @BrandonIbbotson. Шкода, що PyCharm ще не зовсім це відчуває.
Ден Ленскі

3
Ще краще, змінити декоратор, щоб створити описове ім'я з документації функції.
Нік Світінг

33

Відповідь на це запитання: Як вимкнути помилку pep8 у певному файлі? , використовуйте # nopep8або # noqaкінцевий коментар, щоб вимкнути PEP-8 для довгого рядка. Важливо знати, коли порушувати правила. Звичайно, дзен Пітона сказав би вам, що "Особливі випадки недостатньо особливі, щоб порушити правила".


5
Насправді це фантастична ідея, тому що вона дозволяє мені залишити решту тестових файлів. Я просто перевірив це, і воно працює. Я також отримую всі переваги довгих назв методів. --- Моє єдине занепокоєння полягає в тому, що команді не сподобається бачити # nopep8коментар, засмічений протягом усіх тестів;)
byxor

8

Ми можемо застосувати декоратор до класу замість методу, оскільки unittestотримуємо ім'я методу з dir(class).

Декоратор decorate_methodперейде до методів класу та перейменує назву методу на основі func_mappingсловника.

Думав про це, побачивши відповідь декоратора від @Sean Vieira, +1 від мене

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

тестовий запуск, unittestяк показано нижче, показав повну довгу описову назву функції, вважає, що це може працювати для вашого випадку, хоча це може здатися не таким елегантним і читабельним від реалізації

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

7

Своєрідно контекстний підхід до проблеми. Наведений вами тестовий випадок насправді дуже нагадує формат Natural Language, що описує необхідні кроки для проведення тестового кейсу.

Переконайтеся, що використання behaveфреймворку стилю розробки Behaviour Driver має тут більше сенсу. Ваша «особливість» може виглядати наступним чином (див , як given, when, thenвідображають те , що ви мали):

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

Існує також відповідний pyspecsпакет , зразок використання з нещодавньої відповіді на відповідну тему:


Я думав згадати, що я знав, що існують такі варіанти BDD behave. Однак я не хотів занадто відволікати людей у ​​своєму питанні. Це виглядає як дуже хороший фреймворк, і я, мабуть, буду використовувати його в майбутньому. Я насправді запитав свою команду, чи можу я використовувати його в цьому проекті, але вони не хотіли, щоб тести виглядали "дивно";) --- Я ніколи раніше не бачив pyspecs. Дякую за пропозицію.
byxor

1
@BrandonIbbotson gotcha, я розумію, чому ти не хотів про це згадувати - цілком логічно. pyspecs, до речі, може бути простіше інтегрувати у вашу тестову базу коду, хоча - більш "python" спосіб створення BDD - ці файли функцій не потрібні. Дякую!
alecxe

5

Потреба в таких назвах може натякати на інші запахи.

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

ClientConnectionTestзвучить досить широко (і зовсім не схоже на тестову одиницю), і, швидше за все, це великий клас з великою кількістю тестів, які можна перефокусувати. Подобається це:

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

"Тест" не є корисним у назві, оскільки це передбачає.

З усім кодом, який ви мені дали, моя остання порада: переробіть свій тестовий код, потім перегляньте свою проблему (якщо вона все ще є).


Прослуховувач подій - це інтерфейс. Методи в ньому ініціюються тим, що відбувається з ClientConnection. Тестування слухача події самостійно вже зроблено. Особисто я думаю, що ClientConnection досить добре дотримується SRP, але я міг би бути упередженим (і ви цього не бачите). --- Імена тестів Python повинні починатися з, testінакше тест-драйвер їх не підбирає.
byxor

1
@BrandonIbbotson Ах, я зрозумів зараз, ви перевіряєте, чи підключення клієнта запускає щось у прослуховувачі подій. Це було б більш очевидним із іменем типу "test_that_connection_without_server_triggers_connection_refused_event". Вимога до "тестової" частини жахлива, тому що вона змушує вас йти з незграбними іменами, або іменами, повними марного клею.
BM

Це краща назва методу. Я міг би перейменувати кілька із цих методів, як ви запропонували. Хоча у мене, мабуть, ще буде багато методів понад 80 символів
byxor

З того, що я бачу, ви можете вкладати класи в Python. Чи справляється з цим тестовий бігун? Можливо, ви можете розділити внутрішню частину ClientConnectionTest на теми, які є вкладеними класами, що містять відповідні тести. Таким чином клас теми містить частину назви, яку не потрібно писати на кожному тесті.
BM

1
Так, зрозумів, що це може бути так. Не знаю, що тоді ще запропонувати. Можливо, все-таки дамо можливість продовжити обмеження кількості персонажів, ми зробили це самі і врешті-решт зрозуміли, що це не така вже велика угода, і кожен мав місце вітати більше 80 символів. Удачі!
BM

4

Коротше рішення імені функції має багато переваг. Подумайте, що насправді потрібно у вашій фактичній назві функції та що вже поставляється.

test_that_client_event_listener_receives_connection_refused_error_without_server(self):

Напевно ви вже знаєте, що це тест, коли ви його запускаєте? Вам справді потрібно використовувати підкреслення? чи справді потрібні такі слова, як "що", щоб зрозуміти назву? чи був би випадок верблюда таким же читабельним? як щодо першого наведеного нижче прикладу як переписування вищезазначеного (кількість символів = 79): Прийняття конвенції щодо використання скорочень для невеликої колекції загальновживаних слів є ще більш ефективним, наприклад Connection = Conn, Error = Err. Використовуючи абревіатури, ви повинні пам’ятати про контекст і використовувати їх лише тоді, коли немає можливості плутати - другий приклад нижче. Якщо ви визнаєте, що немає фактичної потреби згадувати клієнта як тестового суб'єкта в назві методу, оскільки ця інформація міститься в назві класу, тоді третій приклад може бути доречним. (54) символів.

ClientEventListenerReceivesConnectionRefusedErrorWithoutServer (самостійно):

ClientEventListenerReceivesConnRefusedErrWithoutServer (самостійно):

EventListenerReceiveConnRefusedErrWithoutServer (самостійно):

Я також погоджуюсь з пропозицією B Rad C "використовувати описове ім'я як аргумент msg kwarg в self.assert". Вас повинно цікавити побачення результатів невдалих тестів лише під час запуску тестового набору. Перевірка того, що ви написали всі необхідні тести, не повинна залежати від наявності такої деталізації назв методів.

PS Я б, мабуть, також видалив "WithoutServer" як зайвий. Чи не повинен обробник подій клієнта отримувати подію у тому випадку, якщо сервер з якихось причин не встановлений для контакту? (хоча tbh я вважаю, що було б краще, якщо клієнт не зможе підключитися до сервера, він отримає якесь "підключення недоступне", підключення відмовлено говорить про те, що сервер можна знайти, але відмовляється від самого підключення.)


1
TL; DR - будь ласка, порівняйте тривалість вашої відповіді з іншою відповіддю.
MarianD

3
MarianD: Так вибачте, але відповідь була дана для ОП, який, можливо, потурбується взяти хвилину, щоб прочитати його, і розглянув кілька стратегій скорочення назви на конструктивних прикладах та обгрунтуваннях. Якщо ви хочете коротку версію ... "Уникайте зайвих слів та пунктуації та послідовно скорочуйте загальновживані слова" - це досить коротко?
Charemer

3
У бібліотеці unittest python кожен метод тестування повинен починатися з того, що в testіншому випадку тест-драйвер не піднімає його.
byxor

1
@BrandonIbbotsontest_EventListenerReceiveConnRefusedErrWithoutServer(self):
Hendry

1
Мені подобається CamelCase, але я думаю, що це, мабуть, є порушенням PEP 8: "Використовуйте правила іменування функцій: малі літери зі словами, розділеними підкресленнями, якщо це необхідно для поліпшення читабельності".
Скутер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.