ABAP Test Kit
A library that puts a fluent API on SAP's ABAP Test Double Framework: dummy, stub, spy and mock doubles of a global interface from one facade class, written against the ABAP for Cloud Development language version.
Configuring a double with SAP's ABAP Test Double FrameworkABAP Test Double FrameworkThe test double framework delivered with the ABAP platform: CL_ABAP_TESTDOUBLE creates a double of a global interface or class, configures what each call answers, and verifies the calls made on it.sap official, abap takes two statements, configure_call( ) and a call that looks like a real call but only records its arguments, and a call that matches no configuration returns initial values without saying so. This keeps that engine and puts a fluent API in front of it: stub( ) answers through when( )->with( )->returns( ), spy( ) records calls so the checks are written after the act step as was_called( )->with( )->times( ), mock( ) declares every call up front and fails at an undeclared one, and dummy( ) fills a collaborator the scenario never calls. Method names, parameter names and values are checked against the doubled type while the rule is written, and every mistake surfaces through one exception class, ZCX_ATK, whose text carries what went wrong, how to fix it, and the facts — the arguments, and the method and line of the code under test that made the call, read from the XCO call stack. It doubles global interfaces only, and ZCL_ATK is declared FOR TESTING, so production code cannot depend on it.
Facts
Where it sits
Who is behind it
In the atlas: maintains 5, contributes to 8@greltelmaintainer
Before choosing this
Conditions
PracticalFunctional boundary
Classes cannot be doubled: ZCL_ATK takes the name of a global interface, and a class is rejected with a message that says to extract an interface or to double the class with CL_ABAP_TESTDOUBLE directly; local interfaces of a test include cannot be named either. Static methods and events of the doubled interface are not doubled, and with( ) compares for equality, with matchers listed as planned. Below SAP S/4HANA 2023, or outside the ABAP for Cloud Development language version, the prerequisites the README names are not met.
What it declares
Every artifact
- Type
- Package
- Origin
- Independently maintained
As a package
- Where it runs
- does not say
- Distributed through
- does not say
Connections
Testing application logic, with test doubles for its dependencies
Platform test doubles through CL_ABAP_TESTDOUBLE
Git client written in ABAP that runs inside the system
Download
README
quoted from greltel/abap-test-kit under MIT, readABAP Test Kit
Table of contents
- ABAP Test Kit
- Prerequisites
- Installation
- Quick start
- Versioning
- License
- Contributors-Developers
- Available Test Doubles
- Before and After
- Limitations
- Design Goals-Features
- To-Do
ABAP Test Kit
A small library that makes ABAP Unit tests with test doubles shorter and easier to read, for development on SAP S/4HANA and SAP BTP ABAP Environment.
It is built on top of SAP's ABAP Test Double Framework (CL_ABAP_TESTDOUBLE): the framework
still creates the doubles and intercepts the calls, the library adds a fluent API to configure
and check them, and messages that say what went wrong and how to fix it. It is meant both for
developers who write their first unit test and for those who want their tests shorter and more
descriptive.
The library is written against the ABAP for Cloud Development language version, follows Clean Core principles, and consumes released APIs only.
Prerequisites
- SAP S/4HANA 2023 (or higher) OR SAP BTP ABAP Environment
- ABAP language version: ABAP for Cloud Development
- ABAP Test Double Framework (
CL_ABAP_TESTDOUBLE) - XCO library with the call stack API (
XCO_CP_CALL_STACK), which names the method and line that called a double in failure messages - Statement compatibility from v758 and Cloud
Verified with the ABAP Cloud Developer Trial 2025 (SAP S/4HANA 2023 based) and, on every push, off-stack with the abaplint transpiler. Reports from SAP BTP ABAP Environment and S/4HANA Cloud Public Edition are welcome.
Installation
Install via abapGit into a package flagged as
ABAP Cloud in the customer namespace, e.g. ZATK. The repository is set to
the ABAP language version ABAP for Cloud Development, so abapGit only imports
it into packages with that language version. Create the package and its two
sub-packages in ADT before the pull:
| Package | Super package | Content |
|---|---|---|
ZATK | - | The library: ZCL_ATK, the ZIF_ATK_* interfaces, ZCX_ATK and message class ZATK |
ZATK_TEST | ZATK | Fixtures for the unit tests of the library |
ZATK_DEMO | ZATK | A small order service, tested once with the classic framework and once with the library |
Then link https://github.com/greltel/abap-test-kit.git to ZATK, pull, and
run the unit tests of ZATK and its sub-packages; all of them should pass.
ZCL_ATK is a test class (FOR TESTING), so only test code can use it.
Quick start
The code under test gets its collaborators through the constructor, as interfaces. The test
replaces them with doubles from ZCL_ATK and injects instance( ) of each double:
CLASS ltc_order_service DEFINITION FINAL FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
DATA repository TYPE REF TO zif_atk_stub.
DATA audit_log TYPE REF TO zif_atk_spy.
DATA cut TYPE REF TO zif_atk_demo_order_service.
METHODS setup.
METHODS when_cancel_then_logged_once FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_order_service IMPLEMENTATION.
METHOD setup.
repository = zcl_atk=>stub( 'ZIF_ATK_DEMO_ORDER_REPO' ).
audit_log = zcl_atk=>spy( 'ZIF_ATK_DEMO_AUDIT_LOG' ).
cut = NEW zcl_atk_demo_order_service( repository = CAST #( repository->instance( ) )
audit_log = CAST #( audit_log->instance( ) ) ).
ENDMETHOD.
METHOD when_cancel_then_logged_once.
" arrange: the repository answers with an open order
repository->when( 'GET_ORDER' )->returns( VALUE zif_atk_demo_order_repo=>ty_order(
id = '0000004711'
status = zif_atk_demo_order_repo=>status-open ) ).
" act
cut->cancel( '4711' ).
" assert: the cancellation was written to the log, exactly once
audit_log->was_called( 'WRITE' )->with( parameter = 'ACTION' value = `CANCELLED` )->times( 1 ).
ENDMETHOD.
ENDCLASS.
Run it in ADT with Ctrl+Shift+F10. If a name is misspelled or a value does not fit the parameter, the test fails at that line with a message that says what to change.
Which double do I need?
| The collaborator... | Use | Because |
|---|---|---|
| must be passed in, but the scenario never uses it | dummy( ) | any call fails the test and names the method |
| is asked for data | stub( ) | the test decides the answers, and checks the result of the code under test |
| is told to do something | spy( ) | the test checks after the act step that it was told, and how often |
| must receive exactly the declared calls and nothing else | mock( ) | any other call fails at once; verify( ) reports missing calls |
If unsure between spy and mock, take the spy: it answers like a stub, and the checks are written after the act step, where a reader expects them.
Versioning
Releases are tagged vMAJOR.MINOR.PATCH and listed in
CHANGELOG.md, which also carries the changes on main that are
not released yet under Unreleased. One version covers the whole
repository: in abapGit open the repository, choose Switch tag and pick the
release; Switch branch to main follows the latest state. The rules for what
bumps which part of the version are in CONTRIBUTING.md.
License
This project is licensed under the MIT License.
Contributors-Developers
The repository was created by George Drakos.
Available Test Doubles
Object names follow ZCL_ATK / ZIF_ATK_* / ZCX_ATK.
| Double | Entry point | Interface | Description |
|---|---|---|---|
| Dummy | zcl_atk=>dummy( ) | ZIF_ATK_DUMMY | Fills a parameter of the code under test that the scenario never uses; any call fails the test |
| Stub | zcl_atk=>stub( ) | ZIF_ATK_STUB | Answers calls with the values, output parameters or exceptions the test defines |
| Spy | zcl_atk=>spy( ) | ZIF_ATK_SPY | A stub that records every call, so the test checks the calls after the act step |
| Mock | zcl_atk=>mock( ) | ZIF_ATK_MOCK | Strict double: every call is declared up front, any other call fails at once |
Every double follows the same shape: ZCL_ATK is the only entry point and takes
the name of a global interface. The instance methods of the interface and of its
component interfaces can be configured; static methods cannot be doubled.
Classes are rejected: from ABAP Cloud code, the calls of a class double cannot be
taken over by method name, so extract an interface and let the code depend on
it, or double the class with CL_ABAP_TESTDOUBLE directly. instance( )
hands out the object that is injected into the code under test. Rules and
checks are fluent interfaces, method names, parameter names and values are
checked against the doubled type while the test is set up, and errors surface
through one exception class, ZCX_ATK.
Cheat sheet
| I want to... | Write |
|---|---|
| inject a double | NEW zcl_service( CAST #( double->instance( ) ) ) |
| answer a method | stub->when( 'GET_ORDER' )->returns( order ) |
| answer only for an argument | stub->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = '4711' )->returns( order ) |
| fill an EXPORTING or CHANGING parameter | stub->when( 'READ' )->sets( parameter = 'MESSAGES' value = messages ) |
| raise an exception | stub->when( 'GET_ORDER' )->raises( NEW zcx_not_found( ) ) |
| check that a method was called | spy->was_called( 'WRITE' ) |
| check the arguments of the call | spy->was_called( 'WRITE' )->with( parameter = 'ACTION' value = 'CANCELLED' ) |
| check how often it was called | spy->was_called( 'WRITE' )->with( parameter = 'ACTION' value = 'CANCELLED' )->times( 2 ) |
| check that a method was not called | spy->was_not_called( 'DELETE' ) |
| declare every call up front | mock->expect_call( 'WRITE' )->times( 2 ) ... mock->verify( ) |
| name a method of a component interface | when( 'WRITE' ) or when( 'ZIF_LOG~WRITE' ) |
Names are not case sensitive. Values are converted to the parameter type without loss, so
'4711' fits a numeric text of length 10, and '47A1' is rejected.
Dummy
The code under test needs the collaborator, for example as a constructor parameter, but the scenario never calls it. Any call fails the test and names the method that was called.
| Interface | Purpose |
|---|---|
ZIF_ATK_DUMMY | instance( ) |
DATA(unused_log) = zcl_atk=>dummy( 'ZIF_AUDIT_LOG' ).
cut = NEW zcl_order_service( repository = CAST #( repository->instance( ) )
audit_log = CAST #( unused_log->instance( ) ) ).
Stub
Answers the questions of the code under test. when( ) starts a rule for one
method, with( ) narrows it to an argument value, and returns( ), sets( )
or raises( ) define the answer; sets( ) covers EXPORTING and CHANGING
parameters alike. Parameters without with( ) match any value. When several
rules match a call, the one with the most with( ) conditions wins, on a tie
the one written last. Once a method has rules, a call that matches none of them
fails the test; a method without rules returns initial values. Values are
converted to the type of the parameter without loss: '4711' for a numeric
text becomes 0000004711, while '47A1', a text longer than the parameter or
decimals that would be rounded are rejected. raises( ) accepts only exceptions
the method declares, or CX_NO_CHECK ones.
| Interface | Purpose |
|---|---|
ZIF_ATK_STUB | instance( ), when( ) |
ZIF_ATK_CALL_RULE | Fluent: with( ), returns( ), sets( ), raises( ) |
DATA(repository) = zcl_atk=>stub( 'ZIF_ORDER_REPOSITORY' ).
repository->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = '4711' )->returns( open_order ).
repository->when( 'GET_ORDER' )->returns( unknown_order ).
repository->when( 'READ_TOTALS' )->sets( parameter = 'NET' value = 100 )->sets( parameter = 'TAX' value = 24 ).
repository->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = '0000' )->raises( NEW zcx_order_not_found( ) ).
Spy
A stub that also records every call. The test checks the calls after the act
step, and every step of the check is a check of its own: was_called( ) fails
the test unless the method was called at least once, each with( ) fails unless
a call had the arguments named so far, and times( n ) fails unless exactly n
calls matched. So a check that stops early still checks something, and
was_called( 'WRITE' ) alone reads exactly as it works. For a method that must
not be called, use was_not_called( ); times( 0 ) is rejected. A failed check
shows the expected arguments, the closest actual call with the method and line it
came from, and the parameters that differ.
| Interface | Purpose |
|---|---|
ZIF_ATK_SPY | instance( ), when( ), was_called( ), was_not_called( ) |
ZIF_ATK_CALL_VERIFICATION | Fluent: with( ), times( ) - each one checks at once |
DATA(audit_log) = zcl_atk=>spy( 'ZIF_AUDIT_LOG' ).
cut->cancel( '4711' ).
audit_log->was_called( 'WRITE'
)->with( parameter = 'ORDER_ID' value = '4711'
)->with( parameter = 'ACTION' value = `CANCELLED`
)->times( 1 ).
audit_log->was_not_called( 'DELETE' ).
WRITE was called, but never with the expected arguments.
Check the code under test, or adjust the with( ) conditions.
Expected arguments: ORDER_ID = '0000004711', ACTION = 'CANCELLED'.
Closest actual call: ORDER_ID = '0000004711', ACTION = 'CANCELED' from ZCL_ORDER_SERVICE=>ZIF_ORDER_SERVICE~CANCEL, line 6 of the method.
Differs in: ACTION.
Mock
A strict double for scenarios where any unplanned call is a bug. Every expected
call is declared with expect_call( ) before the act step; a call that was not
declared fails the test at once, and verify( ) reports declared calls that did
not happen as often as declared. Without times( ) an expectation expects
exactly one call, and it can answer like a stub rule. Two expectations for the
same method are two calls: each call goes to an expectation that still waits for
a call, and among those to the most specific one.
| Interface | Purpose |
|---|---|
ZIF_ATK_MOCK | instance( ), expect_call( ), verify( ) |
ZIF_ATK_CALL_EXPECTATION | Fluent: with( ), times( ), returns( ), sets( ), raises( ) |
DATA(audit_log) = zcl_atk=>mock( 'ZIF_AUDIT_LOG' ).
audit_log->expect_call( 'WRITE' )->with( parameter = 'ACTION' value = `CANCELLED` ).
cut->cancel( '4711' ).
audit_log->verify( ).
Error messages
Every mistake in a test surfaces through ZCX_ATK. Its text has the same three parts every
time, in this order: what went wrong, how to fix it, and the facts - the arguments of the
call and the method and line of the code under test that made it, the rules that exist,
the closest one and where it differs, or what a value would become. In the ABAP Unit result, the first part is the message of the failure; the fix and
the facts are its detail, shown under it (Analysis in the SAP GUI, Details in ADT). The
examples below show the parts on separate lines. Close misspellings of method and parameter names get a suggestion; names that are
far from every candidate get the list of candidates with their kinds. The texts live in
message class ZATK; exceptions of the test double framework never reach the test
untranslated. Failures during the act step are recorded without stopping the code under
test, so a CATCH cx_root in the code under test cannot hide them; the facts of such a
failure name the method and the line that called the double (read from the XCO call
stack, with the line counted from the METHOD statement).
ZCL_ORDER_SERVICE is a class, and ATK doubles interfaces only.
Extract an interface from ZCL_ORDER_SERVICE, or double it with CL_ABAP_TESTDOUBLE.
Method GET_ORDER has no parameter CUSTOMER.
Pick one of the available names.
Available: ORDER_ID (IMPORTING), RESULT (RETURNING).
Value '12.345' does not fit parameter AMOUNT of type TY_AMOUNT (P LENGTH 8 DECIMALS 2).
Pass a value of that type, for example a typed variable.
Instead, the parameter would hold: 12.35.
FIRST_NAME of SPLIT_NAME is declared EXPORTING, not as an input.
with( ) is for IMPORTING and CHANGING; try sets( ) or returns( ).
Before and After
Package ZATK_DEMO holds a small order service, ZCL_ATK_DEMO_ORDER_SERVICE:
it reads an order from a repository and, when it cancels an open order, writes
the cancellation to an audit log. Both collaborators are injected through the
constructor. Its test include tests the same scenarios twice, in
ltc_with_raw_atdf with the classic ABAP Test Double Framework and in
ltc_with_atk with the library; the examples below are taken from there.
" Before
repository = CAST zif_atk_demo_order_repo( cl_abap_testdouble=>create( 'ZIF_ATK_DEMO_ORDER_REPO' ) ).
audit_log = CAST zif_atk_demo_audit_log( cl_abap_testdouble=>create( 'ZIF_ATK_DEMO_AUDIT_LOG' ) ).
cut = NEW zcl_atk_demo_order_service( repository = repository
audit_log = audit_log ).
" After
repository = zcl_atk=>stub( 'ZIF_ATK_DEMO_ORDER_REPO' ).
audit_log = zcl_atk=>spy( 'ZIF_ATK_DEMO_AUDIT_LOG' ).
cut = NEW zcl_atk_demo_order_service( repository = CAST #( repository->instance( ) )
audit_log = CAST #( audit_log->instance( ) ) ).
Return a value for a specific argument
" Before
cl_abap_testdouble=>configure_call( repository )->returning( lth_orders=>cancelled_order( ) ).
repository->get_order( '4711' ).
DATA(is_cancelled) = cut->is_cancelled( '4711' ).
cl_abap_unit_assert=>assert_true( act = is_cancelled msg = `A cancelled order must be reported as cancelled` ).
" After
repository->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = '4711' )->returns( lth_orders=>cancelled_order( ) ).
DATA(is_cancelled) = cut->is_cancelled( '4711' ).
cl_abap_unit_assert=>assert_true( act = is_cancelled msg = `A cancelled order must be reported as cancelled` ).
The configuration is one statement, and the second line of the classic version - a call that only records its arguments - is gone.
Raise an exception
" Before
cl_abap_testdouble=>configure_call( repository )->raise_exception( NEW zcx_atk_demo_not_found( ) ).
repository->get_order( '4711' ).
" After
repository->when( 'GET_ORDER' )->raises( NEW zcx_atk_demo_not_found( ) ).
The recording call of the classic version declares RAISING zcx_atk_demo_not_found,
so the test method needs a RAISING clause for a line that never raises. The
library also checks that GET_ORDER declares the exception.
Check that a method was called once, with the right arguments
" Before
cl_abap_testdouble=>configure_call( repository )->returning( lth_orders=>open_order( ) ).
repository->get_order( '4711' ).
cl_abap_testdouble=>configure_call( audit_log )->and_expect( )->is_called_times( 1 ).
audit_log->write( order_id = '4711'
action = zif_atk_demo_audit_log=>action-cancelled ).
cut->cancel( '4711' ).
cl_abap_testdouble=>verify_expectations( audit_log ).
" After
repository->when( 'GET_ORDER' )->returns( lth_orders=>open_order( ) ).
cut->cancel( '4711' ).
audit_log->was_called( 'WRITE'
)->with( parameter = 'ORDER_ID' value = '4711'
)->with( parameter = 'ACTION' value = zif_atk_demo_audit_log=>action-cancelled
)->times( 1 ).
Five statements become two, and the check is written after the act step instead of before it. When it fails, the message shows the closest actual call (see Spy).
Check that a method was not called
" Before
cl_abap_testdouble=>configure_call( repository )->returning( lth_orders=>cancelled_order( ) ).
repository->get_order( '4711' ).
cl_abap_testdouble=>configure_call( audit_log )->ignore_all_parameters( )->and_expect( )->is_never_called( ).
audit_log->write( order_id = '4711'
action = `` ).
cut->cancel( '4711' ).
cl_abap_testdouble=>verify_expectations( audit_log ).
" After
repository->when( 'GET_ORDER' )->returns( lth_orders=>cancelled_order( ) ).
cut->cancel( '4711' ).
audit_log->was_not_called( 'WRITE' ).
No placeholder arguments for a call that must not happen, and no
ignore_all_parameters( ) or verify_expectations( ).
Every call declared up front
The classic framework has no strict mode: a call without a matching configuration returns initial values. With the library, a mock fails the test at the undeclared call:
" After
DATA(strict_log) = zcl_atk=>mock( 'ZIF_ATK_DEMO_AUDIT_LOG' ).
strict_log->expect_call( 'WRITE' )->with( parameter = 'ACTION' value = zif_atk_demo_audit_log=>action-cancelled ).
service->cancel( '4711' ).
strict_log->verify( ).
The same mistake, both messages
Three mistakes, each made twice in the demo test class - once with CL_ABAP_TESTDOUBLE,
once with the library - and what ABAP Unit shows for each. The texts are copied from the
ADT result on an S/4HANA 2023 system; the first line is the message of the failure, the
rest its details.
The expected action is misspelled
The test expects CANCELED; the service writes CANCELLED.
" Before
cl_abap_testdouble=>configure_call( audit_log )->and_expect( )->is_called_times( 1 ).
audit_log->write( order_id = '4711' action = `CANCELED` ).
cut->cancel( '4711' ).
cl_abap_testdouble=>verify_expectations( audit_log ).
Exception Error <CX_ATD_EXCEPTION>
[ ABAP Testdouble Framework ] Method WRITE was expected to be called 1 times, but was called 0 times
" After
cut->cancel( '4711' ).
audit_log->was_called( 'WRITE' )->with( parameter = 'ACTION' value = `CANCELED` ).
WRITE was called, but never with the expected arguments.
Check the code under test, or adjust the with( ) conditions.
Expected arguments: ACTION = 'CANCELED'.
Closest actual call: ORDER_ID = '0000004711', ACTION = 'CANCELLED' from ZCL_ATK_DEMO_ORDER_SERVICE=>ZIF_ATK_DEMO_ORDER_SERVICE~CANCEL, line 6 of the method.
Differs in: ACTION.
The classic framework counts calls that match the recorded one: zero, and nothing about the call that did happen. The library shows the call, the parameter that differs and the line of the code under test that made it.
The double is configured for another order
The double answers for order 0815; the code under test asks for 4711.
" Before
cl_abap_testdouble=>configure_call( repository )->returning( lth_orders=>cancelled_order( ) ).
repository->get_order( '0815' ).
DATA(is_cancelled) = cut->is_cancelled( '4711' ).
cl_abap_unit_assert=>assert_true( act = is_cancelled msg = `A cancelled order must be reported as cancelled` ).
Critical Assertion Error: 'A cancelled order must be reported as cancelled'
True expected
" After
repository->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = '0815' )->returns( lth_orders=>cancelled_order( ) ).
DATA(is_cancelled) = cut->is_cancelled( '4711' ).
GET_ORDER was called with arguments that match none of its rules.
Add a rule for these arguments, or check the code under test.
Actual arguments: ORDER_ID = '0000004711'.
Called from: ZCL_ATK_DEMO_ORDER_SERVICE=>ZIF_ATK_DEMO_ORDER_SERVICE~IS_CANCELLED, line 2 of the method.
Rules: (ORDER_ID = '0000000815').
Closest rule differs in: ORDER_ID.
The classic framework finds no matching configuration, returns an initial order and says nothing; the test fails at its own assertion, and the developer starts debugging. The library fails at the call, with both order numbers side by side.
A value that does not fit the parameter
'ABC' for an order number of type N LENGTH 10.
" Before
DATA(order_id) = 'ABC'.
cl_abap_testdouble=>configure_call( repository )->returning( lth_orders=>cancelled_order( ) ).
repository->get_order( order_id ).
Critical Assertion Error: 'A cancelled order must be reported as cancelled'
True expected
" After
repository->when( 'GET_ORDER' )->with( parameter = 'ORDER_ID' value = order_id )->returns( lth_orders=>cancelled_order( ) ).
Exception Error <ZCX_ATK>
Value 'ABC' does not fit parameter ORDER_ID of type TY_ORDER_ID (N LENGTH 10).
Pass a value of that type, for example a typed variable.
Instead, the parameter would hold: '0000000000'.
The recording call converts 'ABC' to 0000000000 silently, so the classic framework is
configured for an order that no test asks for; the failure comes from the assertion at the
end. The library rejects the value where it is written.
Summary
In the four scenarios that exist in both test classes, the doubles need 6 statements instead of 14, and none of them is a call that only records arguments. What the library takes off your hands, compared with the classic framework used directly:
With CL_ABAP_TESTDOUBLE directly | With the library |
|---|---|
Two statements per configuration: configure_call( ), then a call that looks like a real call but only records arguments | One statement: when( )->with( )->returns( ) |
The recording call needs every mandatory parameter, even those the scenario does not care about, and a RAISING clause when the method raises | Only the parameters named in with( ) matter; the rest match any value |
A never-called check needs ignore_all_parameters( ), a placeholder call and verify_expectations( ) | was_not_called( ) |
Expectations are written before the act step and checked by verify_expectations( ) | Spy checks are written after the act step, where the reader expects them |
set_parameter( ) for outputs; EXPORTING and CHANGING are not distinguished for the reader | sets( ) for both |
| A call without a matching configuration returns initial values silently | A call that matches no rule of a configured method fails the test at the call, with the arguments |
| A wrong value in a configuration fails the test somewhere later, or not at all | Values are checked against the parameter type when the rule is written; the message says what the parameter would hold instead |
| A failed expectation says how many calls were made, not which | The failure shows the expected arguments, the closest call and the parameter that differs |
| Which line of the code under test made the unwanted or unmatched call is in the stack trace, if the failure happens at the call at all | Every recorded call carries the method and line of the code under test that made it: from ZCL_ORDER_SERVICE=>ZIF_ORDER_SERVICE~CANCEL, line 6 of the method |
Framework exceptions (CX_ATD_EXCEPTION) with technical texts | One exception class whose text says what went wrong and how to fix it, with a suggestion for misspelled names |
One difference to keep in mind: method and parameter names are strings. The classic framework catches a misspelled name at activation, the library the first time the test runs, with a suggestion for the closest name; a rename refactoring in ADT does not update them.
Limitations
- Only global interfaces can be doubled. Classes are rejected with a message that says why (see Available Test Doubles); local interfaces of a test include cannot be described by name.
- Static methods and events of the doubled interface are not doubled.
when( )on a static method reports it as unknown. - An optional parameter the caller leaves out is recorded as initial, not with its
DEFAULTvalue, because RTTI does not expose defaults. A condition on such a parameter is not met, and the failure marks the parameter with(not supplied). - Mandatory parameters typed generically as
SORTED TABLEorHASHED TABLEcannot be doubled yet;ANY TABLE,INDEX TABLE,STANDARD TABLE,ANY,DATA,c,n,p,xand the other generic types work. A condition on a generic parameter keeps the type of the value the test gives; the value of the call is compared in that type, or the other way round. - A value given with
sets( )orreturns( )for a generically typed output (EXPORTINGorCHANGINGTYPE any,TYPE STANDARD TABLE, ...) is handed to the framework without a check against the variable of the caller, because that variable is not known when the rule is written. with( )compares for equality. Matchers such as "any text containing" are planned.- A mock that is never verified checks only the undeclared calls; a forgotten
verify( )is planned to be detected by the testability analyzer. raises( )accepts exceptions the method declares andCX_NO_CHECKones; an undeclaredCX_DYNAMIC_CHECKexception is rejected, although ABAP would allow it.
Design Goals-Features
- ABAP Cloud / Clean Core compatibility — passes the ATC variant
ABAP_CLOUD_DEVELOPMENT_DEFAULT - Released APIs only (release contract C1), no dependencies besides SAP's own classes
- Built on
CL_ABAP_TESTDOUBLE— no own mocking engine; both can be used in the same test class - Tests in arrange-act-assert order: spies are checked after the act step
- Messages in three parts - what went wrong, how to fix it, the facts; framework exceptions never reach the test untranslated
- Method names, parameter names and values checked against the doubled type with RTTI
- Small public surface: one facade class, fluent interfaces and one exception class; everything else is local to
ZCL_ATK - Test code only —
ZCL_ATKisFOR TESTING, so production code cannot depend on it - Clean Code following the Clean ABAP Style Guides
- Modern ABAP syntax (7.58 / 9.14) — expressions, inline declarations, string templates
- 177 unit tests of the library run with ABAP Unit against the real
CL_ABAP_TESTDOUBLE, and off-stack on every push with the abaplint transpiler; abaplint on every push - Documented with ABAP Doc on every public declaration
To-Do
Work planned for the next releases, in the order it will be built. What is
already on main but not yet released is listed under Unreleased in
CHANGELOG.md.
Next phases
- Assertions —
zcl_atk=>expect( actual )->to_equal( expected )on top ofCL_ABAP_UNIT_ASSERT, with matchers for tables, structures and exceptions, and failure messages that always show the expected and the actual value - Testability analyzer — an ATC check or abaplint rules that explain why a
class is hard to test (
SELECTin a method,NEWof dependencies, static calls, function modules) and how to fix each finding; it will also check the method and parameter names in tests that use the library, and find mocks withoutverify( ) - Koans — red-to-green exercises built on the test doubles and the assertions
Improvements to the test doubles
- Consecutive answers — a different answer for the first, second and following calls of a method
- Matchers for
with( )— conditions such as "any text containing" instead of equality - Argument capture — read the arguments of a recorded call in the test, for assertions on structures and tables with the developer's own checks
- Events — raise the events of a doubled type
- Generic sorted and hashed tables — mandatory parameters typed
SORTED TABLEorHASHED TABLE
History
Changed 3, Fixes 1
Notes
No change in behavior or public API; updating from 1.1.0 is a plain pull.
Changed
- README: section The same mistake, both messages - three mistakes made with
CL_ABAP_TESTDOUBLEand with the library, with the texts ABAP Unit shows for each. - Off-stack toolchain: the unit tests and abaplint use
CL_ABAP_TESTDOUBLEand ABAP Unit of open-abap-core; the ATDF stand-in, the runtime patches andabaplint-stubs/are gone, and no test is skipped off-stack any more. Needs@abaplint/transpiler-cliand@abaplint/runtime2.14.3 or later. open-abap-core and open-abap-xco are development dependencies inpackage.json, pinned to a commit. - Internal: reading an argument from the test double framework and copying it into the
parameter type are separate steps in
lcl_doubled_method.
Fixed
- CONTRIBUTING and the comments of the unit tests no longer say that some tests run on a
system only: all 177 run off-stack as well. The release steps name the two version
entries of
package-lock.json.
Features 1, Changed 5, Fixes 10
Notes
Added
- Every recorded call carries the method of the code under test that made it and the
line within that method, read from the XCO call stack while the double answers
(
from ZCL_ORDER_SERVICE=>ZIF_ORDER_SERVICE~CANCEL, line 6 of the method). A failure at the call - an unmatched or unwanted call, a dummy that is called - has it as the factCalled from:; the matching and closest calls of a spy or mock failure show it after their arguments.
Changed
- Breaking: every step of a spy check checks on its own.
was_called( )fails the test at once unless the method was called at least once, eachwith( )fails unless a recorded call has the arguments named so far, andtimes( n )needsnof 1 or more -times( 0 )is rejected,was_not_called( )is the check for a method that must not be called. Before, a check withouttimes( )checked nothing. New problemsno_call_recorded(027/124) andno_matching_call(028/125);negative_expected_calls(016/113) is gone,times( )of a spy and of a mock both raiseinvalid_expected_calls. - A failure reported to ABAP Unit has what went wrong as its message and the fix and the
facts as its detail, so the failure list stays readable and the rest shows under it
(Analysis in the SAP GUI, Details in ADT).
zcx_atk->get_text( )is unchanged. - The facts about a value that does not fit name the type of the value and what the
parameter would hold instead as labeled sentences (
The value has the type:,Instead, the parameter would hold:), so long type names are no longer cut at the 50 characters of a message placeholder. - The failure of
was_called( )on a method that was never called names the other methods the double received calls for, with their counts, or says that the double received no call at all. The arguments of a call of a method without inputs readnone, notany. - 168 unit tests.
Fixed
- A
with( )condition on an optional parameter the caller left out is never met, not even with an initial value: the method may have seen itsDEFAULT, which the double cannot know. - A call goes to an expectation that still waits for a call before a more specific one that got all its calls, as documented; the failure of an unmet expectation lists the calls that expectation answered, not the calls a twin expectation took.
- Parameters typed with the generic
c,n,porxaccept values of any length; before,CREATE DATAsilently gave them the standard length and every longer value was refused. - A table value in another row order than the sorted table type of the parameter is accepted.
- A condition on a generic parameter also matches when the value of the call fits the type of
the condition the other way round (
'4711'against a numeric text). - A method named with the prefix of the doubled interface itself (
'ZIF_ORDERS~GET_ORDER'), or by an alias declared in the interface, is found. - A failure to read the call stack no longer fails the call; the origin is left out.
- An enumerated value is only compared with a value of its own type.
- Fix texts for
with( ),sets( )andreturns( )name the right alternative, and an interface without instance methods listsnoneinstead of nothing. - The unit tests of
ZCL_ATKno longer depend on the demo package.
- Every recorded call carries the method of the code under test that made it and the
line within that method, read from the XCO call stack while the double answers
(
Features 9
Notes
Added
Test doubles on top of
CL_ABAP_TESTDOUBLE.ZCL_ATKwithdummy( ),stub( ),spy( )andmock( )for global interfaces. Classes are rejected with a message that points to an interface or toCL_ABAP_TESTDOUBLE.- Rules with
when( ),with( ),returns( ),sets( )andraises( ); the most specific rule wins, and a call that matches no rule of a method fails the test. - Spy checks with
was_called( )->with( )->times( )andwas_not_called( ), after the act step. - Mock expectations with
expect_call( )->times( )andverify( ). Two expectations for the same method are two calls: a call goes to the most specific expectation that still waits for a call. - Methods of component interfaces (
INTERFACESinside the doubled interface), named with or without their interface prefix. - Method names, parameter names and values checked with RTTI, with suggestions for misspelled names; values converted without loss.
ZCX_ATKand message classZATK: every text has three parts, in this order - what went wrong, how to fix it, and the facts: the actual arguments, the rules or expectations of the method and where the closest one differs, the matching or closest calls, what a value that does not fit would become, the parameters or outputs a method has, the exceptions it declares. An optional parameter the caller left out is shown as(not supplied).- 133 unit tests against the real
CL_ABAP_TESTDOUBLE, and packageZATK_DEMOwith the same scenarios tested with the classic framework and with the library. - abaplint in ABAP Cloud mode and the unit tests off-stack (abaplint transpiler, open-abap-core) on every push.
Others providing
Compare all 25 on the Unit testing page →- ABAP Test Double FrameworkABAP Test Double FrameworkThe test double framework delivered with the ABAP platform: CL_ABAP_TESTDOUBLE creates a double of a global interface or class, configures what each call answers, and verifies the calls made on it.sap official, abapPlatform test doubles through CL_ABAP_TESTDOUBLEfrom ABAP
- abap2UI5 headless frontendabap2UI5 headless frontendPlays the frontend's side of the abap2UI5 request/response contract in ABAP, so an app class is driven and asserted on from a plain ABAP Unit test.community org, abapDrives abap2UI5 app classes from ABAP Unit testsfrom ABAP
- @abap2ui5/mcp-server@abap2ui5/mcp-serverAn MCP server that puts the abap2UI5 development loop on the machine an agent runs on: check the view the ABAP builds, photograph it, deploy the class into a sandbox, boot the app headless and read the errors.community org, abapMCP server for the abap2UI5 check, deploy and run loopfrom ABAP
- assertassertA fluent assertion helper for ABAP Unit tests that wraps cl_abap_unit_assert in chained calls such as assert(x)->equals(y), also usable outside tests through ZCL_ASSERTABLE.independent, abapFluent assertion helper for ABAP Unit testsfrom ABAP
- @abaplint/transpiler@abaplint/transpilerTranspiles ABAP 7.02 syntax to ES6 JavaScript, so ABAP code and its unit tests run on Node.js in CI without an ABAP system, with optional SQLite, PostgreSQL, Snowflake or SAP HANA drivers.community org, abapTranspiles ABAP to JavaScript to run on Node.jsfrom ABAP
- Modernization Agents for SAP on AWSModernization Agents for SAP on AWSAWS's suite of agents for SAP modernisation work: custom ABAP converted for S/4HANA, PI/PO interfaces moved to Integration Suite, BW objects moved to SAP Datasphere, deployed into a customer's own AWS account and reached only after registration.partner, abapAWS agents for ABAP, PI/PO and BW modernisation workfrom ABAP
- abap_db_preparatorabap_db_preparatorA wrapper over ABAP's OpenSQL replacement service that exports chosen rows into a stored bundle and, in a unit test's setup, replays it as fake table content in two method calls.independent, abapReplays stored database snapshots in ABAP unit testsfrom ABAP
- cacamber-BDD-for-ABAPcacamber-BDD-for-ABAPA BDD framework for ABAP that turns Gherkin-style Given/When/Then scenarios into ABAP Unit test steps, matching each step against a regular expression and calling the test class method it maps to.independent, abapBDD framework turning Given/When/Then into ABAP Unit stepsfrom ABAP
- abap-utesthelpabap-utesthelpTwo helper classes for ABAP Unit tests: one verifies a domain constant class against its domain's fixed values, the other creates test doubles without naming the interface.independent, abapHelper classes for ABAP Unit tests and test doublesfrom ABAP
- ABAP Debugger Data View ExtensionABAP Debugger Data View ExtensionAn extension of the ABAP New Debugger that renders the internal table or structure on screen as a typed VALUE statement, ready to paste into an ABAP Unit test.independent, abapDebugger menu entry rendering a table or structure as VALUE codefrom ABAP
- regression_testregression_testA snapshot library for ABAP that records a value once and compares against it on every later run, from either the debugger or a unit test, storing snapshots in eCATT test data containers.independent, abapSnapshot testing library for ABAP, from debugger or unit testfrom ABAP
- abapCovabapCovA report, ZABAPCOV, that uploads ABAP Unit test code coverage results to codecov.io, once the repository is installed through abapGit and codecov.io's certificates through STRUST.independent, abapUploads ABAP Unit code coverage to codecov.iofrom ABAP
- ABAP remote filesystemABAP remote filesystemA Visual Studio Code extension that opens an ABAP system over ADT as an editable filesystem and exposes system tools to GitHub Copilot and external MCP clients.independent, abapVS Code extension editing ABAP systems over ADT as a filesystemfrom ABAP
- open-steamgateopen-steamgateAn ABAP application server you clone and run on your own machine: transpiled ABAP serving classic /IWBEP/ OData v2 to Fiori over a local database, with no SAP system attached.independent, abapLocal ABAP server serving /IWBEP/ OData v2 without an SAP systemfrom ABAP
- express-icf-shimexpress-icf-shimA shim that runs an ABAP ICF handler written against if_http_extension behind an Express 4.x server, for ABAP transpiled to JavaScript and run on Node.js in testing and continuous integration.community org, abapShim running an ABAP ICF handler behind an Express 4.x serverfrom ABAP
- open-abap-coreopen-abap-coreReusable ABAP functionality for code transpiled by @abaplint/transpiler: the core ABAP artifacts that code runs against on Node.js.community org, abapCore ABAP artifacts for code transpiled by @abaplint/transpilerfrom ABAP
- ABAP Development Tools for VS CodeABAP Development Tools for VS CodeSAP's ABAP tools in VS Code: editing and debugging against a connected system, ABAP Unit and ATC runs, transport management in the Transport Organizer, over RFC to an on-premise system or HTTP to a cloud one, with the ADT MCP Server behind an AI-unit licence.sap official, abapSAP's ABAP tools in VS Code, over RFC to on-premise or HTTP to cloudfrom ABAP
- mockup_loadermockup_loaderA tool that loads ABAP unit test data prepared in Excel as TAB-delimited text into ABAP structures and tables, and generates interface stubs that return that mock data.independent, abapLoader of Excel-prepared ABAP unit test data, with stub generationfrom ABAP
- mockAmockAA free mocking framework for ABAP, published under the Apache License 2.0.independent, abapA free mocking framework for ABAP, published under the Apache License 2.0.from ABAP
- Vise_for_ABAPVise_for_ABAPA characterisation-test helper: ZCL_VISE=>GRIP records a field's value once and fails a later run if it has changed.independent, abapA characterisation-test helper: `ZCL_VISE=>GRIP` records a field's value once and fails a later run if it has changed.from ABAP
- abap-senchaabap-senchaA BDD/TDD assertion library for ABAP Unit, styled after the JavaScript library Chai.js, that wraps cl_abap_unit_assert in chainable expect, should and assert styles.independent, abapBDD/TDD assertion library for ABAP Unit, styled after Chai.jsfrom ABAP
- abap-test-containerabap-test-containerA test data container for ABAP Cloud that loads unit test data in JSON, XML or raw formats from GitHub and maps it onto the target structure, filling the platform's missing native one.independent, abapGitHub-hosted test data container for ABAP Cloudfrom ABAP
- abap-unit-runnerabap-unit-runnerAn application job that runs and schedules ABAP Unit tests in SAP's ABAP Environment through the SAP_COM_0735 API, where the on-premise test runner has no SAP GUI to run from.independent, abapApplication job that runs and schedules ABAP Unit tests in the cloudfrom ABAP
- adash-servicesadash-servicesThe backend service behind ADASH Cli, exposing ABAP Unit test runs and coverage as REST and OData services for CI/CD pipelines, with a report scheduled to run tests per package.independent, abapBackend exposing ABAP Unit runs and coverage over REST and ODatafrom ABAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
abaplint
Flavors
“The library is written against the **ABAP for Cloud Development** language version, follows **Clean Core** principles, and consumes **released APIs only**.” — greltel/abap-test-kit — README
History in the atlas
- First seen.
For your agent
- Ref
artifact:greltel-abap-test-kit- JSON
- /artifacts/greltel-abap-test-kit.json
- MCP
- Connecting an agent