Тестирование табличных данных с hamcrest на Java

в 12:47, , рубрики: java, Блог компании Luxoft, таблицы данных, тестирование, метки: , ,

Тестирование табличных данных с hamcrest на JavaПо ходу написания функциональных тестов мне частенько приходилось проверять корректность данных в различных таблицах. Таблицы встречались на веб страницах, базах данных или даже excel файлах. В любом случае, необходимо было проверять, что их содержимое соответствует заданному, то есть тому, что создается в тестовом сценарии.

Этот пост про то, как записывать такие проверки с помощью библиотеки hamcrest и зачем это надо.

Поначалу все было просто.

Проверки вида: в колонке «Зарплата», в 15 ряду, должно быть «миллион». Для этого у меня был метод, вроде такого: assertCellByColumnAndRowNumber, который дублировался с завидной регулярностью, то тут, то там.

Потом все немного усложнилось, и проверять надо было не по номеру ряда, а по первичному ключу: в колонке «Зарплата» для «Имени» «Василий Иванович» должно быть «миллион». Ничего страшного, родился метод assertCellByColumnAndPrimaryKey, и разумеется, он родился не в одном месте.

Далее встретились места, где первичный ключ был составной. Методы которые работали с составным ключом стали принимать на вход еще больше значений, понимать код стало труднее.

Добил меня случай, когда нужно было проверить следующее: Для рядов, где «Статус» равен «Ок», в колонке «Тип» содержатся значения «А, Б, Ц» по порядку сверху вниз.
Можно было бы сделать очередной метод с очень длинным названием и кучей переменных, но я начал понимать, что на этом дело не остановится, методов будет становиться все больше, и писать, поддерживать тесты будет все сложнее.

Поэтому я решил использовать hamcrest для того, чтобы записывать любые условия к таблицам в унифицированном виде и избавиться, наконец, от кучи методов делающих разнообразные проверки.

У меня получилось записать проверку для колонки «Тип» так:

assertThat(
	table,
	column("Type",contains("A","B","C")).where(cell("Status", is("Ok")))
);

Теперь подробнее о том, как это работает.

Таблица (table) представляется коллекцией рядов (class Table extends Collection<Row>)

Для того чтобы записать проверку такой таблицы, я создал hamcrest матчеры, которые задают условие на ряд или на всю таблицу целиком.

Пока мне хватило следующих матчеров:

  1. CellMatcher задает условие на одну ячейку ряда.
    Например:

    cell("Id", greaterThan(0))

    будет отбирать ряд, если в колонке «Id» будет содержаться значение больше 0.

    Для того чтобы создать такой матчер, нужно указать название колонки и любой «стандартный» hamcrest матчер, который будет проверять значение в этой колонке.

    Используя этот матчер и стандартные матчеры на коллекцию, можно записать условия на всю таблицу.

    Например, с помощью «библиотечного» матчера everyItem, который проверяет каждый элемент коллекции (ряд таблицы) на соответствие определенному правилу, можно записать такое условаие:
    В каждом ряду таблицы, значение Id больше нуля и Time не пустое (не null):

    everyItem(both(cell("Id", greaterThan(0))).and(cell("Time", notNullValue())))

    А добавив к CellMatcher функциональность стандартного CombinableMatcher, такое условие запишется еще проще – без слова both:

    everyItem(cell("Id", greaterThan(0)).and(cell("Time", notNullValue()))))
    

  2. FilterMatcher – фильтрует таблицу на основании одного матчера, а затем применяет второй матчер к оставшимся рядам.

    В качестве первого матчера (фильтра) используется CellMatcher, или объединение нескольких CellMatcher.
    С помощью FilterMatcher можно переписать предыдущий пример так:

    where(cell("Id",greaterThan(0)),everyItem(cell("Time",notNullValue())))
    

    В этом случае мы проверим, что Time не пусто (не null) для всех рядов где Id>0. Там же, где Id равен 0 или отрицателен, Time может быть пустым, в отличие от предыдущего примера.

  3. ColumnMatcher задает условие на все значения одной колонки таблицы.
    Например:

    column("Action", contains("Active", "Pause", "Active", "Closed"))
    

    задает условие, согласно которому, в колонке «Action» содержатся значения по порядку: «Active», «Pause», «Active», «Closed».
    Вместо стандартного, библиотечного матчера contains можно использовать любые другие матчеры на коллекции (колонка представлена как одномерная коллеция объектов), такие как containsInAnyOrder, hasItem и другие.

    К таким условиям, разумеется, можно добавить фильтр:

    column("Action", contains("Active", "Closed")).where(cell("Id",greaterThan(2)))
    

    Так мы проверим, что для рядов с id, большим чем 2, в колонке Action содержатся значения по порядку: «Active», «Closed».

    ColumnMatcher дает возможность применять аггрегирующие матчеры для элегантной проверки условий на сумму, минимум, максимум по столбцу. Например

    column("Salary", sum(is(100000))).where(cell("Type",is("fulltime")))
    

    позволяет проверить сумму зарплат fulltime работников.

  4. ColumnsMatcher позволяет вырезать из таблицы несколько колонок и задать условие на результирующий двумерный массив данных. Например:
    sliced(byColumns("Action", "Time"),
    	contains(row("Pause", "12:00"),
    	  	  row("Active", "12:30"),
    		  row("Closed", "14:00")))
    		.where(<some condition>)
    

    Здесь, выделив из всей, вероятно очень большой, таблицы, лишь колонки «Action» и «Time», мы проверяем, что они содержат четко заданные значения.

  5. Т.к. таблица является стандартной коллекцией, мы можем задать условие на количество ее рядов, например: not(empty()),iterableWithSize(lessThan(10)) используя стандартные матчеры из hamcrest и не изобретать свой велосипед.

На написание матчеров ушло полтора дня, и само по себе было очень интересным занятием, которое послужило отличной практикой в шаблонах проектирования. Мне пришлось принимать много архитектурных, дизайнерских, решений, начиная с того момента, как лучше представить таблицу?

Наверное, это были самые насыщенные полтора дня, исходя из количества архитектурных решений на строчку кода, которого получилось менее 15 килобайт.

Вообще это получился отдельный минипроект, с несколькими циклами рефакторинга, и изменениями дизайна, которые потребовались по ходу написания и применения написанных матчеров. Я даже написал небольшой unit тест, на практике применив TDD в первый раз в жизни.

Я подумал, что это могло быть отличным примером для изучения TDD (и других практик) новичками или хорошей темой для практических вопросов на собеседовании (которые мне приходится проводить), для того чтобы выявить архитектурные, «дизайнерские» способности кандидата, которые уже часто знают ответ на вопрос, почему канализационные люки круглые. (шучу, я никогда его не задаю, а Вы?).

Вывод:
Описанные табличные матчеры позволили:

  • записать все проверки к таблицам, которые я встретил в своих тестах,
  • избавиться от дубликации кода,
  • сделать код короче и понятнее,
  • сделать понятнее сообщения об ошибках, с более точным указанием рядов или ячеек, которые вызвали проблему,
  • автоматически получить лог с описанием всех проверок, который мы используем в рамках подхода "BDD наоборот", про который я напишу в следующий раз.

Автор: susliks

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js