Skip to content

dominikpanzer/cacamber-BDD-for-ABAP

Repository files navigation

Run abaplint Run Unit Tests abap package version Twitter

Cacamber - the BDD-Framework for ABAP

Hi! Cacamber makes it possible for you to run BDD-style tests in a SAP-system. Behavior driven development is an agile approach to software development. It focusses on collaboration through a common language between the customer and developers, testers etc. by defining examples which describe the behavior of a software system in a natural language. This language has a specific format and is called Gherkin. When using the ubiquitous language it looks like this (note: Cacamber supports different languages):

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/

These scenarios or examples will be delivered by the business or ideally by the 3 amigos and will be used as executable specifications. You guessed it right: automated tests! So how can we execute these scenarios as automated tests?

You might already know the Given ... When ... Then pattern. It's quite similar to arrange ... act ... assert. They are both techniques to give tests a clear structure. In ABAP this usually looks like this. We focus on the the feature, 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

The Given ... When ... Then comments are not very helpful. 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! Personally I only know one ABAP project with such kind of tests. 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 test case description. 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.

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 it fails ABAP Unit can tell you this way:

...
Critical Assertion Error: 'Discount Calcuation: 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.

Cacamber supports keywords in different languages, so there is no need for you to translate your test cases to english. Have I sparked your interest? Great.

Give a Star! ⭐

If you like or are using this project please give it a star. Thanks!

Examples

If you don't like to read docs, check out the example class ZCL_BDD_EXAMPLE and ZCL_BDD_EXAMPLE_2, which show how to use Cacamber. They provide implementations of the above scenario "Discount on Slayer albums for VIP Slayer fans (exclusive contract with BMG)".

The BDD cycle when using Cacamber

BDDs main target is to deliver quality software by making the communication between different team members easier. But most people just think about tools like Cucumber (or Cacamber ;-)) and test automation whenever they hear about BDD.

So how does BDD work and how does Cacamber fit in? Let us have a look:

  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. A step method needs to be a public method of your test class. Make sure there is just one THEN. You will need to inherit your test class from ZCL_CACAMBER and define a FEATURE and the CONFIGURATION for the steps in the SETUP of your test class. You will also need a test method FOR TESTING, put a SCENARIO and different steps using GIVEN, WHEN, THEN and the other available methods into the test 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.

If this is too abstract or too high level for you, have a look at the example implementation ZCL_BDD_EXAMPLE and ZCL_BDD_EXAMPLE_2 or check out the Cacamber API documentation.

Advantages of BDD

  • 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 devs 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 dev process
  • Automated BDD tests give the devs 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.

How does Cacamber technically work?

Cacamber basically 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.

Cacamber API

This part of the document describes the public methods of Cacambers main class ZCL_CACAMBER. It provies methods for english Gherkin keywords. If you want to use another language, have a look at this mapping table:

English German
ZCL_CACAMBER ZCL_CACAMBER_GERMAN
Given Angenommen
And Und
Or Oder
But Aber
_ _
When Wenn
Then Dann
Feature Funktion
Scenario Szenario
Example Beispiel
Rule Regel

To get startet with Cacamber using english keywords, your local test class needs to inherit from ZCL_CACAMBER. For other languages have a look at the previous table.

ZCL_CACAMBER as your superclass provides the following public methods:

FEATURE

The FEATURE method is optional and can be used to structure your test cases in a domain-centric way. Usually one test class will represents one feature, e.g. "discount calculation". This method has to be used in the SETUP of your test class. A FEATURE can be used to enhance the MSG parameter of your asserts.

Importing parameters:

  • FEATURE - the name of your feature, max. 255 characters long

Example:

...
feature( 'Discount Calcuation' ).
...
cl_abap_unit_assert=>assert_equals( msg = current_feature exp = expected act = actual ).
...

CONFIGURE

The method CONFIGURE maps a regex-string to a method, which should be executed whenever the regex matches. This is called a step. If you are not a regex-pro, you can use tools like regex101 to make things easier. Inside the regex you can use (.+) or other matchers to extract the variables from the string, which will be used by Cacamber as parameters for the method call. The order of the variables must match the order of the parameters of the step method which should be called when the regex matches. Your method is only allowed to have IMPORTINGparameters. The configuration is usually done in the SETUP-method of your test class.

Supported types and regular expressions: Currently Cacamber is able to handle the following basic datatypes, which you can use in your step methods parameters. You can also use DDIC types based on these.

datatype example matcher description example value
STRING ^hello (.+)$ a string can basically by anything, use this as a fallback, ALPHA IN will be applied Dominik
DATE ^today is (.+)$ DD.MM.YYYY a date will automatically be converted to internal format 24.12.2023
TIMS ^it's (.+) o'clock$ HH:MM:SS a time will automatically be converted to internal format 14:01:00
CHAR ^add (.+) to cart$ a character sequence book
INTEGER ^I am (.+) years old$ for positive or negative integers -200
PACKED ^the price is (.+)$ a number with decimals seperated by a dot 1001.50
datatable ^Slayer had the following members:(.+)$ a line or a table see section about datatables

Importing parameters:

  • PATTERN - a REGEX-string which is used as a matcher
  • METHODNAME - name of a public method of your test class with a matching interface. Can be upper or lower case.

Example:

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

The public method of your test class then must look 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.

Your test method looks like this:

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

SCENARIO

A scenario describes a variant of a feature and represents a test case. Therefore the SCENARIO method is usually used as the first method call in a test method of your local test class. A SCENARIO can be used to enhance the MSG parameter of your asserts.

Importing parameters:

  • SCENARIO - the name of the scenario, max. 255 characters long

Example:

...
scenario( 'Discount on Slayer Albums for VIP Slayer fans (exclusive contract with BMG)' ).
...
cl_abap_unit_assert=>assert_equals( msg = current_scenario exp = expected act = actual ).
...

EXAMPLE

The method EXAMPLE works the same way SCENARIO does.

Importing parameters:

  • SCENARIO - the name of the scenario, max. 255 characters long

Example:

...
example( 'Discount on Slayer Albums for VIP Slayer fans (exclusive contract with BMG)' ).
...
cl_abap_unit_assert=>assert_equals( msg = current_scenario exp = expected act = actual ).
...

RULE

With the method RULE you can describe a single business rule which will be implemented. It can be used in your local test methods just like SCENARIO or EXAMPLE. A RULE can be used to enhance the MSG parameter of your asserts.

Importing parameters:

  • RULE - description of a business rule, max. 255 characters long

Example:

...
rule( 'only VIP customers can buy the album 7 days before the officiall realease date' ).
...
cl_abap_unit_assert=>assert_equals( msg = current_rule exp = expected act = actual ).
...

GIVEN

The GIVEN method is an actual keyword of the Gherkin language. It represents a step. A BDD test case consists of steps which combined represent an expectation of the bahavior of the system. The paramter STEP will be matched against the regex-patterns of the configuration to find an actual method to process the data provided by the step. A step can use any supported data type. If this doesnt suit your usecase, import the variable as a STRING into your step method and convert it there.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example:

...
given( 'the customers first name is Dominik and his last name is Panzer' ).
...
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

Also a Gherkin keyword to write tests in a natural language. The method AND works like GIVEN.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example:

...
and( 'his birthdate according to our CRM system is 06.06.2006' ).
...

OR

Also a Gherkin keyword to write tests in a natural language. The method OR works like GIVEN.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example:

...
or( 'his birthdate according to our CRM system is 01.01.2001' ).
...

BUT

Also a Gherkin keyword to write tests in a natural language. The method Bb works like GIVEN.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example:

...
but( 'today is not the Monday' ).
...

_ (underscore)

The underscore method _ can be used to define a list, because - is not an allowed name for a method in ABAP.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example: ``ABAP ... given( 'in the shopping cart there are: ). _( '3 laptops' ). _( '5 ipads' ). _( '1 Slayer album' ). -( '1 David Hassehoff album' ). ...


### WHEN 
Also a Gherkin keyword to write tests in a natural language. It usually desribes an action.  The method `WHEN` works like `GIVEN`.

Importing parameters:
* `STEP` - a string describing something relevant to the business using the ubiquitious language.

Example:
```ABAP 
...
when( 'the sales clerk lets the system calculate the customers discount on a Slayer Album' ).
...

THEN

Also a Gherkin keyword to write tests in a natural language. It describes an expected reaction of the system. The method THEN works like GIVEN.

Importing parameters:

  • STEP - a string describing something relevant to the business using the ubiquitious language.

Example:

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

VERIFY

Verify makes it possible to parse a whole scenario at once. So you don't need to call the methods GIVEN THEN WHEN etc. Just one method call is enought. Instead you just use the Gherkin keywords you need in your description. Currently supported are:

  • Scenario: (optional)
  • Rule: (optional)
  • Given, When, Then, And, Or, But

Important: All Keywords start with a capital letter. The scenario will be split at these keywords into singular steps. The steps will be parsed by the configuration. Sadly ABAP doesn't allow multiline strings without connecting them via &&. ZCL_BDD_EXAMPLE_2 shows how to use VERIFY.

Importing parameters:

  • SCENARIO - a string containing the whole scenario

Example:

...
METHOD discount_on_slayer_albums.
verify( '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 the Slayer Album' &&
        'Then the discount is 66% \m/' ).
ENDMETHOD.
...

GET_CURRENT_FEATURE

Returns the name of the current feature which was set with FEATURE.

Returning parameters:

  • CURRENT_FEATURE - a string describing the current feature.

Example:

...
DATA(feature) = get_current_feature( ).
...

GET_CURRENT_SCENARIO

Returns the name of the current scenario which was set with SCENARIO / EXAMPLE or during a VERIFY.

Returning parameters:

  • CURRENT_SCENARIO - a string describing the current scenario.

Example:

...
DATA(scenario) = get_current_scenario( ).
...

GET_CURRENT_RULE

Returns the name of the current rule which was set with RULE or during a VERIFY.

Returning parameters:

  • CURRENT_RULE - a string describing the current rule.

Example:

...
DATA(rule) = get_current_rule( ).
...

Datatable API

If you want to use datatables in your steps, you can use the class ZCL_DATATABLE to work with them in your test methods.

FROM_STRING

To parse the table defined in your step to a datatable, you have to use the static method TO_STRING.

Importing parameters:

  • TABLE_AS_STRING - the table from your step configuration which has been extracted via your regex. Returning parameters:
  • DATATABLE- instance of ZCL_DATATABLE

Example:

CLASS testclass DEFINITION FINAL FOR TESTING INHERITING FROM zcl_cacamber DURATION SHORT RISK LEVEL HARMLESS.

PUBLIC SECTION.
METHODS: process_shopping_cart IMPORTING shopping_cart_raw TYPE string.
...
PRIVATE SECTION.
METHODS: setup.
METHODS: no_discount_on_shopping_cart FOR TESTING RAISING cx_static_check.
...

CLASS testclass IMPLEMENTATION.
METHOD setup.
...
configure( pattern = '^in his shopping cart are the following items:(.*)$' methodname = 'process_shopping_cart' ).
...
ENDMETHOD.

METHOD process_shopping_cart.
shopping_cart = zcl_datatable=>from_string( shopping_cart_raw ).
ENDMETHOD.

METHOD no_discount_on_shopping_cart.
scenario( 'Customer is not eligible for a discount on the shopping cart' ).
given( '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) |' ).
...
ENDMETHOD.
...

READ_ROW

With READ_ROW you can get the values of all columns of a certain row of your table as a table of strings for further processing.

Importing parameters:

  • ROWNUMBER - the number of the row you want to read Returning parameters:
  • LINE - the values of the cells of the row as a string table

Example:

...
DATA(line) = datatable->read_row( 666 ).
...

READ_CELL

With READ_CELL you can get the values of a cell of your table

Importing parameters:

  • ROWNUMBER - the number of the row you want to read
  • COLUMNNUMBER - the number of the column you want to read Returning parameters:
  • VALUE - the values of the cell as a string for further processing

Example:

...
DATA(cell) = datatable->read_cell( rownumber = 23 columnnumber = 23 ).
...

TO_TABLE

TO_TABLE will transform your datatable into a plain internal table of a specified table type.

Importing parameters:

  • DDIC_TABLE_TYPE_NAME - the name of a table type Exporting parameters:
  • TABLE - the converted table

Example:

...
DATA internal_table  TYPE ztt_bdd_demo.
datatable->to_table( EXPORTING ddic_table_type_name = 'ZTT_BDD_DEMO'
                     IMPORTING table = internal_table ).
...

How to install Cacamber

You can copy and paste the source code into your system or simply clone this repository with abapGit. There are currently no dependencies and it should work on 7.5x systems.

Feature List

I like to create a simple acceptance test list or feature lists before I start coding. It's my personal todo-list. Often the list is very domain-centric, this one is quite technical.

Feature List
✅ first proof of concept somehow seems to works
✅ a user can use the gherkin keyword "example" and it behaves like "scenario"
✅ a user can use the gherkin keyword "but" and it behaves like "scenario"
✅ a user can use the gherkin keyword "" for lists. because "" is not allowed in ABAP an underscore should be used. it behaves like "scenario"
✅ a user can add a rule via the keyword "rule" and it is stored so it can be used in assertions
✅ error handling: method doesnt have the same number of parameters as variables exist
✅ error handling: no regex match / no method found should throw
✅ error handling: dynamic method call for the step-method fails. it should throw.
✅ a user can define tables and those are parsed and successfully
✅ a user can can read values from those tables
✅ a user can transform the table into a real internal table
✅ a user can use empty cells in a table
✅ set up a class as example on how to use cacamber
✅ update the docs 👹
✅ fix the linter
✅ a user can use a float number in the tests (1000.25) and it is parsed into a packed datatype succcessfully
✅ a user can use a time in the test (12:00:00 or 14:01:00) and it is parsed into TIMS datatype successfully
✅ update the docs 👹
✅ add a method verify(string) which can parse a whole scenario in a string format and no single given() when() then() etc methods need to be called
✅ find beta testers
✅ add ADRs
✅ check compatibility to 7.5x-systems
✅ introduce versioning
✅ 💥 FIRST PUBLIC RELEASE 💥 Twitter + Blog
✅ enable some kind of i18n to support german Gherkin keywords
✅ fix the unit tests runner - thanks Lars
🔲 get rid of ddic objects
🔲 maybe text file parsing is a good idea? maybe not?
🔲 your awesome idea

Solution Architecture

You will find a product vision as well as all architectural decisions for Cacamber documented as Architecture Decision Records in this folder of the repository.

How to support this project

PRs are welcome! You can also just pick one of the test list entries from above and implement the solution, add your favourite language or implement your own idea. Fix a bug. Improve the docs... whatever suits you. Please keep the current ADRs in mind.

Greetings, Dominik

follow me on Twitter or Mastodon