ABAP Unit Testing Part 2: Test Doubles and Mocking in SAP
ABAP Mocking

ABAP Unit Testing Part 2: Test Doubles and Mocking in SAP

If you’ve already taken the first steps into ABAP unit testing, you know the basics: writing test classes, using CL_ABAP_UNIT_ASSERT, and structuring your tests around the Arrange-Act-Assert pattern. But here’s where most developers hit a wall — how do you test a class that calls a database, triggers a BAPI, or depends on system state you can’t control? This is where test doubles, mocking strategies, and proper dependency injection in ABAP become absolutely essential. In this second part of the ABAP Unit Testing series, we’re going deep into these techniques so you can build truly isolated, reliable, and maintainable tests in SAP S/4HANA.

If you haven’t read the foundation article yet, I’d strongly recommend starting with ABAP Unit Testing in SAP S/4HANA: A Senior Architect’s Guide to Writing Tests That Actually Matter before diving in here.


Why Test Isolation Is the Real Challenge

Let me be direct: writing a test that passes in your sandbox but fails in CI, or that takes 45 seconds because it hits the database, is not a unit test — it’s an integration test wearing a costume. I’ve reviewed codebases where developers technically had “unit tests” but 90% of them were slow, brittle, and meaningless because they weren’t isolated at all.

Real unit testing isolation means your test should:

  • Run without a database connection
  • Not depend on customizing settings or master data
  • Execute in milliseconds, not seconds
  • Be fully deterministic — same inputs, same outputs, every single time

Achieving this in ABAP requires you to design your classes with testability in mind from the start. That means dependency injection, interface-based programming, and the strategic use of test doubles.


Understanding the Test Double Taxonomy

Before writing any code, let’s align on terminology. The term “mock” is commonly used as a catch-all, but Martin Fowler’s original definitions matter here because they describe different behaviors:

Dummy

An object passed around but never actually used. It fills a parameter slot. You use dummies when a method requires a parameter you don’t care about in a specific test scenario.

Stub

Returns predefined answers to calls made during the test. A stub doesn’t verify behavior — it just returns what you tell it to. Use stubs when you need to control what a dependency returns.

Spy

A stub that also records information about how it was called. Useful for verifying that certain methods were invoked with the right parameters.

Mock

Pre-programmed with expectations about which calls it should receive. If the expectations aren’t met, the test fails. Mocks verify interaction behavior.

Fake

A working implementation that takes shortcuts — like an in-memory database or a simplified version of a service. Fakes have actual business logic but aren’t suitable for production.

In ABAP, you’ll most commonly use stubs and fakes built through interface implementations, and increasingly, you’ll leverage the ABAP Test Double Framework for mock behavior.


Dependency Injection in ABAP: Making Your Classes Testable

Before you can inject a test double, your production class needs to accept dependencies from the outside rather than creating them internally. This is the single most impactful architectural change you can make for testability.

The Anti-Pattern: Hard-Coded Dependencies


CLASS zcl_order_processor DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS process_order IMPORTING iv_order_id TYPE vbeln.
  PRIVATE SECTION.
    DATA: mo_db_reader TYPE REF TO zcl_db_reader. " Hard dependency!
ENDCLASS.

CLASS zcl_order_processor IMPLEMENTATION.
  METHOD process_order.
    " Creating dependency internally - untestable!
    mo_db_reader = NEW zcl_db_reader( ).
    DATA(ls_order) = mo_db_reader->get_order( iv_order_id ).
    " ... processing logic
  ENDMETHOD.
ENDCLASS.

You cannot unit test this class without hitting the database. Every test is an integration test.

The Solution: Constructor Injection via Interface

First, define an interface for the dependency:


" Step 1: Define the interface
INTERFACE zif_order_reader PUBLIC.
  METHODS get_order
    IMPORTING iv_order_id   TYPE vbeln
    RETURNING VALUE(rs_order) TYPE zst_order
    RAISING   zcx_order_not_found.
ENDINTERFACE.

" Step 2: Production implementation
CLASS zcl_db_order_reader DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_order_reader.
ENDCLASS.

CLASS zcl_db_order_reader IMPLEMENTATION.
  METHOD zif_order_reader~get_order.
    " Real DB call here
    SELECT SINGLE * FROM vbak INTO CORRESPONDING FIELDS OF rs_order
      WHERE vbeln = iv_order_id.
    IF sy-subrc <> 0.
      RAISE EXCEPTION TYPE zcx_order_not_found.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

" Step 3: Refactored processor using constructor injection
CLASS zcl_order_processor DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_reader TYPE REF TO zif_order_reader.
    METHODS process_order
      IMPORTING iv_order_id    TYPE vbeln
      RETURNING VALUE(rv_result) TYPE string.
  PRIVATE SECTION.
    DATA mo_reader TYPE REF TO zif_order_reader.
ENDCLASS.

CLASS zcl_order_processor IMPLEMENTATION.
  METHOD constructor.
    mo_reader = io_reader.
  ENDMETHOD.

  METHOD process_order.
    TRY.
        DATA(ls_order) = mo_reader->get_order( iv_order_id ).
        " Business logic here — completely testable!
        rv_result = |Order { iv_order_id } processed for { ls_order-kunnr }|.
      CATCH zcx_order_not_found.
        rv_result = 'Order not found'.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

Now your class is testable. You can inject any implementation of zif_order_reader — including a test double — into the constructor.


Hand-Crafted Stubs: The Pragmatic Approach

The simplest form of a test double in ABAP is a stub you write yourself inside your test class. It’s fast to build and extremely readable.


CLASS ltc_order_processor_test DEFINITION FINAL FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    " Inner stub class implementing the interface
    CLASS lcl_order_reader_stub DEFINITION.
      PUBLIC SECTION.
        INTERFACES zif_order_reader.
        DATA ms_order TYPE zst_order.
        DATA mv_should_fail TYPE abap_bool.
    ENDCLASS.

    CLASS lcl_order_reader_stub IMPLEMENTATION.
      METHOD zif_order_reader~get_order.
        IF mv_should_fail = abap_true.
          RAISE EXCEPTION TYPE zcx_order_not_found.
        ENDIF.
        rs_order = ms_order.
      ENDMETHOD.
    ENDCLASS.

    METHODS test_process_order_success FOR TESTING.
    METHODS test_process_order_not_found FOR TESTING.
ENDCLASS.

CLASS ltc_order_processor_test IMPLEMENTATION.

  METHOD test_process_order_success.
    " Arrange
    DATA(lo_stub) = NEW lcl_order_reader_stub( ).
    lo_stub->ms_order = VALUE zst_order( vbeln = '0000001234' kunnr = 'CUST001' ).

    DATA(lo_processor) = NEW zcl_order_processor( io_reader = lo_stub ).

    " Act
    DATA(lv_result) = lo_processor->process_order( '0000001234' ).

    " Assert
    cl_abap_unit_assert=>assert_char_cp(
      act = lv_result
      exp = '*CUST001*'
      msg = 'Result should contain customer number'
    ).
  ENDMETHOD.

  METHOD test_process_order_not_found.
    " Arrange
    DATA(lo_stub) = NEW lcl_order_reader_stub( ).
    lo_stub->mv_should_fail = abap_true.

    DATA(lo_processor) = NEW zcl_order_processor( io_reader = lo_stub ).

    " Act
    DATA(lv_result) = lo_processor->process_order( '9999999999' ).

    " Assert
    cl_abap_unit_assert=>assert_equals(
      act = lv_result
      exp = 'Order not found'
      msg = 'Should handle missing orders gracefully'
    ).
  ENDMETHOD.

ENDCLASS.

Clean, isolated, and runs in milliseconds. No database. No setup. No teardown drama.


Using the ABAP Test Double Framework (ABAP TDF)

For more complex scenarios — especially when you need to verify that specific methods were called with specific parameters — the ABAP Test Double Framework is your tool of choice. It’s available from SAP NetWeaver 7.40 SP08 onwards and significantly reduces the amount of stub boilerplate you need to write.

TDF is particularly powerful for mocking ABAP DB access objects and interfaces without writing manual stub classes.


" Using ABAP Test Double Framework to mock an interface
METHOD test_with_tdf_mock.
  " Create a test double for the interface
  DATA(lo_mock) = CAST zif_order_reader(
    cl_abap_testdouble=>create( 'ZIF_ORDER_READER' )
  ).

  " Configure the expected return value
  DATA(ls_expected_order) = VALUE zst_order(
    vbeln = '0000005678'
    kunnr = 'BIGCLIENT'
  ).

  cl_abap_testdouble=>configure_call( lo_mock
    )->returning( ls_expected_order
    )->when_method( 'GET_ORDER'
    )->is_called_with( '0000005678'
  ).

  " Inject the mock and execute
  DATA(lo_processor) = NEW zcl_order_processor( io_reader = lo_mock ).
  DATA(lv_result) = lo_processor->process_order( '0000005678' ).

  " Assert outcome
  cl_abap_unit_assert=>assert_char_cp(
    act = lv_result
    exp = '*BIGCLIENT*'
  ).

  " Verify the mock was called as expected
  cl_abap_testdouble=>verify_expectations( lo_mock ).
ENDMETHOD.

The verify_expectations call at the end is what separates mocks from stubs — it confirms not just what was returned, but that the interaction happened correctly.


Faking the Database Layer: OSQL Test Environment

Sometimes you genuinely need some data in a table-like structure without hitting the real database. SAP provides the OSQL Test Environment (Open SQL Test Environment) which lets you inject test data for SELECT statements:


METHOD setup.
  " Prepare the OSQL test environment
  cl_osql_test_environment=>create(
    i_dependency_list = VALUE #(
      ( 'VBAK' )
      ( 'VBAP' )
    )
  )->insert_test_data(
    i_data = VALUE vbak_tt(
      ( mandt = sy-mandt vbeln = '0000001111' kunnr = 'TESTCUST' erdat = sy-datum )
    )
  ).
ENDMETHOD.

This approach is extremely useful when your class directly accesses database tables rather than going through an abstraction layer. However, I always recommend introducing that abstraction layer if you’re designing new code. Direct table access in business logic classes is a code smell from a testing perspective.


Structuring Your Test Classes for Scale

As your codebase grows, you’ll need test infrastructure that scales. Here are the patterns I recommend from experience:

1. One Test Class Per Production Class

Keep your test classes co-located in the same development object. Use meaningful test method names that describe the scenario: test_given_invalid_order_when_processing_then_returns_error_message is far more useful than test_01.

2. Shared Test Data Builders

If multiple test methods need the same data structures, create a local builder class inside your test class to avoid duplication:


CLASS lcl_test_data_builder DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS build_order
      IMPORTING iv_customer TYPE kunnr DEFAULT 'TESTCUST'
      RETURNING VALUE(rs_order) TYPE zst_order.
ENDCLASS.

CLASS lcl_test_data_builder IMPLEMENTATION.
  METHOD build_order.
    rs_order = VALUE #(
      vbeln = '0000001234'
      kunnr = iv_customer
      erdat = '20240101'
      netwr = '1000.00'
    ).
  ENDMETHOD.
ENDCLASS.

3. Setup and Teardown Discipline

Use SETUP to initialize what every test needs, and TEARDOWN to clean up. But don’t overload SETUP — if only two out of ten tests need a certain object, initialize it in those two tests only. Shared setup creates hidden coupling.


Common Pitfalls I’ve Seen in the Field

After reviewing dozens of SAP codebases over the years, these are the testing mistakes I see most often:

  • Testing framework code, not business logic: Don’t test that SAP’s SELECT works. Test your logic that uses the result.
  • Overly broad test methods: One test should verify one behavior. If your test method has 15 assertions, split it.
  • Missing negative path coverage: Every TRY/CATCH block needs a test that actually exercises the CATCH branch.
  • Skipping tests under time pressure: I’ve heard “we don’t have time for tests” more times than I can count. You always pay later — either in debugging time or in production incidents.
  • Not running tests in CI: Tests only provide value if they’re executed continuously. Integrate your ABAP Unit tests into your transport pipeline using SCI or a CI/CD framework.

For a broader look at maintainable code practices that complement testing, check out the ABAP Clean Code in Practice: Refactoring Legacy SAP Code to Modern Standards guide — the principles there directly support testability.


Connecting Tests to Your Error Handling Architecture

If you’ve built a proper exception handling architecture (which we’ve covered in depth in the ABAP Exception Handling series), your tests should explicitly verify exception behavior. Use assert_that_an_exception_is_raised patterns or wrap test execution in TRY/CATCH blocks with explicit assertions on the exception type and message.


METHOD test_invalid_input_raises_exception.
  DATA(lo_stub) = NEW lcl_order_reader_stub( ).
  lo_stub->mv_should_fail = abap_true.
  DATA(lo_processor) = NEW zcl_order_processor( io_reader = lo_stub ).

  TRY.
    " If this doesn't raise, test should fail
    lo_processor->process_critical_order( '' ).
    cl_abap_unit_assert=>fail( 'Expected exception was not raised' ).
  CATCH zcx_invalid_input INTO DATA(lx_error).
    cl_abap_unit_assert=>assert_char_cp(
      act = lx_error->get_text( )
      exp = '*order ID*'
      msg = 'Exception message should reference order ID'
    ).
  ENDTRY.
ENDMETHOD.

Key Takeaways

Let me summarize what we’ve covered in this installment:

  • Design for testability first: Use interfaces and constructor injection to make your classes naturally testable.
  • Choose the right test double: Stubs for controlling return values, mocks for verifying interactions, fakes for complex behavioral substitution.
  • ABAP TDF is powerful: For interface mocking with interaction verification, the Test Double Framework beats hand-crafted stubs.
  • OSQL Test Environment bridges the gap when you need data without a real database.
  • Scale with structure: Test data builders, clear naming conventions, and disciplined setup/teardown keep your test suites maintainable as they grow.

Testing isn’t something you bolt on at the end of a project. It’s an architectural discipline that shapes how you write production code. The developers and architects who embrace this early are the ones who sleep better at night when a deployment goes to production.


What’s Next?

In Part 3 of this series, we’ll look at integrating ABAP Unit Testing into your CI/CD pipeline — how to run tests automatically on transport release, how to configure the Code Inspector to enforce test coverage thresholds, and how to build a quality gate that actually prevents bad code from reaching production.

Until then — go write a test for that class you’ve been meaning to refactor. You know the one.


Found this useful? Share it with your SAP development team, or drop a comment below with the biggest testing challenge you’re facing in your current project. I read every comment and respond where I can.

Further Reading

Testing RAP business logic? See ABAP RAP Behavior Definitions Deep Dive (Part 2).