Friday, 24 March 2017

An ABAP test case pattern using fixtures inspired by Javascript

An ABAP test case pattern using fixtures inspired by Javascript


21.03.17 – Frank Gales

After years of ABAP development I some years ago moved to Javascript but I’m for a short intermezzo doing some ABAP development again. In Javascript in my team we do have a pattern for test fixtures that evolved during time. A test fixture is a set of test data.

Here is the pattern. It is mostly JSON so it should also be understandable for ABAP developers. In order to get the most benefit out of a test it should be easy to add a new fixture (test data set) e.g. to cover more edge cases or after a bug is found to have that specific context covered in a test.

[
  {
    testDescription: "we do have two distinct target mappings”,
      oInput: {
        parameter1: "value1",
        parameter2: "value2"
      oExpected: {
        result1: 4711,
        result2: {
        …
  },
  {
    testDescription: "we do have two equal target mappings",
      oInput: {
        parameter1: "value21",
        parameter2: "value22"
      oExpected: {
        result1: 1972,
        result2: {
        …
  }
].forEach(function(oFixture) {
asyncTest("getInbound … when " + oFixture.testDescription, function () {

   // the actual test code

})

Code Snippet: Fixture coding pattern in Javascript

[ ] defines an array of two objects { } – the fixtures – which each have 3 properties – like a structured data type in ABAP – testDescription, oInput, oExpected. testDescription describes the specific test fixture and is displayed in the test result. oInput is a an object – a structure in ABAP – that has all data that is needed for the test and oExpected is or are the values we need for the assertions. We iterate over the objects in the array and run the test for each fixture.

Now in ABAP things are more complicated. One needs to define structures and make things more explicit. Let’s consider a unit test. We would have a test class for each method to test. And we would have a ‘for testing’ method for each fixture. This methods do not contain the test code but only create the fixture and then call the actual test code which is contained in method ‘test’. BTW, my ABAP code style is influenced by being a Javascript developer.

class ltc_get_inbound definition " Tests method get_inbound of the productive class
  for testing
  duration short
  risk level harmless.
  private section.
    types:
      begin of ts_input,
        parameter_1 type string,
        parameter_2 type string,
      end of ts_input,
      begin of ts_expected,
        result_1 type i,
        result_2 type cl_productive_class=>ts_inbound,
      end of ts_expected,
      begin of ts_fixture,
        description type string,
        input type ts_input,
        expected type ts_expected,
      end of ts_fixture.
    methods two_disctint_target_mappings
      for testing.
    methods two_equal_target_mappings
      for testing.
    methods test
      importing
        is_fixture type ts_fixture.
endclass.

class ltc_get_inbound implementation.

  method two_disctinct_target_mappings.
    data:
      ls_fixture type ts_fixture.
 
    " Arrange 1 – create the fixture
    ls_fixture = value #(
      description = 'we do have two distinct target mappings'
      input = value #(
        parameter_1 = 'value1'
        parameter_2 = 'value2'
      )
      expected = value #(
        result_1 = 4711
        result_2 = value #(
          ...
        )
      )
    ).
    test( ls_fixture ).
  endmethod.
  ...
  method test.
    data:
      lv_result_1 type string,
      ls_result_2 type cl_productive_class=>ts_inbound.

    " Arrange 2
    data(lo_productive_class) = new cl_productive_class( ).
       " I don’t like lo_cut. I prefer a name derived from the actual class name.
       " Here I used productive_class wich I would not use in real life code.
       " For the example it is okay.
    " Act
    lo_productive_class->get_inbounds(
      exporting
        iv_parameter_1 = is_fixture-input-parameter_1
        iv_parameter_2 = is_fixture-input-parameter_2
      importing
        ev_result_1 = lv_result_1
        es_result_2 = ls_result_2
    ).
    " Assert
    cl_abap_unit=>assert_equals(
      exp = is_fixture-expected-result_1
      act = lv_result_1
      msg = |result_1 is not calculated correctly when { is_fixture-description }|
    ).
    cl_abap_unit=>assert_equals(
      exp = is_fixture-expected-result_2
      act = lv_result_2
      msg = |result_2 is not calculated correctly when { is_fixture-description }|
    ).
  endmethod.

endclass.

* Code Snippet: Fixture coding pattern in ABAP

Add-On:


What if I need to deal with JSON as input and/or expected data?
Writing JSON in ABAP code is a pain. Splitting the JSON into ABAP strings which get concatenated is the usual way.
I do often create JSON using ad-hoc Javascript code in the browser console (Chrome preferred; the biggest one was 107,000 lines). Let’s say you have a few hundred lines of JSON.

Here is what I did. I added the JSON as comments in the code in the ‘for testing’ methods with some specific begin and end marker. As it is the fixture definition it’s where it belongs to.

method two_disctinct_target_mappings.
* begin extraction - marker: cl_productive_class->two_disctinct_target_mappings
* {
*   [

*     {

*       testDescription: "we do have two distinct target mappings”,

*       oInput: {

*         parameter1: "value1",

*         parameter2: "value2"

  ...

* end extraction - marker: cl_productive_class->two_disctint_target_mappings

With the following code you could extract the json:

data:
  lt_test_class_source_code type table of string.

data(lv_method_name) = |{ iv_class_name }->{ iv_method_name }|.
translate iv_class_name to upper case.
translate lv_method_name to lower case.

data(lv_test_class_include) =
    cl_oo_classname_service=>get_local_testclasses_include( iv_class_name ).
read report lv_test_class_include into lt_test_class_source_code.
find first occurrence of |begin of json in method { lv_method_name }|
  in table lt_test_class_source_code
  match line data(lv_begin).
assert sy-subrc = 0.
find first occurrence of |end of json in method { lv_method_name }|
  in table lt_test_class_source_code
  match line data(lv_end).
assert sy-subrc = 0.
delete it_table from lv_end.
delete it_table from 1 to lv_begin.
loop at it_table assigning field-symbol(<lv_line>).
  concatenate rv_json <lv_line>+1 into rv_json.
endloop.

One last remark:
The 107,000 lines JSON ended up in an eCatt test data container as even ABAP in Eclipse did not really like so big source code files. The drawback with that solution is that the fixture is decoupled from the test which I don’t like. But with such a big fixture – it is used to performance challenge our code – this seemed to be the more appropriate approach.

No comments:

Post a Comment