Делаем матчеры веселее с Groovy
Последняя заметка в этом году. Я думаю, что активные читатели давно заметили мою склонность к Groovy. Я уже писал ряд заметок как о Groovy, так и о создании матчеров для Hamcrest. Теперь опишу свою борьбу с написанием такого же матчера только в Groovy style.
Перед началом написания кода и шевеления извилинами, покажу интересный инструмент от Yandex, который позволяет генерировать метчеры для ваших доменных объектов - смотреть здесь. Единственный недостаток этой штуки: если у вас нету возможности натыкать аннотаций в объектах, то придется писать самим.
Если у вас нету возможности воспользоваться инструментом, описанным выше, велкам кодить матчеры самостоятельно.
Итак, сначала напишем наш доменный класс:
@ToString
class Person {
String name, phone
}
После написания метчеров у нас будет такая вот штука:
Person p = new Person(name: "Ivan", phone: "0661234567")
assertThat(p, hasName("Ivan"))
assertThat(p, withPhone("0661234567"))
Ну и в случае ошибки ожидаем увидеть сообщение:
Exception in thread "main" java.lang.AssertionError:
Expected: person with phone 1235678
but: was 0661234567
Начнем реализовывать метод hasName. Стандартная реализация будет выглядеть так:
def hasName(String name) {
new TypeSafeMatcher<Person>() {
@Override
protected boolean matchesSafely(Person p) {
name == p.name
}
@Override
void describeTo(Description description) {
description.appendText("a Person with name ").appendValue(name)
}
@Override
protected void describeMismatchSafely(Person person, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(person.name)
}
}
}
Ничего особо военного, анонимный метод и реализация трех методов - не слишком в стиле Groovy.
Давайте же поправим это все. Groovy крут из-за такой его возможности, как metaprogramming. Прибегая к чудесам метапрограммирования, мы можем в классе Description переопределить метод leftShift:
Description.metaClass.leftShift = { text ->
delegate.appendText(text.toString())
}
Теперь, используя полученный метод <<, напишем наш матчер:
static hasName(String name) {
[
matchesSafely: { name == it.name },
describeTo: { it << "a person with name" + name },
describeMismatchSafely: { item, descr -> descr << "was " + item.name }
] as TypeSafeMatcher<Person>
}
Оба-на! кода стало в разы меньше и его читаемость улучшилась. Правда ухудшилась степень понимания, особенно если вы не знаете таких слов, как Closure.
От себя хочу добавить, что подход с реализацией интерфейса через Map в случае с тремя методами не очень удачен, вот если бы у нас был один метод, тогда бы было все очень красиво и просто.
Давайте же посмотрим на упрощение реализации с тремя методами. Реализовываем класс GroovyMatcher<T>:
abstract class GroovyMatcher<T> extends TypeSafeMatcher<T> {
private Description description = new StringDescription()
private Description mismatchDescription = new StringDescription()
@Override
boolean matchesSafely(T item) {
match(item, description, mismatchDescription)
}
@Override
void describeTo(Description description) {
description << this.description
}
@Override
void describeMismatchSafely(T item, Description mismatchDescription) {
mismatchDescription << this.mismatchDescription
}
abstract boolean match(T item, Description description, Description mismatchDescription)
}
Все достаточно стандатно, но давайте теперь применим магию Groovy. В написанном нами классе GroovyMatcher нам нужно реализовать всего один абстрактный метод match. Пишем матчер для проверки номера телефона withPhone:
static withPhone(String actual) {
{ item, description, mismatchDescription ->
def expected = item.phone
description << "person with phone " | actual
mismatchDescription << "was " | expected
actual == expected
} as GroovyMatcher<Person>
}
Как вы можете заметить, мы использовали мощь Groovy и реализовали абстрактный метод через closure.
Перед подведением итогов и определением, какой-же подход лучше, хочу поделиться собственным опытом написания ExtentionModule.
Дабы не переживать по поводу метапрограммирования, можно написать ExtentionModule для нашего Description класса. Делается это так: пишем сначала класс, в котором реализовываем нужные нам методы:
class MatchersExtention {
static Description leftShift(Description self, StringDescription desc) {
self.appendText(desc.toString())
self
}
static Description leftShift(Description desc, String text) {
desc.appendText(text)
desc
}
static Description or(Description self, String value) {
self.appendValue(value)
self
}
}
Далее, чтобы этот класс начал работать, мы создаем в папке src/main/resources папку META-INF/services, в ней создаем файлик org.codehaus.groovy.runtime.ExtensionModule со следующим содержанием:
moduleName = matchers-module
moduleVersion = 1.0
extensionClasses = org.example.MatchersExtention
Все, теперь наш класс Description обзавелся поддержкой метода leftShift(<<) и or (|).
Вот так с использованием силы Groovy вы можете добавить в любой класс всяких-всячин. Даже если это класс из какой-то библиотеки!!!
Теперь давайте проанализируем подходы к написанию матчеров на Groovy. Я показал три подхода к написанию матчеров. Но какой из них лучше? Лично у меня все реализовано на данный момент через классический путь с анонимным методом. Но там куча лишнего кода и при наличии парочки таких методов в классе становится реально страшно в него заходить.
Подход c использованием Map, по моему мнению, самый классный. Магия метапрограммирования делает его очень коротким и понятным.
Ну и наконец последний подход с применение closure. Да, этот подход элегантен, нам нужно реализовать только один метод, но зачастую проверки бывают не настолько простыми и реализация этого одного метода может наносить больше вреда чем пользы.
На этом у меня все! Всех с наступающими праздниками и до встреч в Новом 2016 году…уиииии =)