Wednesday 8 July 2020

Cheaper Unit Tests with less Mock Logic

Automated tests have become an important part in my professional life as a software developer. However, you cannot always avoid external dependencies of individual methods you want to test. The more dependencies you have, the more cumbersome testing gets.

Different Approaches to automated testing


The standard advice is to mock any logic that is called by the code under test.

So, suppose tests are needed for such an application:

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

When each class is tested separately, mock logic has to be written for all coding the class depends on. This leads to implementing a lot of test coding:

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

The tests in this design typically refer to behavior that is invisible to the user, but “under the hood” (how a class interferes with other classes).

And even though the simplified diagram above does not show any boxes for the test coding itself, we already have a lot of boxes. Tests in this design may be difficult to understand. On the other hand, one of the tasks of these tests is exactly to enable a better understanding when you later get back to the code under test for some modification.

So why not write a single unit test?

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

There is much less test coding needed. The tests may also be easier to read as it is likely that they are more related to the original user requirements. I used therefore green color for the test coding.

Yes, in this simple example, it looks like an integration test. Especially in more complex software, it does make sense to cover a broader area of the functionality with automated integration tests, rather than focusing too much on a single part of the application. You will end up getting tests for the “right size”of code units (relative to your needs), instead of being forced into having isolated local test classes with every production code class.

Version 1 of my test designs


Already many years ago, the Clean Code initiative by Robert C. Martin (Uncle Bob) tried to minimize code complexity. The ABAP programming guideline by SAP contains much helpful advice, too, but testing was hardly covered by the original 2009 edition. I had the impression that Clean Code was more popular than automatic testing.

Now my daily job deals with ABAP in BW systems, sometimes also SAP BW transformations, Planning functions and other high level “coding”.

I wrote my first automated test in 2013, for just one complicated function to count days for an HR reporting application. I could do the test, because this function had near to no dependencies to other coding. The rest of the application remained without automated tests, because I did not know how to deal with dependencies.

Even when I learned to handle external dependencies in tests a few years later, my test coverage stagnated at about 5% of the code, because adding mock logic (either by inheritance or by interfaces) was too much effort to me.

Version 2 of my test designs


This changed when I was able to use ABAP Test Seams starting 2016. Test Seams are special statements of SAP ABAP to handle dependencies during automatic tests. They have restrictions, but the good point is, that they are easy to use. So with Test Seams it was easy to add automatic tests, even when dependencies made testing difficult. This changed the situation for me completely. Before that , I wrote automatic tests rather rarely after I decided that I really needed them. With Test Seams there was no excuse anymore. My new motto was: “Bad tests are better than no tests”.

I greatly increased my test coverage. When I needed to adapt existing code, I often added tests. With Test Seams, this is less risky and cheaper than with conventional techniques. Test seams also deal very well with legacy ABAP code that does not contain interfaces or classes that can be mocked easily.

Version 3 of my test designs


In 2018, I read this tweet…

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Exam Prep, SAP ABAP Prep

Ian Cooper speaks about his experiences with automatic tests and all the errors he made. It is an excellent presentation, have a look for yourselves !

He advised to read the original book by Kent Beck about Test Driven Design. In his concept, the idea about unit tests, is not testing individual classes, but about the proper size of code to be tested. Not too small and not too big. That may be an individual class, but it may also be a differently sized unit.

So after seeing this presentation, I changed may testing style once more.

I tried to mock only when I found it really necessary. In all other cases I read and wrote from the database (in the development system). Sometimes I was able to keep the test data separate from other data. But often this was not possible. So I did it nonetheless, accepting the risk that a test breaks because someone changed the data. Actually, that occurred sometimes, but not on a regular basis. As a benefit, I needed less mock coding and less test seams.

Ian Cooper also advised that Test Driven Design means:

1. Add or adapt a test so that the tests break.
2. Write a code that fixes the broken tests. But do not write it beautifully, write it fast.
3. When the tests pass: Refactor and think about improving the code you wrote.

With this in mind, writing automated tests felt even more simple. In many cases, I needed no mock logic at all.

Of course, testing still means some effort. For each new application you have to think how it can be tested well.

◉ Having a separate “space” in the data reserved for testing is my preferred way.
◉ Sometimes (not very often), Mock coding is needed.
◉ Sometimes adding a Test Seam is the fastest and best method. The amount of tests seams has decreased significantly, however, as I avoid mock code as far as I can.
◉ As the tests are not related to a single class anymore, I often implement special classes just for the unit tests.

Currently I focus on learning how to write more readable test code. I include a lot of helper methods just to have easy to read tests. All distracting statements are refactored into separate methods.

It is an example for a test where reading from the database is not mocked. The former test design looks like this (get_data and get_data2 are test methods):

  METHOD get_data.

    f_cut = NEW #( ).

    DELETE FROM zunitdemo_table1 WHERE test = 'X'.
    DATA: new_data TYPE STANDARD TABLE OF zunitdemo_table1 WITH DEFAULT KEY.
    new_data = VALUE #( ( test = |X|
                          key_a = |A|
                          field_1 = |C| ) ).

    INSERT zunitdemo_table1 FROM TABLE new_data.
    COMMIT WORK. " Required in case the data shall really be stored or deleted

    DATA: read_data_act TYPE STANDARD TABLE OF zunitdemo_table1 WITH DEFAULT KEY,
          read_data_exp TYPE STANDARD TABLE OF zunitdemo_table1 WITH DEFAULT KEY.

    read_data_act = f_cut->get_data( ).

    read_data_exp = VALUE #( (
                          mandt = sy-mandt
                          test = |X|
                          key_a = |A|
                          field_1 = |C| ) ).

   cl_abap_unit_assert=>assert_equals( msg = 'Expect correct data' exp = read_data_exp act = read_data_act ).

  ENDMETHOD.

The simplified test design looks like this:

  METHOD get_data2.

    " Version of test method get_data which is easier to read

    _prepare_test_data( key = |A| field = |C| ).

    read_data2_exp = VALUE #( ( mandt = sy-mandt
                                test = |X|
                                key_a = |A|
                                field_1 = |C| ) ).

    _test( message = |Expect correct data| ).

  ENDMETHOD.

The problem becomes very obvious when a test class has about a dozen of such tests. It becomes difficult to read and understand what exactly is being tested there. A couple of tests like in get_data2 are much easier to read.

Yes, the second test uses global variables, but I don’t think this is a real problem.

No comments:

Post a Comment