Pull to refresh

Comments 8

Mockito утверждает, что это вообще-то антипаттерн https://webcache.googleusercontent.com/search?q=cache:6sm46ByiAY0J:https://monkeyisland.pl/2008/07/12/should-i-worry-about-the-unexpected/+&cd=1&hl=en&ct=clnk&gl=nl&lr=lang_en%7Clang_ru

Лучше тестировать состояние. В вашем примере можно сделать состояние логгера доступным для теста и проверять его с помощью 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().

naming conventions были сформулированы для того чтоб упростить жизнь девелоперам, однако это не означает что слепое следование решит все проблемы. К тому же Sun часто сами их нарушали (на счет oracle не в курсе).

Использовать наименования классов в виде IXService, IXComponent, IXDao — очень удобно т.к. простая подстановка "*" вместо конкретного названия сущности позволяет вам найти список всех компонент которые относятся к конкретному слою приложения. Необходимость в таком поиске часто возникает в долго играющих проэктах.

Откуда корни ростут… незнаю как у ТС а у меня из исходников java: префиксы Abstract, Base используются для маркировки абстрактных классов, однако в enterprise и так хватает длинных названий, поэтому намного удобнее использовать AXService вместо AbstractXService. А если можно для абстрактных классов то можно и для интерфейсов. В целом позволяет избавится от многих не всегда полезных приставок в названиях классов, если интерфейс называется AccountService скорее за все имплементация будет иметь имя: DefaultAccountService, AccountServiceImpl, RestAccountService или WebAccountService, которые полезной нагрузки практически не несут

p.s. сейчас не вспомню, но точно видел нейминг IInterface в какой-то достаточно популярной либе
Sign up to leave a comment.

Articles