Skip to Content
Technical Articles

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.

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.

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.

As always, I hope you enjoyed this. I might write more posts like this in the future, so consider subscribing!

2 Comments
You must be Logged on to comment or reply to a post.
  • Hi Frederik,

    nice blog. However, I think you are ignoring two important points: readability and unit test as documentation.

    IMHO the first to examples are much more readable, and consequently easier to maintain, than the latter two examples. While there is quite some boilerplate code in the first two examples, the intention of the tests is very clear. Furthermore, it’s quite easy to refactor the first example into something like this:

    METHOD test_subtraction.
        cl_abap_unit_assert=>assert_equals(
           exp = 0 
           act = zfh_calculator=>calculate( ‘2 - 2’ ) ).
    ENDMETHOD.

    Now we have a very concise test method. If we now also add a msg to the test we get something like this:

    METHOD test_subtraction.
        cl_abap_unit_assert=>assert_equals(
           exp = 0 
           act = zfh_calculator=>calculate( ‘2 - 2’ ) 
           msg = ‘The result of substracting 2 from 2 should be 0’ ).
    ENDMETHOD.

    No the messages in the tests read like a specification. With this approach your first example would be four method with the four lines shown above. In about 20 lines of code it would be possible to see what the test cases are and what the expected outcome is. So basically unit test as a documentation and specification.

    In my opinion this approach is much clearer and easier to understand then the approach you described. In addition no conditional breakpoints are needed when debugging. Therefore, I think the simple approach is the preferable one.

    Christian

     

     

    • If you feel your test case is complicated enough to warrant a special method, then by all means do it. For example, I would handle division by zero in a separate test. I would also try to split anything with too many cases.

      It works best in cases where the method is simple enough that you don’t need to write extra documentation because the signature is self-explanatory. “These expressions should have these results” is what you read from

            ( input = `2 + 2` exp = 4 )
            ( input = `2 - 2` exp = 0 )
            ( input = `2 * 2` exp = 4 )
            ( input = `2 / 2` exp = 1 )
      

      without needing any comments.

      In your example, I feel that you are just repeating the test in a string, while the generic message is actually better:

      Failed at 2 – 2. Expected 0, received 1. vs. The result of substracting 2 from 2 should be 0.

      • You have to maintain the values yourself in multiple places. It’s easy to make a mistake. You already have a typo in the word substracting.
      • You don’t state the actual result (although the assert will do this for you).