Writing simple, readable unit tests
One of the mantras for unit tests is that they should be simple and easy to write. The converse is often also true, if testing is difficult, then the tested code is too complex. I’ve found that writing unit tests has had a positive impact on the way I structure my code.
After reading the blog ABAP Unit test patterns – test cases and the short discussion that followed, I would like to offer an alternative approach to creating unit tests in a way that is both easy to write and easy to understand.
Christian makes an important point that tests also serve as documentation of the intended logic. I couldn’t agree more! As a fan of Clean Code principles, I also think that code should be self-documenting and it should be immediately clear what it does.
With this in mind, it is quite easy to write a unit test for testing a complex method with a table input that may look something like:
METHOD detect_duplicate_ids. given_values( VALUE #( ( id = '1' ) ( id = '1' ) ). status_should_be( 'DUPLICATE_IDS' ). ENDMETHOD. METHOD detect_non_numeric. given_values( VALUE #( ( id = '1' val = '1' ) ( id = '2' val = 'A' ) ). status_should_be( 'NON_NUMERIC' ). ENDMETHOD. METHOD sum_adds_values. given_values( VALUE #( ( id = '1' val = '5' ) ( id = '2' val = '2' ) ). sum_should_be( 7 ). ENDMETHOD. METHOD sum_handles_negatives. given_values( VALUE #( ( id = '1' val = '5' ) ( id = '2' val = '-2' ) ). sum_should_be( 3 ). ENDMETHOD.
So this represents the actual test part, which also doubles as documentation of the expected logic. It reads easily and the technicalities of unit test framework are tucked away in more appropriately named helper methods.
The implementation of the helper methods should also be simple. The test class is something like:
CLASS lcl_test DEFINITION FOR TESTING... ... PRIVATE SECTION. DATA cut TYPE REF TO zcl_myclass. "Class Under Test ... ENDCLASS. ... METHOD given_values. cut = NEW zcl_myclass( i_values ). ENDMETHOD. METHOD status_should_be. cut->do_whatever( ). cl_abap_unit_assert=>assert_equals( act = cut->get_status( ) exp = i_status ). ENDMETHOD. METHOD sum_should_be. cl_abap_unit_assert=>assert_equals( act = cut->get_sum( ) exp = i_value ). ENDMETHOD.
A tabular approach such as Frederik presented in his blog is useful in some scenarios, but I find that often my test scenarios used mixed inputs, where a table becomes cumbersome.
For readability, I really like the fact that you don’t need to specify all elements in a VALUE constructor. My example above may be a structure with 30 fields, and I can easily add further tests where different fields are relevant, such as:
METHOD future_dated_is_open. given_values( VALUE #( ( keydate = sy-datum + 1 ) ). status_should_be( 'OPEN' ). ENDMETHOD.
In other words, I’m not just testing a fixed series of values, but different input permutations, all with a reasonably simple syntax and without the noise of irrelevant data.
Astute readers may have noticed I skipped the ID in the last example. Another trick we can use is to code mandatory or default values into the ‘given_…’ method: Check for empty values and supply defaults. Keep the test method only about the test.
This is obviously just a simple example, the same technique can be applied to database abstractions where the ‘given’ methods prime the mock data accessed by the CRUD methods. I wanted to keep this blog simple, but I could do a more fully-fledged example if there is sufficient interest.