Комментарии 8
Лучше тестировать состояние. В вашем примере можно сделать состояние логгера доступным для теста и проверять его с помощью assertThat.
(И еще сильно режут глаза интерфейсы, начинающиеся с I — вы до этого на Delphi писали?)
Mockito утверждает, что это вообще-то антипаттерн
Да, это также упоминается в javadoc-е Mockito.verifyNoMoreInteractions(Object...)
. Но я считаю, что для систем, от которых ожидается чётко регламентированное поведение (как, например, строгое журналироване в тексте статьи), такая практика вполне даже применима и может быть использована именно во благо, а не во вред, о чём говорят разработчики Mockito, ссылаясь на возможное злоупотребление этой возможностью. Например, если метод тестируемого юнита обращается к какому-либо другому юниту и это влияет на состояние сообщения, которое планируется отправить в журнал — тест может указать на возможные проблемы во взаимодействии компонент между собой. И если это не считать проблемами, то тест может фактически выступать в роли документа, формально описывающего ожидаемые результаты взаимодействия нескольких компонент (если считать таковыми тесты, конечно). Намеренное игнорирование Mockito.verifyNoMoreInteractions(Object...)
можно сравнить с намеренным подавлением предупреждений компилятора или других инструментов анализа.
Лучше тестировать состояние. В вашем примере можно сделать состояние логгера доступным для теста и проверять его с помощью assertThat.
Возможно, но мне такое утверждение кажется весьма спорным: здесь логгер выступает в роли компонента с write-only семантикой, и я бы не хотел видеть, как он предоставляет доступ к своему временному состоянию (имеется ввиду время жизни с begin()
до log(...)
) даже с помощью простейшего .contains(LogEntryKey key, Object value)
. Кроме того, тест с помощью Mockito позволяет гарантировать, что логгер собрал не только данные для следующей записи в журнале (т.е., состояние), а также и отослал эти данные куда-то (log(...)
). Можно возразить, что и log(...)
можно реализовать так, чтобы он выставлял некоторое состояние, описывающее факт "отосланного сообщения", но тогда наверняка пришлось бы пожертвовать или чистотой интерфейса, добавив в него что-то типа isLogged()
, или в тестах завязываться на конкретную реализацию и каким-то образом узнавать о таком флажке (пусть даже приватном). Подход с Mockito, я считаю, более естественнен.
(И еще сильно режут глаза интерфейсы, начинающиеся с I — вы до этого на Delphi писали?)
Не полностью по Java, да. На самом деле это прямо позаимствовано из C#/.NET (я, честно говоря, с Delphi только TFoo
помню). Мне кажется, это немного елегантнее, чем FooImpl
, BarImpl
и BazImpl
, которые реализуют один и тот же интерфейс. Плюс, с практической точки зрения, такие имена удобнее читать за компьютерами коллег, которые принебрегают возможностями подстветки, или в системах, в которых такая возможность отсутствует вообще.
логгер выступает в роли компонента с write-only семантикой
Не обязательно использовать Mockito для этого. Если логгер — это интерфейс, то можно написать свою имплементацию, которая собирает вызовы в журнал, который торчит наружу. И можно просто проверять через assertThat(logger.getActions(), has(<list of stuff>))
Мне кажется, так будет короче и проще, чем у вас. У вас очень похоже на overengineering.
Не полностью по Java, да
Дело вкуса, однако я за всю карьеру ни разу не сталкивался с IInterface на Java.
Я согласен вот с этими доводами https://stackoverflow.com/questions/541912/interface-naming-in-java
Если логгер — это интерфейс, то можно написать свою имплементацию, которая собирает вызовы в журнал, который торчит наружу. Мне кажется, так будет короче и проще, чем у вас.
Вот как раз этого я и пытаюсь избежать, поэтому предпочитаю тестирование поведения в таких случях. Мне бы не хотелось видеть состояние наружу только ради тестирования, даже если бы реализацей такого логгера стал бы внутренний класс, а сам интерфейс бы не регламентировал передачу внутреннего состояния наружу. В таком случае Mockito берёт всю возню с состоянием на себя и, я уверен, делает это великолепно.
Мне бы не хотелось видеть состояние наружу только ради тестирования, даже если бы реализацей такого логгера стал бы внутренний класс, а сам интерфейс бы не регламентировал передачу внутреннего состояния наружу
Реализацией был бы класс в директории src/test
То есть это был бы класс только для тестов. Мне кажется, так было бы проще/чище.
Mockito берёт всю возню с состоянием на себя
Я бы согласился, если бы не простыня кода, чтобы заставить ваш подход работать.
Обычно нет нужды писать какой-то сложный код, чтобы работать с Mockito.
Если же вам пришлось это сделать, то, скорее всего, вы используете Mockito не по назначению.
Мы, возможно, говорим о немного разных вещах. Я ставил целью статьи не показать преимущество тестирования поведения над тестированием состояния или наооборот, а в том, как с помощью средств языка и немножко — инструмента тестирования — создать некое формальное описание строгого порядка выполнения проверок в виде некого подобия DSL. Не более того. Это и повлекло за собой создание той "простыни", ведь без неё никак в обеих случаях. Т.е., упор именно на переходы между проверками:
- сначала обязательная проверка на operation caller;
- потом обязательная проверка на operation type;
- и лишь потом — проверка аргументов value.
Единственное, что меня действительно огорчает — пришлось засыпать код лямбдами, потому что Mockito так работает. В случае использования только чистых JUnit/TestNG[+Hamcrest], в них (в лямбдах), конечно, не было бы нужды. И даже если я бы сделал упор на тестировании состояния, следуя вашей рекоммендации, у меня бы всё-равно в базовом абстрактном тесте был бы базовый метод, verifyLog()
, который знал бы о состоянии, а производные тесты бы просто описывали конкретные правила, например:
verifyLog()
.withOperationCaller(any(IAdministratorService.class))
.withOperationType(eq(CREATE), eq(ADMINISTRATOR))
.withValue(eq(VALUE_ADMINISTRATOR_NAME), eq(USERNAME))
.withValue(eq(VALUE_FIRST_NAME), eq(FIRST_NAME))
.withValue(eq(VALUE_LAST_NAME), eq(LAST_NAME))
.withValue(eq(VALUE_EMAIL), eq(EMAIL))
.then()
... // здесь не уверен
что по смыслу тождественно прямой проверке через has
/contains
или их аналоги, которые полностью инкапсулированы в базовом verifyLog()
.
Использовать наименования классов в виде IXService, IXComponent, IXDao — очень удобно т.к. простая подстановка "*" вместо конкретного названия сущности позволяет вам найти список всех компонент которые относятся к конкретному слою приложения. Необходимость в таком поиске часто возникает в долго играющих проэктах.
Откуда корни ростут… незнаю как у ТС а у меня из исходников java: префиксы Abstract, Base используются для маркировки абстрактных классов, однако в enterprise и так хватает длинных названий, поэтому намного удобнее использовать AXService вместо AbstractXService. А если можно для абстрактных классов то можно и для интерфейсов. В целом позволяет избавится от многих не всегда полезных приставок в названиях классов, если интерфейс называется AccountService скорее за все имплементация будет иметь имя: DefaultAccountService, AccountServiceImpl, RestAccountService или WebAccountService, которые полезной нагрузки практически не несут
p.s. сейчас не вспомню, но точно видел нейминг IInterface в какой-то достаточно популярной либе
Рефакторинг последовательных проверок в Mockito с помощью fluent-интерфейсов