Поиск хороших имен — это одна из задач разработки программного обеспечения. И вам нужно найти их все время и для всего — классы, методы, переменные, и это лишь некоторые из них. Но что делает имя хорошим именем? По словам Oncle Bob: «Три вещи: читабельность, читаемость и читаемость!» Который он определяет позже один ясностью, простотой и плотностью выражения 1 .
Хотя это имеет смысл для меня, я наблюдаю за тем, как я, в частности, немного борюсь с именами тестовых методов. Чтобы лучше понять, о чем я говорю, нужно знать, что я пишу свой код, управляемый тестом . И делая это какое-то время, я постепенно менял фокус своей работы с тестируемого устройства на сам тест. Вероятно, это потому, что мне нравится думать о тестовом примере как о живой спецификации и гарантии качества в одном экземпляре и, следовательно, о том, что это жизненно важно 2 .
Поэтому, когда бы тест не проходил, в идеале я мог бы сразу увидеть, какая спецификация была нарушена и почему. И лучший способ достичь этого, по-видимому, — найти выразительное имя теста, потому что это первая информация, отображаемая в представлении отчетов:
С этой точки зрения я не всегда доволен тем, что появляется в этой точке зрения, поэтому я потратил немного времени на исследования, чтобы понять, какая школа мысли может быть полезной. К сожалению, большинство результатов, которые я нашел, были несколько устаревшими и — менее удивительными — мнения по этой теме разделились. Этот пост представляет мои размышления, основанные на этих выводах и небольшом личном опыте.
Тесты по методам или именам тестов поведения?
В чистом виде подход «тесты по методу» часто обеспечивается инструментами, которые, например, генерируют одну заглушку теста после факта. Если у вас есть класс Foo
с bar
методов, сгенерированный метод будет называться testBar
. Я всегда скептически относился к полезности такого стиля разработки или соглашения об именах и утверждал бы, как эта цитата из старого потока JavaRanch: «вы вообще не должны думать об этом как о методах тестирования, вы должны думать об этом как о поведении тестирования» класса. Следовательно, мне нравятся имена моих тестовых методов, чтобы сообщить об ожидаемом поведении » 3 .
Интересно, что я собираюсь немного изменить свое мнение на этот счет. Идея передачи «поведения», как указано выше, требует найти краткое имя, которое выражает это «поведение» всесторонне. Но тогда термин «поведение» подразумевает переход из одного состояния в другое, осуществляемое действием или обозначаемое в терминах BDD, например, шаблон «Given-When-Then». Честно говоря, я не думаю, что в целом это хорошая идея, чтобы поместить всю эту информацию в одно имя 4 :
1
2
3
4
5
6
7
8
9
|
@Test public void givenIsVisibleAndEnabledWhenClickThenListenerIsNotified() {} @Test public void givenIsVisibleAndNotEnabledWhenClickThenListenerIsNotNotified() {} @Test public void givenIsNotVisibleAndEnabledWhenClickThenListenerIsNotNotified() {} |
Может быть, это просто вопрос вкуса, но по моему опыту этот подход часто не читается из-за отсутствия простоты и / или ясности, независимо от того, какой стиль формата я выбрал. Кроме того, такие перегруженные имена, как правило, имеют ту же проблему, что и комментарии — имена легко устаревают по мере развития контента. Из-за этого я предпочел бы использовать шаблон BUILD-OPERATE-CHECK 5 . Это позволит разделить фазы на отдельные имена под-методов, размещенные в одном тесте:
01
02
03
04
05
06
07
08
09
10
11
|
@Test public void testNameHasStillToBeFound() { // do what is needed to match precondition givenIsVisibleAndEnabled(); // execute the transition whenClick(); // verify the expected outcome thenListenerIsNotified(); } |
К сожалению, это приводит нас к тому, с чего мы начали. Но если вы внимательно посмотрите на приведенные выше примеры, все методы группируются вокруг общего знаменателя. Все они принадлежат к одному действию, которое запускает переход. В нашем случае событие клика. Учитывая, что с точки зрения процесса разработки я считаю тестовый пример более важным, чем тестируемый модуль, его можно интерпретировать как знак, отражающий действие с помощью соответствующего имени метода в разрабатываемом модуле 6 .
Итак, для примера предположим, что у нас есть ClickAction
который оборачивается вокруг ClickAction
управления пользовательского интерфейса. И введение метода под названием ClickAction#execute()
может показаться нам подходящим, учитывая ситуацию выше. Поскольку простота имеет значение, мы могли бы использовать это имя также для метода теста, который представляет переход от состояния по умолчанию конструкции ClickAction
— control через ClickAction#execute()
:
01
02
03
04
05
06
07
08
09
10
11
12
|
class ClickActionTest { @Test public void execute() { Control control = mock( Control. class ); ClickAction clickAction = new ClickAction( control ); clickAction.execute(); verify( control ).notifyListeners(...) } } |
Для простоты имя следующего теста может содержать только информацию о состоянии, которая важна, поскольку она отличается от значения по умолчанию и приводит к другому результату:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
class ClickActionTest { [...] @Test public void executeOnDisabledControl() { Control control = mock( Control. class ); when( control.isEnabled() ).thenReturn( false ); ClickAction clickAction = new ClickAction( control ); clickAction.execute(); verify( control, never() ).notifyListeners(...) } @Test public void executeOnInvisibleControl() { [...] } |
Как вы можете видеть, этот подход приводит к набору имен тестов, которые в техническом плане представляют собой множество шаблонов «тесты на метод», но не по совершенно плохим причинам, как мне кажется. Учитывая контекст, который я считаю, этот шаблон именования прост, понятен и выразителен до одной точки:
Ожидаемый результат теста до сих пор не упомянут вообще. На первый взгляд это выглядит неудовлетворительно, но с моей нынешней точки зрения я готов принять это как разумный компромисс. Тем более что причина неудачного теста обычно также указывается в представлении отчетов JUnit. Из-за этого эта проблема может быть решена путем предоставления значимых сбоев теста 7 .
Вывод
На самом деле я использую тестовый шаблон именования, описанный выше в течение некоторого времени. Пока все работает не так уж плохо. В частности, при работе с довольно маленькими модулями, как я обычно делаю, мало места для неправильного толкования. Однако этот подход не соответствует всем случаям, а иногда он просто чувствует себя лучше и все еще достаточно читабелен, чтобы упомянуть результат. Я не буду говорить о принципах здесь и, возможно, я все неправильно понимаю. Так что я был бы рад любым указателям на более сложные подходы, которые вы могли бы знать, чтобы расширить мою точку зрения.
- Роберт К. Мартин о чистых тестах, Чистом коде, Глава 9 Модульные тесты ↩
- Что может быть хуже: потеря тестируемого устройства или тестового случая? В случае хорошего тестового примера восстановление устройства в большинстве случаев должно быть простым, однако, наоборот, вы можете легко пропустить один из угловых случаев, которые были указаны в потерянном тестовом примере.
- Соглашение об именах для методов с JUnit, Соглашение об именах для методов с JUnit ↩
- Чтобы избежать недоразумений: BDD не делает ничего подобного и поставляется с собственной платформой тестирования. Я только что упомянул это здесь, так как термин «поведение», по-видимому, подсказывает это, а термин «данный, когда» появляется во многих дискуссиях о названиях тестов. Однако на самом деле вы найдете такие предложения, как соглашения Роя Ошерова об именах, помеченные «UnitOfWork_StateUnderTest_ExpectedBehavior», которые все еще кажутся хорошо принятыми, хотя пост видел большинство дней последнего десятилетия ↩
- Роберт К. Мартин, Чистый код, Глава 9, Чистые тесты ↩
- Или даже выделить весь функционал в отдельный класс. Но этот случай описан в моем посте More Units with MoreUnit ↩
- Возможно, это отдельная тема, и так как я должен закончить, я оставлю это так! ↩
Ссылка: | Получение имен тестов JUnit прямо от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine . |