Wednesday 31 May 2023

BDD-style tests for ABAP with Cacamber

Do you know this problem? New features should be developed, but communication with the customer is difficult. People talk past each other. Requirements are unclear. Finally, when the feature is implemented, it turns out the customer actually had a different requirement: “But we thought that does something completely different!”

One solution to this problem is to intensify communication between the customer and the development team and get feedback as soon as possible. An agile process model that focuses on this is Behavior Driven Development (BDD).

BDD focuses on the behavior of the user and attempts to establish consistency between requirements, implementation, and testing – a common understanding. For this purpose, examples are specified, so-called scenarios, which describe the behavior. This is often done during  “3 Amigos Sessions”, in which business, developers, and QA work together.

BDD is quite similar to acceptance test-driven software development (ATDD). In addition, there is a focus on automation: scenarios should serve as executable specifications – they should be automatically testable.

SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP BDD Style

Scenarios are written in a specific language called Gherkin, like this scenario for an online shopping system:

Feature: Discount calculation

Scenario: Discount on Slayer albums for VIP Slayer fans (exclusive contract with BMG)

GIVEN the customers first name is Dominik and his last name is Panzer
AND his birthdate according to our CRM system is 06.06.2006
WHEN the sales clerk lets the system calculate the customers discount on a Slayer album
THEN the discount is 66% \m/

You might already know Gherkin and the GIVEN … WHEN … THEN pattern. It’s quite similar to ARRANGE… ACT… ASSERT. They are both techniques to give unit tests a clear structure.

In ABAP this usually looks like this. Developers focus choose a feature, create a test class, give the test method an according name and write an acceptance test:

METHOD discount_calculation.
* Given
sut->do_this( parameter ).
sut->do_that( parameter ).
"more complex stuff here
DATA(product) = |Slayer Album|.

* When
DATA(discount) = sut->calculate( product ).

* Then
cl_abap_unit_assert=>assert_equals( exp = 66 act = discount ).
ENDMETHOD

GIVEN is used to do the test setup. WHEN defines what action is being performed and THEN is responsible for the assertions.

But the GIVEN… THEN… WHEN… comments are not very helpful here. Also, the individual parts of the test are not reusable. And the whole test is not very domain-centric compared to the scenario the business actually defined.

So some people wanted to do better and started writing their tests like this:

METHOD discount_on_slayer_albums.
given_cstmr_first_last_brthdy( EXPORTING first_name = 'Dominik'
                                         last_name = 'Panzer'
                                         birthdate = '20060606' ).
when_the_price_is_calculated_for( 'Slayer Album' ).
then_the_discount_is_correct( ).
ENDMETHOD

That’s way better! Most of the details and complexity are hidden behind well-named methods – another level of abstraction has been introduced. Also, there is the possibility to change the test parameters. This makes the steps of the test more flexible and reusable. But using parameterless methods would make the code more readable.

But this solution is no way near our original BDD scenario. It’s not natural language, it is mainly code. Also, this approach is limited by ABAPs maximum method length. Developers are forced to use abbreviations etc.

So I decided to change this and start an OSS hobby project. I developed a BDD layer for ABAP called “Cacamber”. Currently Cacamber supports English and German Gherkin keywords.

If you choose to use Cacamber as a bridge between the scenario written in plain Gherkin and ABAP Unit, your test steps will look like this:

METHOD discount_on_slayer_albums.
scenario( 'Discount on Slayer albums for VIP Slayer fans (exclusive contract with BMG)' ).

given( 'the customers first name is Dominik and his last name is Panzer' ).
and( 'his birthdate according to our CRM system is 06.06.2006' ).
when( 'the sales clerk lets the system calculate the customers discount on a Slayer album' ).
then( 'the discount is 66% \m/' ).
ENDMETHOD.

If the test fails ABAP Unit can tell you this way:

...
Critical Assertion Error: 'Discount Calculation: Discount on Slayer Albums for VIP Slayer fans (exclusive contract with BMG)'
...

Your tests can even look like this:

METHOD discount_on_a_shopping_cart.
scenario( 'Discount voucher applied to whole shopping card' ).

given( 'the customers has the following items in his shopping cart:' &&
       '| 2 | Slayer | Reign In Blood | LP | 9,99€ |' &&
       '| 1 | Slayer | South Of Heaven | LP | 9,99€ |' &&
       '| 1 | David Hasselhoff | Crazy For You | LP | 9,99€ |' ).
and( 'he adds a valid 10% voucher' ).
when( 'the customer checks out' ).
then( 'the discount is 4€.' ).
ENDMETHOD.

Or like that:

METHOD no_discount_on_shopping_cart.
verify( 'Scenario: Customer is not eligible for a discount on the shopping cart' &&
        'Given the customers first name is Dominik and his last name is Panzer' &&
        'And his birthdate according to our CRM system is 06.06.2006' &&
        'And in his shopping cart are the following items:' &&
        '| 1 | Scooter - Hyper Hyper |' &&
        '| 1 | Scooter - How Much Is The Fish |' &&
        '| 1 | Scooter - Maria (I like it loud) |' &&
        'When the sales clerk lets the system calculate the customers discount on the shopping cart' &&
        'Then the discount is 0% \m/' ).
ENDMETHOD.

As you can see the scenarios the business defined are (nearly) 1:1 in your code. BDD and this kind of test automation have some major advantages:

  • Increases / improves collaboration between business, development, and QA
  • Makes communication in the team easier by using natural domain-centric language
  • The team can talk about the system behavior instead of technical implementation details
  • Devs know when they are “done” because the clearly defined scenarios can be used as  acceptance criteria
  • Test scenarios are easily readable for new developers because they are written in a natural language
  • Test steps can be reused
  • Tests can be easily parameterized
  • Tests are a living and up-to-date documentation
  • Testing is shifted to the left of the development process
  • Automated BDD tests give the developers the security that the code still works and nothing breaks
  • Fosters unit testing to implement the scenarios
  • Hides complexity of implementations behind an abstraction layer
  • etc.

So how does this work and how can you write your own BDD-style tests in ABAP?

In the Cacamber repository, there is extensive documentation and two example classes.

On a high level it works like this:

  • When you call one of Cacambers methods like GIVEN, WHEN, THEN, etc., or VERIFY, you provide a string as a parameter of these methods. This string is a single step in your test (when using GIVEN etc.) or also a complete scenario (when using VERIFY) written in natural language.
  • Cacamber will take this string and check it against the configuration that was provided via the CONFIGURE method in the SETUP of your test class.
  • The configuration consists of different entries. The first entry will be used to check if the regular expression (“pattern”) and the provided string match. If it does Cacamber will extract the variables from the string.
  • It then will call the method that was provided via the configuration and use the variables as parameters.
  • If no regular expression matches, the next configuration entry will be checked, etc.
  • If no configuration entry can be found, Cacamber throws an exception.
 
Here is a short example:

Let’s have a look at how Cacamber parsesthe given-part of our scenario:

METHOD discount_on_slayer_albums.
...
given( 'the customers first name is Dominik and his last name is Panzer' ).
...
ENDMETHOD.

From a unit testing perspective, we want to get a first name and a last name and use it to set up our test.
Our local test class needs to inherit from ZCL_CACAMBER, so you can use Cacambers features.

Then we need to tell Cacamber in the SETUP method of our local test class which method should be called when a certain regex matches:

...
configure->( pattern = '^the customers first name is (.+) and his last name is (.+)$' methodname = 'set_first_and_second_name' ).
...

This configuration will call the method SET_FIRST_AND_SECOND_NAME when the regex PATTERN matches. Additionally, it will extract the two variables from the placeholder “(.+)” and use them as importing parameters for SET_FIRST_AND_SECOND_NAME. The method looks like this:

...
PUBLIC SECTION.
METHODS set_first_and_second_name IMPORTING first_name TYPE char30
last_name TYPE char30.
...
METHOD set_first_and_second_name.
discount_calculator->set_first_name( first_name ).
discount_calculator->set_last_name( last_name ).
ENDMETHOD.
...

Inside these methods, you can place the actual logic of your tests, like calling different methods of your business object.

You will also need to do assertions for your unit test. Usually, this is done with the THEN keyword:

...
then( 'the discount is 66% \m/' ).
...

So we tell Cacamber in our SETUP-method to call the method EVALUATE_APPLIED_DISCOUNT when the following regex matches

...
configure( pattern = '^the discount is (.+)% \\m\/$' method_name = 'evaluate_applied_discount' ).
...

The method EVALUATE_APPLIED_DISCOUNT looks like this:

PUBLIC SECTION.
METHODS: evaluate_applied_discount IMPORTING expected TYPE int4.

METHOD evaluate_applied_discount.
cl_abap_unit_assert=>assert_equals( msg = |{ current_feature }: { current_scenario }| exp = expected act = discount ).
ENDMETHOD.

That’s it: Define a pattern in the configuration and tell Cacamber what public method of your test class should be called.

For more details please have a look at the provided example classes.

So how can you include Cacamber In your development process?

  1. Identify the important business features of your software and prioritize them.
  2. Identify different scenarios of these features and prioritize them.
  3. Define acceptance test descriptions for these scenarios. Use the Gherkin language for this. Make sure the whole team understands them. Scenarios are no technical descriptions. They describe the users’ intention, what the user actually does and what he wants to achieve.
  4. Write the first scenario with Cacamber consisting of the relevant steps and place an ASSERT in the THEN step method.
  5. The new test method will most likely fail with an exception because the step methods are not implemented or the ASSERT failed. It might also go green if the step methods are already implemented and the existing business logic is able to pass the criteria of the ASSERT. Then you are done.
  6. If the scenario fails, you will most likely need to write a new method for your business logic. Use TDD for this: Red-Green-Refactor. When your TDD tests are green, place the newly created or changed method into your step method. There might be more than one method call to your business logic in a single step method. You will also need to save the results of your business logic in attributes, so other step methods can access it (one step calculates a value, the next step validates it etc.)
  7. Repeat until your scenario is green.
  8. Refactor.
  9. Start with the next scenario and reuse your steps methods.

No comments:

Post a Comment