Wednesday, 3 April 2019

ABAP Unit test patterns – test cases

By following good unit test practices, you can easily end up with a lot of boilerplate code. For example, it is recommended to have a single assert per test and split up tests into multiple methods instead of piling them up into one giant test. So it would not be uncommon for a new programmer to write a test class like this:

 METHOD test_addition.

    " given
    DATA(input) = `2 + 2`.

    " when
    DATA(result) = zfh_calculator=>calculate( input ).

    " then
    cl_abap_unit_assert=>assert_equals( exp = 4 act = result ).

  ENDMETHOD.

  METHOD test_subtraction.

    " given
    DATA(input) = `2 - 2`.

    " when
    DATA(result) = zfh_calculator=>calculate( input ).

    " then
    cl_abap_unit_assert=>assert_equals( exp = 0 act = result ).

  ENDMETHOD.

  METHOD test_multiplication.

    " given
    DATA(input) = `2 * 3`.

    " when
    DATA(result) = zfh_calculator=>calculate( input ).

    " then
    cl_abap_unit_assert=>assert_equals( exp = 6 act = result ).

  ENDMETHOD.

  METHOD test_division.

    " given
    DATA(input) = `4 / 2`.

    " when
    DATA(result) = zfh_calculator=>calculate( input ).

    " then
    cl_abap_unit_assert=>assert_equals( exp = 4 act = result ).

  ENDMETHOD.

If the calculator was extended to handle more complicated input, it would get messy very fast. Maybe people would copy paste code around and make accidental mistakes. You may have noticed I made an intentional mistake in the above code to better illustrate that point.

Some people would see this approach as inefficient and instead opt for something like this:

 METHOD test_calculate.
    cl_abap_unit_assert=>assert_equals(
      act = zfh_calculator=>calculate( `2+2` )
      exp = 4 ).

    cl_abap_unit_assert=>assert_equals(
      act = zfh_calculator=>calculate( `2-2` )
      exp = 0 ).

    cl_abap_unit_assert=>assert_equals(
      act = zfh_calculator=>calculate( `2*3` )
      exp = 6 ).

    cl_abap_unit_assert=>assert_equals(
      act = zfh_calculator=>calculate( `4/2` )
      exp = 2 ).
  ENDMETHOD.

This is way more readable, but it violates the one assert per test principle. The point of this principle is that when one assert fails, the test fails and you won’t know the result of the remaining asserts.

ABAP unit offers a way around this, by using the quit parameter. Each assert can be called with

quit = if_aunit_constants=>quit-no

which will make the test continue even after the assert fails. You can then see the results of all failed asserts in the test runner. Notice that even with an unimplemented method, only 3 of the 4 tests failed as it successfully returned 0 for the 2 – 2 case. This is why you should test multiple inputs.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

This works, but again we added some boilerplate, which makes the inputs/outputs being tested harder to see. It would be nice to have them in one place.

If you are fortunate enough to not have to worry about older versions of ABAP, here is my attempt at making the pattern as clean as possible:

METHOD test_cases.
    TYPES:
      BEGIN OF test_case,
        input TYPE string,
        exp   TYPE i,
      END OF test_case,
      test_cases TYPE HASHED TABLE OF test_case WITH UNIQUE KEY input.

    " given
    DATA(test) = VALUE test_cases(
      ( input = `2 + 2` exp = 4 )
      ( input = `2 - 2` exp = 0 )
      ( input = `2 * 2` exp = 4 )
      ( input = `2 / 2` exp = 1 )
     ).

    LOOP AT test ASSIGNING FIELD-SYMBOL(<test_case>).

      " when
      DATA(result) = f_cut->calculate( <test_case>-input ).

      " then
      cl_abap_unit_assert=>assert_equals(
        exp = <test_case>-exp
        act = result
        msg = |Failed at { <test_case>-input }. Expected { <test_case>-exp }, received { result }|
        quit = if_aunit_constants=>quit-no ).

    ENDLOOP.
  ENDMETHOD.

We introduce a type to hold our test cases, then we use an inline declaration for the data.

Don’t use a member variable or type for this. Do everything locally within the test method. This makes your test properly encapsulated as no other test can touch the data.

It is also very easy to move the test elsewhere, as you don’t have to copy the definition from a different place. There is also a good chance the data would be created in the class constructor, which adds an additional place you have to look.

A unique key ensures we don’t accidentally have duplicate test cases, as the test will just fail due to the duplicate table key.

You can easily add more tests by copy-pasting lines, and it is impossible to introduce duplicate data as a runtime error will occur when you run the test.

Then we loop through the table to get our test results. Make sure to use the quit = no and to specify a message which identifies a potential failing test case.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications

A slight disadvantage is that there is no way to run a particular case only.

◈ For debugging purposes, you can use conditional breakpoints to break at a specific point. You can also be creative and break for example on all tests which are supposed to return 4
◈ If you have the ability to change and activate code, you can move a case to the front of the table or comment something out.

It is ideal for methods which have one input and one output, although it can still be readable with one or two additional parameters. Make sure your lines are short enough to be seen in a diff view without horizontal scrolling.

And best for last, this pattern is very generic, so I turned it into a reusable template. After inserting, you only need to change the parameter types and the called method. It also assumes that you have an f_cut declared somewhere by convention.

  METHOD test_cases.
  
    TYPES:
      BEGIN OF test_case,
        input TYPE string,
        exp   TYPE string,
      END OF test_case,
      test_cases TYPE HASHED TABLE OF test_case WITH UNIQUE KEY input.

    DATA(test) = VALUE test_cases(
      ( input = `a` exp = `b` )
     ).

    LOOP AT test ASSIGNING FIELD-SYMBOL(<test_case>).
      DATA(result) = f_cut->tested_method( <test_case>-input ).
      cl_abap_unit_assert=>assert_equals(
        exp = <test_case>-exp
        act = result
        msg = |Failed at { <test_case>-input }.| &&
              |Expected { <test_case>-exp }, received { result }.|
        quit = if_aunit_constants=>quit-no ).
    ENDLOOP.

  ENDMETHOD.

Even though you don’t need more than the above code, you can find all of the code in this post available in this repository.

1 comment:

  1. Nice job Sabrina, thanks for creating this. It's great to see AUT being pioneered in this way.
    As an aside, have you ever found a way to retrieve AUT Code Coverage results in the background? I'm looking for a way to run AUT with coverage without the screen I/O.

    ReplyDelete