
In the course of writing functional tests, I often had to check the correctness of the data in various tables. The tables were found on web pages, databases, or even excel files. In any case, it was necessary to verify that their contents correspond to the given, that is, what is created in the test script.
This post is about how to record such checks using the hamcrest library and why.
At first, everything was simple.
')
Type checks: in the “Salary” column, in the 15th row, there should be a “million”. For this, I had a method like this:
assertCellByColumnAndRowNumber
, which was duplicated with enviable regularity, here and there.
Then everything got a little more complicated, and it was necessary to check not by the row number, but by the primary key: in the column “Salary” for “Name” “Vasily Ivanovich” should be “million”. Nothing terrible, the
assertCellByColumnAndPrimaryKey
method was born, and of course, it was not born in one place.
Then there were places where the primary key was composite. Methods that worked with a composite key began to take on input even more values, it became more difficult to understand the code.
I got a chance when it was necessary to check the following: For rows where the “Status” is “OK”, the “Type” column contains the values ​​“A, B, C” in order from top to bottom.
It would be possible to make another method with a very long name and a bunch of variables, but I began to understand that this would not stop the matter, there would be more and more methods, and it would be harder
to write and maintain tests.
Therefore, I decided to use
hamcrest in order to write down any conditions to the tables in a unified form and finally get rid of a heap of methods that do various checks.
I managed to write a check for the “Type” column like this:
assertThat( table, column("Type",contains("A","B","C")).where(cell("Status", is("Ok"))) );
Now more about how this works.
The table (
table
) is represented by a collection of series (
class Table extends Collection<Row>
)
In order to record the verification of such a table, I created
hamcrest matchers who set a condition on a row or on the entire table.
So far the following matchers have been enough for me:
CellMatcher
sets a condition per row cell.
For example: cell("Id", greaterThan(0))
will select a row if the “Id” column contains a value greater than 0.
In order to create such a matcher, you need to specify the name of the column and any “standard” hamcrest matcher that will check the value in this column.
Using this matcher and standard matchers on the collection, you can record the conditions on the entire table.
For example, using the “library” matcher everyItem , which checks each element of the collection (table row) for compliance with a specific rule, you can write the following condition:
In each row of the table, the Id value is greater than zero and Time is not empty (not null
):
everyItem(both(cell("Id", greaterThan(0))).and(cell("Time", notNullValue())))
And by adding the functionality of a standard CombinableMatcher
, this condition is written even easier - without the word both: everyItem(cell("Id", greaterThan(0)).and(cell("Time", notNullValue()))))
FilterMatcher
- filters the table on the basis of one matcher, and then applies the second matcher to the remaining rows.
The first matcher (filter) is CellMatcher
, or the union of several CellMatcher
.
Using the FilterMatcher
you can rewrite the previous example as follows: where(cell("Id",greaterThan(0)),everyItem(cell("Time",notNullValue())))
In this case, we verify that Time is not empty (not null
) for all series where Id> 0. Where Id is 0 or negative, Time can be empty, unlike the previous example.
ColumnMatcher
sets a condition for all values ​​of a single column of a table.
For example: column("Action", contains("Active", "Pause", "Active", "Closed"))
sets the condition that the column “Action” contains the values ​​in order: “Active”, “Pause”, “Active”, “Closed”.
Instead of the standard, library matcher contains
you can use any other matchers on the collection (the column is presented as a one-dimensional collection of objects), such as containsInAnyOrder, hasItem
and others.
Of course, you can add a filter to such conditions: column("Action", contains("Active", "Closed")).where(cell("Id",greaterThan(2)))
So we check that for rows with id greater than 2, the Action column contains the values ​​in order: “Active”, “Closed”.
ColumnMatcher
allows you to apply aggregate matchrs to elegantly check conditions for a sum of at least a maximum of a column. for example column("Salary", sum(is(100000))).where(cell("Type",is("fulltime")))
allows you to check the amount of wages fulltime workers.
ColumnsMatcher
allows ColumnsMatcher
to cut multiple columns from a table and set a condition on the resulting two-dimensional data array. For example: sliced(byColumns("Action", "Time"), contains(row("Pause", "12:00"), row("Active", "12:30"), row("Closed", "14:00"))) .where(<some condition>)
Here, selecting from the whole, probably very large, tables, only the columns “Action” and “Time”, we check that they contain clearly defined values.
- Because the table is a standard collection, we can set a condition on the number of its rows, for example:
not(empty()),iterableWithSize(lessThan(10))
using standard hamcrest match players and not reinvent your bike.
It took a day and a half to write matchmakers, and in itself was a very interesting exercise, which served as an excellent practice in design patterns. I had to make a lot of architectural, design, decisions, starting from the moment, how best to present the table?
Probably, these were the most saturated one and a half days, based on the number of architectural solutions per line of code, which turned out to be less than 15 kilobytes.
In general, it turned out to be a separate mini-project, with several cycles of refactoring, and design changes that were required in the course of writing and using written matchmakers. I even wrote a small unit test, in practice, using TDD for the first time in my life.
I thought that this could be a great example for learning TDD (and other practices) by newbies or a good topic for practical interview questions (which I have to do) in order to reveal the architectural, “designer” abilities of a candidate who often already know the answer to the question why sewers are round. (joking, I never ask him, and you?).
Conclusion:
The table matchers described allowed:
- write down all the checks to the tables that I met in my tests,
- get rid of duplicate code
- make the code shorter and clearer
- make the error messages clearer, with a more precise indication of the rows or cells that caused the problem,
- automatically get a log with a description of all the checks that we use in the framework of the approach " BDD vice versa ", about which I will write next time.
