Очень жаль. :( Именно его статьи в тогда ещё печатном издании "][akep" в начале-середине 2000-х и позднее его книги были своего рода технологическим интернетом того времени по крайней мере для меня лично. Тогда я зачитывал статьи Криса Касперски в "][акере" и, по-моему, в "][акер-Спец" до дыр, хотя и многое тогда мне казалось сложным, но чертовски увлекательным и поражающим воображение. Я искренне продолжаю восхищаться этим Человеком, и хочу искренне сказать спасибо за всё то, что он успел сделать.
Честно говоря, нет, не рассматривал, поскольку ориентировался исключительно на чистую Retrolambda. Пример именно со Stream API (а не с таким "несерьёзным" Optional) был больше интересен как эксперимент, который показал бы некоторые нетривиальные техники портирования любой библиотеки или API, даже если для неё не существует бекпорта. Насколько я понимаю как работает streamsupport/streamsupport, она также требует привязки з пакету java8.*?
Мы, возможно, говорим о немного разных вещах. Я ставил целью статьи не показать преимущество тестирования поведения над тестированием состояния или наооборот, а в том, как с помощью средств языка и немножко — инструмента тестирования — создать некое формальное описание строгого порядка выполнения проверок в виде некого подобия 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().
Если логгер — это интерфейс, то можно написать свою имплементацию, которая собирает вызовы в журнал, который торчит наружу. Мне кажется, так будет короче и проще, чем у вас.
Вот как раз этого я и пытаюсь избежать, поэтому предпочитаю тестирование поведения в таких случях. Мне бы не хотелось видеть состояние наружу только ради тестирования, даже если бы реализацей такого логгера стал бы внутренний класс, а сам интерфейс бы не регламентировал передачу внутреннего состояния наружу. В таком случае 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, которые реализуют один и тот же интерфейс. Плюс, с практической точки зрения, такие имена удобнее читать за компьютерами коллег, которые принебрегают возможностями подстветки, или в системах, в которых такая возможность отсутствует вообще.
Не за что! На самом деле всё просто: в статье и так много декораторов, а так с помощью прокси получился дешёвый null object. И я, пожалуй, одолжу вашу предстоящую задачу и тоже решу её у себя.
Честно говоря, нет, не рассматривал, поскольку ориентировался исключительно на чистую Retrolambda. Пример именно со Stream API (а не с таким "несерьёзным"
Optional
) был больше интересен как эксперимент, который показал бы некоторые нетривиальные техники портирования любой библиотеки или API, даже если для неё не существует бекпорта. Насколько я понимаю как работает streamsupport/streamsupport, она также требует привязки з пакетуjava8.*
?Мы, возможно, говорим о немного разных вещах. Я ставил целью статьи не показать преимущество тестирования поведения над тестированием состояния или наооборот, а в том, как с помощью средств языка и немножко — инструмента тестирования — создать некое формальное описание строгого порядка выполнения проверок в виде некого подобия DSL. Не более того. Это и повлекло за собой создание той "простыни", ведь без неё никак в обеих случаях. Т.е., упор именно на переходы между проверками:
Единственное, что меня действительно огорчает — пришлось засыпать код лямбдами, потому что Mockito так работает. В случае использования только чистых JUnit/TestNG[+Hamcrest], в них (в лямбдах), конечно, не было бы нужды. И даже если я бы сделал упор на тестировании состояния, следуя вашей рекоммендации, у меня бы всё-равно в базовом абстрактном тесте был бы базовый метод,
verifyLog()
, который знал бы о состоянии, а производные тесты бы просто описывали конкретные правила, например:что по смыслу тождественно прямой проверке через
has
/contains
или их аналоги, которые полностью инкапсулированы в базовомverifyLog()
.Вот как раз этого я и пытаюсь избежать, поэтому предпочитаю тестирование поведения в таких случях. Мне бы не хотелось видеть состояние наружу только ради тестирования, даже если бы реализацей такого логгера стал бы внутренний класс, а сам интерфейс бы не регламентировал передачу внутреннего состояния наружу. В таком случае Mockito берёт всю возню с состоянием на себя и, я уверен, делает это великолепно.
Да, это также упоминается в javadoc-е
Mockito.verifyNoMoreInteractions(Object...)
. Но я считаю, что для систем, от которых ожидается чётко регламентированное поведение (как, например, строгое журналироване в тексте статьи), такая практика вполне даже применима и может быть использована именно во благо, а не во вред, о чём говорят разработчики Mockito, ссылаясь на возможное злоупотребление этой возможностью. Например, если метод тестируемого юнита обращается к какому-либо другому юниту и это влияет на состояние сообщения, которое планируется отправить в журнал — тест может указать на возможные проблемы во взаимодействии компонент между собой. И если это не считать проблемами, то тест может фактически выступать в роли документа, формально описывающего ожидаемые результаты взаимодействия нескольких компонент (если считать таковыми тесты, конечно). Намеренное игнорированиеMockito.verifyNoMoreInteractions(Object...)
можно сравнить с намеренным подавлением предупреждений компилятора или других инструментов анализа.Возможно, но мне такое утверждение кажется весьма спорным: здесь логгер выступает в роли компонента с write-only семантикой, и я бы не хотел видеть, как он предоставляет доступ к своему временному состоянию (имеется ввиду время жизни с
begin()
доlog(...)
) даже с помощью простейшего.contains(LogEntryKey key, Object value)
. Кроме того, тест с помощью Mockito позволяет гарантировать, что логгер собрал не только данные для следующей записи в журнале (т.е., состояние), а также и отослал эти данные куда-то (log(...)
). Можно возразить, что иlog(...)
можно реализовать так, чтобы он выставлял некоторое состояние, описывающее факт "отосланного сообщения", но тогда наверняка пришлось бы пожертвовать или чистотой интерфейса, добавив в него что-то типаisLogged()
, или в тестах завязываться на конкретную реализацию и каким-то образом узнавать о таком флажке (пусть даже приватном). Подход с Mockito, я считаю, более естественнен.Не полностью по Java, да. На самом деле это прямо позаимствовано из C#/.NET (я, честно говоря, с Delphi только
TFoo
помню). Мне кажется, это немного елегантнее, чемFooImpl
,BarImpl
иBazImpl
, которые реализуют один и тот же интерфейс. Плюс, с практической точки зрения, такие имена удобнее читать за компьютерами коллег, которые принебрегают возможностями подстветки, или в системах, в которых такая возможность отсутствует вообще.