How to Test BOPF Actions, Determinations and Validations Using BUnit – An Introduction to the BOPF Unit Test Framework
The Business Object Processing Framework (BOPF) provides actions, determinations and validations to define the (main) logic of their application. The behavior is usually defined in the execute method of those interfaces (/BOBF/IF_FRW_ACTION, /BOBF/IF_FRW_DETERMINATION and /BOBF/IF_FRW_VALIDATION). The application-related data can be obtained using the parameter io_read (type ref to /BOBF/IF_FRW_READ), and can be modified by io_modify (type ref to /BOBF/IF_FRW_MODIFY, not available for validations). Either one of them, if not both, might always be used. Writing unit tests for the execute methods will have mocking io_read and / or io_modify as a prerequisite. This would probably mean to perform at least the following:
- Create at least one mocking class to implement both interfaces (or two classes which implement /BOBF/IF_FRW_READ and /BOBF/IF_FRW_MODIFY, respectively)
- Create some sort of data container for the mocking class / classes with the most fundamental functionalities like
- Store the data for retrieve
- Define the relationship between different node instances following a composition
- Make changes to the container whenever there is any modification on the instances
- Create some message handling mechanism if you are creating some messages
- Result handling to make sure your implementation does really what you want it to
- Making all the above well readable and as reusable as possible in case you need some further actions, determinations or validations
This non-exhausting list makes mocking either one of io_read and io_modify absolutely no fun. The effort to mock them will probably exceed the effort to implement the execute method itself and to write the unit test itself. This leads to the fact that many of those implementations do not have unit tests at all.
BUnit – The BOPF Unit test framework will take care of all the mock objects and let you concentrate on writing unit tests only for the most significant parts of your implementation (the fun part). With this article, I will explain how to use it with some simple examples.
This how-to guide assumes you have already some basic understanding of BOPF, and familiarity of the actions (at least this for the following examples), determinations and validations. If you are new to BOPF, you can check this nice article Introduction to Business Object Processing Framework (BOPF) and the references therein.
Besides, you will need to be working on SAP NetWeaver AS ABAP 7.51 or higher.
I will be using ABAP Development Tools (ADT) for the coding. However, that is not a hard constraint.
Testing an Action for a Single BO Instance
Assume we are using BUnit to write a unit test for the action “MARK_AS_PAID” of the business object (BO) /BOBF/EPM_SALES_INVOICE (this sample BO is available in a part of NetWeaver shipments >= NW 750, SP0). This action sets the field “PAYMENT_STATUS” of the root instances to the value “P”. The implementation can be found in /BOBF/CL_EPM_A_MARK_AS_PAID. The only information we need about the BO is its constants interface, which is, in our example, /BOBF/IF_EPM_SOI_CONSTANTS.
Now we can create a new class (name it, for example zcl_a_mark_as_paid). Then go to the “Test Classes” tab and create a local test class with our first test method and some aliases for convenience. If you are writing unit tests for your own BO actions, you might directly use the implementation classes of the corresponding action instead of creating a new one.
Listing: Skeleton of the test class for testing the MARK_AS_PAID action
CLASS ltc_action_mark_as_paid DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PUBLIC SECTION. INTERFACES /bobf/if_epm_soi_constants. ALIASES: bo_key FOR /bobf/if_epm_soi_constants~sc_bo_key, node FOR /bobf/if_epm_soi_constants~sc_node, association FOR /bobf/if_epm_soi_constants~sc_association, action FOR /bobf/if_epm_soi_constants~sc_action, attribute FOR /bobf/if_epm_soi_constants~sc_node_attribute . PRIVATE SECTION. METHODS: mark_one_root_as_paid FOR TESTING RAISING cx_static_check . ENDCLASS. CLASS ltc_action_mark_as_paid IMPLEMENTATION. METHOD mark_one_root_as_paid. ENDMETHOD. ENDCLASS.
Since the action is defined on the ROOT node, we need a root instance. This can be done by calling the create_root method of BUnit
METHOD mark_one_root_as_paid. DATA(lo_root) = /bobf/cl_bunit=>create_root( bo_key ). ENDMETHOD.
The BUnit root (node) object (lo_root) serves as a representation of the data (in form of the combined structure) stored in the BUnit data container. This representation of data corresponds exactly to one BOPF node instance, with the extra ability to create child instances, execute actions / determination / validations, without having to instantiate the BOPF service manager or /BOBF/IF_FRW_MODIFY objects. Now that we have our ROOT instance, all we need to do is to execute the action
DATA(lo_result) = lo_root->execute_action( action-root-mark_as_paid ).
Behind the scene, the method /BOBF/IF_FRW_ACTION~EXECUTE of the class /BOBF/CL_EPM_A_MARK_AS_PAID is called with io_read and io_modify already mocked by BUnit. Concretely, on calling io_read->retrieve, the data of the ROOT instance is read from the BUnit data container, which was created when we called the create_root method. Then io_modify->update is called (in a loop over all the ROOT instances), and the data of each ROOT instance is updated in the BUnit data container.
Now we need to check whether we really get the changes we expected, and more specifically, whether the value of the field “PAYMENT_STATUS” of each ROOT instance has been updated. This is as simple as to call
DATA(lo_assert) = /bobf/cl_bunit=>assert( ). lo_assert->node( lo_root )->attribute( attribute-root-payment_status )->equals( 'P' ).
As can be easily understood from the names of the methods, the BUnit assert object (lo_assert) here checks whether the value of the field “PAYMENT_STATUS” has been successfully updated.
That’s it. With four statements, we finished the main part of our first unit test. Actually we can achieve that with two statements if we want to. I think you can figure it out how yourself or with a tip: Notice we have not used the lo_result yet. The test method looks now like this
Listing: Test method implementation for single root instance
METHOD mark_one_root_as_paid. DATA(lo_root) = /bobf/cl_bunit=>create_root( bo_key ). DATA(lo_result) = lo_root->execute_action( action-root-mark_as_paid ). DATA(lo_assert) = /bobf/cl_bunit=>assert( ). lo_assert->node( lo_root )->attribute( attribute-root-payment_status )->equals( 'P' ). ENDMETHOD.
Testing an Action for Multiple BO Instances (Mass-Enablement)
Since IT_KEY (type /BOBF/T_FRW_KEY) shows up as an import parameter in the execute method of all three interfaces and it can contain several keys, we need some sort of mass-enabling mechanism in BUnit. This can be achieved by building a set for several BUnit node instances. Let’s create a second test method for the action
METHOD mark_two_roots_as_paid. DATA(lo_root_1) = /bobf/cl_bunit=>create_root( bo_key ). DATA(lo_root_2) = /bobf/cl_bunit=>create_root( bo_key ). DATA(lo_root_set) = /bobf/cl_bunit_node_set=>create_with_node( lo_root_1 )->add( lo_root_2 ). ENDMETHOD.
The BUnit node set object (lo_root_set) is a collection of zero or more BUnit node instances belonging to the same node (i.e., having the same node key). BUnit instances can be added to, or removed from the set. Two sets can be merged as one. For executing actions / determinations / validations, the node set object has the same method names as those of the node object
DATA(lo_result) = lo_root_set->execute_action( action-root-mark_as_paid ).
With some checks, we can also finish our second test
DATA(lo_assert) = /bobf/cl_bunit=>assert( ). LOOP AT lo_root_set->as_node_table( ) ASSIGNING FIELD-SYMBOL(<lo_node>). lo_assert->node( <lo_node> )->attribute( attribute-root-payment_status )->equals( 'P' ). ENDLOOP.
The method looks like this at the end
Listing: Test method implementation for multiple root instance …
METHOD mark_two_roots_as_paid. DATA(lo_root_1) = /bobf/cl_bunit=>create_root( bo_key ). DATA(lo_root_2) = /bobf/cl_bunit=>create_root( bo_key ). DATA(lo_root_set) = /bobf/cl_bunit_node_set=>create_with_node( lo_root_1 )->add( lo_root_2 ). DATA(lo_result) = lo_root_set->execute_action( action-root-mark_as_paid ). DATA(lo_assert) = /bobf/cl_bunit=>assert( ). LOOP AT lo_root_set->as_node_table( ) ASSIGNING FIELD-SYMBOL(<lo_node>). lo_assert->node( <lo_node> )->attribute( attribute-root-payment_status )->equals( 'P' ). ENDLOOP. ENDMETHOD.
Some further functionalities
We can still add further checks to the test, for example, we can assert that the action does not return any failed keys
lo_assert->action_result( lo_result )->has_no_failed_keys( ).
Or that a success message should be returned
lo_assert->action_result( lo_result )->message( )->is_success( ).
Maybe we are expecting an error message with a specific message ID
lo_assert->action_result( lo_result )->message( iv_message_id = 'SOME_MESSAGE_ID' )->is_error( ).
What if we want to test a determination defined on item instead of root? We just create one item
DATA(lo_item) = lo_root->create_child( node-item ).
and call the execute_determination method with lo_item.
Supposed that the business partner of this invoice has the ID “42”, we will need some special treatment for it. Then we need to get one node instance and set its value directly
lo_root->attribute( attribute-root-bp_id )->set( '42' ).
For more details, please refer to the interface documentations (using F2 in ADT) of the BUnit package (/BOPF/BUNIT) (whose names start with /BOBF/IF_BUNIT…).
Currently, the following features are still not available
- Regarding mocking of /BOBF/IF_FRW_READ
- Before image support: We are not able to differentiate the database state and the current state of the data yet
- We are not able to deal with EO_MESSAGE exported by the methods RETRIEVE or RETRIEVE_BY_ASSOCIATION yet
- We do not support the import parameter IT_REQUESTED_ATTRIBUTES in RETRIEVE or RETRIEVE_BY_ASSOCIATION yet
- Regarding executing actions / determinations / validations
- The parameter IS_PARAMETERS of the method /BOBF/IF_FRW_ACTION~ EXECUTE is not available for the method EXECUTE_ACTION of node or node set yet
- The support for action invocation inside an action (either using service manager or io_modify->do_action) is not supported yet
- Context-specific support (import parameter IS_CTX of the EXECUTE method for all three interfaces) is not available yet
- Other features
- Implemented association invocation is not available yet
- Modeled association (binding) is not available yet
- General support for dependent objects, BO enhancement and inheritance is not available yet
- General draft support is not available yet
Noticed the word “yet” at the end? There is more to come.
At the end, happy testing with BUnit and any questions / suggestions are welcome!