Guide for writing effective ABAP Unit tests including test class setup, assertions, test doubles, mocking frameworks, and test environments for CDS, SQL, and RAP.
Workflow
-
Determine the testing goal:
- Testing business logic in a class method
- Testing a CDS view entity
- Testing a RAP BO behavior implementation
- Testing database-dependent logic with SQL test doubles
- Setting up test doubles for external dependencies
-
Choose the right approach:
- Direct unit test for pure logic (no dependencies)
- Constructor/setter injection for mockable dependencies
- CDS test environment for CDS view tests
- OSQL test environment for SQL-dependent code
- RAP BO test doubles for RAP behavior tests
-
Follow the AAA pattern: Arrange → Act → Assert
-
Ensure test isolation: Tests must not depend on persistent data or external systems
Test Class Fundamentals
Test Class Definition
"! Test class for ZCL_MY_CLASS
CLASS ltc_my_class DEFINITION FINAL FOR TESTING
DURATION SHORT
RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO zcl_my_class. "Class Under Test
CLASS-METHODS class_setup. "Once before all tests
CLASS-METHODS class_teardown. "Once after all tests
METHODS setup. "Before each test
METHODS teardown. "After each test
METHODS test_calculate_total FOR TESTING.
METHODS test_validate_input FOR TESTING.
METHODS test_empty_input FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_my_class IMPLEMENTATION.
METHOD class_setup.
" One-time setup for all tests in this class
ENDMETHOD.
METHOD class_teardown.
" One-time cleanup
ENDMETHOD.
METHOD setup.
" Create fresh instance before each test
cut = NEW #( ).
ENDMETHOD.
METHOD teardown.
" Cleanup after each test
ENDMETHOD.
METHOD test_calculate_total.
" Arrange
DATA(lv_quantity) = 5.
DATA(lv_price) = CONV decfloat34( '10.50' ).
" Act
DATA(lv_result) = cut->calculate_total(
iv_quantity = lv_quantity
iv_price = lv_price ).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_result
exp = CONV decfloat34( '52.50' )
msg = 'Total should be quantity * price' ).
ENDMETHOD.
METHOD test_validate_input.
cl_abap_unit_assert=>assert_true(
act = cut->validate_input( 'VALID_INPUT' )
msg = 'Valid input should return true' ).
ENDMETHOD.
METHOD test_empty_input.
TRY.
cut->validate_input( '' ).
cl_abap_unit_assert=>fail( msg = 'Should have raised exception' ).
CATCH zcx_validation_error INTO DATA(lx_error).
cl_abap_unit_assert=>assert_bound(
act = lx_error
msg = 'Exception should be raised for empty input' ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
Test Class Attributes
| Attribute | Options | Purpose |
|---|---|---|
DURATION |
SHORT / MEDIUM / LONG |
Expected execution time; SHORT < 1s (default for CI) |
RISK LEVEL |
HARMLESS / DANGEROUS / CRITICAL |
Impact on system data; HARMLESS = no DB changes |
Test Method Additions
| Addition | Purpose |
|---|---|
FOR TESTING |
Marks method as a test method |
RAISING cx_static_check |
Allows exceptions to propagate (test fails on unhandled exception) |
CL_ABAP_UNIT_ASSERT — Assertion Methods
| Method | Purpose | Example |
|---|---|---|
assert_equals |
Value equality | assert_equals( act = result exp = 42 ) |
assert_true |
Boolean true | assert_true( act = lv_flag ) |
assert_false |
Boolean false | assert_false( act = lv_flag ) |
assert_initial |
Value is initial | assert_initial( act = lt_table ) |
assert_not_initial |
Value is not initial | assert_not_initial( act = lt_result ) |
assert_bound |
Reference is bound | assert_bound( act = lo_instance ) |
assert_not_bound |
Reference is not bound | assert_not_bound( act = lo_ref ) |
assert_differs |
Values are different | assert_differs( act = val1 exp = val2 ) |
assert_char_cp |
Character pattern match | assert_char_cp( act = lv_text exp = '*error*' ) |
assert_char_np |
Character pattern no match | assert_char_np( act = lv_text exp = '*secret*' ) |
assert_number_between |
Number in range | assert_number_between( number = val lower = 1 upper = 10 ) |
assert_table_contains |
Table contains line | assert_table_contains( line = wa table = lt_result ) |
assert_table_not_contains |
Table does not contain line | assert_table_not_contains( line = wa table = lt_result ) |
assert_return_code |
sy-subrc check | assert_return_code( act = sy-subrc exp = 0 ) |
fail |
Force test failure | fail( msg = 'Should not reach here' ) |
Common Assertion Patterns
" Check table has expected number of entries
cl_abap_unit_assert=>assert_equals(
act = lines( lt_result )
exp = 3
msg = 'Expected 3 result entries' ).
" Check exception message
TRY.
cut->some_method( ).
cl_abap_unit_assert=>fail( msg = 'Expected exception' ).
CATCH zcx_my_exception INTO DATA(lx).
cl_abap_unit_assert=>assert_equals(
act = lx->get_text( )
exp = 'Expected error message' ).
ENDTRY.
" Check that table contains a specific key
cl_abap_unit_assert=>assert_table_contains(
line = VALUE zstructure( key_field = 'ABC' )
table = lt_result
msg = 'Result should contain entry ABC' ).
Dependency Injection & Test Doubles
Constructor Injection Pattern
" Production interface
INTERFACE zif_data_provider.
METHODS get_data
RETURNING VALUE(rt_data) TYPE ztab_data.
ENDINTERFACE.
" Production class with injectable dependency
CLASS zcl_processor DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_provider TYPE REF TO zif_data_provider OPTIONAL.
METHODS process
RETURNING VALUE(rv_result) TYPE string.
PRIVATE SECTION.
DATA mo_provider TYPE REF TO zif_data_provider.
ENDCLASS.
CLASS zcl_processor IMPLEMENTATION.
METHOD constructor.
mo_provider = COND #(
WHEN io_provider IS BOUND THEN io_provider
ELSE NEW zcl_default_provider( ) ).
ENDMETHOD.
METHOD process.
DATA(lt_data) = mo_provider->get_data( ).
" Process data...
ENDMETHOD.
ENDCLASS.
Test Double (Manual Mock)
" Test double implementing the interface
CLASS ltd_data_provider DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_data_provider.
DATA mt_test_data TYPE ztab_data.
ENDCLASS.
CLASS ltd_data_provider IMPLEMENTATION.
METHOD zif_data_provider~get_data.
rt_data = mt_test_data.
ENDMETHOD.
ENDCLASS.
" Test class using the double
CLASS ltc_processor DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA cut TYPE REF TO zcl_processor.
DATA mo_provider TYPE REF TO ltd_data_provider.
METHODS setup.
METHODS test_process_with_data FOR TESTING.
ENDCLASS.
CLASS ltc_processor IMPLEMENTATION.
METHOD setup.
mo_provider = NEW #( ).
cut = NEW #( io_provider = mo_provider ).
ENDMETHOD.
METHOD test_process_with_data.
" Arrange — configure test double
mo_provider->mt_test_data = VALUE #(
( key = '1' value = 'A' )
( key = '2' value = 'B' ) ).
" Act
DATA(lv_result) = cut->process( ).
" Assert
cl_abap_unit_assert=>assert_not_initial( act = lv_result ).
ENDMETHOD.
ENDCLASS.
CDS Test Environment
Use cl_cds_test_environment=>create( i_for_entity = 'ZI_ENTITY' ) to stub all data sources of a CDS view. Insert test data with insert_test_data( ), clear with clear_doubles( ) in setup, and call destroy( ) in class_teardown.
OSQL (Open SQL) Test Environment
Use cl_osql_test_environment=>create( i_dependency_list = ... ) to stub specific DB tables/views for testing SQL-dependent classes. Same lifecycle pattern as CDS test environment.
RAP BO Test Doubles
- Transactional buffer double (
cl_botd_txbufdbl_bo_test_env): For testing code that consumes a RAP BO via EML - Mock EML API (
cl_botd_mockemlapi_bo_test_env): For testing RAP handler method implementations by configuring mock responses
Test Seams (Legacy Code)
Use TEST-SEAM / END-TEST-SEAM in production code and TEST-INJECTION / END-TEST-INJECTION in tests. Prefer constructor injection for new code.
For full code examples of all the above, read references/test-environment-examples.md.
Best Practices
Test Design
- One assertion concept per test — each test should verify one behavior
- Descriptive test method names —
test_reject_negative_quantitynottest_1 - Independent tests — no test should depend on another test's outcome or execution order
- Fast tests — use
DURATION SHORTand avoid unnecessary setup
Test Isolation
- Always use test doubles for external dependencies (DB, APIs, other BOs)
- Use
setup/teardownto ensure clean state - Use
class_setup/class_teardownfor expensive one-time setup (test environments) - Always call
clear_doubles( )insetupfor test environment classes - Always call
destroy( )inclass_teardown
Test Structure
- Place test classes in local test include (test classes tab in ADT)
- Prefix test doubles with
ltd_(local test double) - Prefix test classes with
ltc_(local test class) - Group related tests in the same test class
What to Test
- Business logic and calculations
- Validation rules and error cases
- Edge cases (empty input, boundary values, null references)
- CDS view calculations and aggregations
- RAP handler logic (actions, validations, determinations)
What Not to Test
- Framework-provided functionality (managed CRUD in RAP)
- Simple getter/setter methods
- ABAP runtime behavior
References
- SAP ABAP Cheat Sheets — ABAP Unit Tests
- SAP Help — ABAP Unit
- SAP Help — CDS Test Double Framework
- SAP Help — RAP BO Test Doubles
- Clean ABAP — Testing
Related Skills
- cds-view-entities: Use for testing CDS view entities with CDS test environments
- rap: Use for testing RAP business objects with RAP test doubles
- clean-abap: Use for ensuring test code follows Clean ABAP principles