📜 ⬆️ ⬇️

Testing Java table from hamcrest

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:
  1. 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())))) 

  2. 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.

  3. 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.

  4. 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.

  5. 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:image

Source: https://habr.com/ru/post/150368/


All Articles