OOP ABAP
ABAP Developmentadvanced

OOP ABAP Part 9: Dependency Injection and ABAP Unit

Key takeaway

Make classes testable with dependency injection: constructor injection, optional parameters with defaults, injection through a factory, and ABAP Unit test doubles written by hand or generated with CL ABAP TESTDOUBLE.

Explanation

📌 KEY POINTS
✅ Dependency injection (DI): a class receives the objects it needs (DAO, logger, tax calculator) instead of creating them itself.

✅ Constructor injection is the clearest form: dependencies are IMPORTING parameters of the constructor, typed as interfaces.

✅ In ABAP Unit, you inject test doubles — hand-written local classes or generated with CL_ABAP_TESTDOUBLE.

⚠️ A class that does CREATE OBJECT of its own DAO or calls static SELECT helpers cannot be isolated in tests.

━━━━━━━━━━━━━━━━━━━━
▶ WHERE WE ARE

Part 8 created objects in factories. Now the consumer receives them — and tests pass in fakes. (Modularization Part 10 introduced ABAP Unit basics; here we focus on the OO design side.)

━━━━━━━━━━━━━━━━━━━━
▶ 1. THE PROBLEM

METHOD constructor. CREATE OBJECT mo_dao TYPE zcl_order_dao. ENDMETHOD.

The class is hard-wired to the real database access. A test would need specific data in the database, which changes and differs between systems.

━━━━━━━━━━━━━━━━━━━━
▶ 2. CONSTRUCTOR INJECTION

METHODS constructor IMPORTING io_dao TYPE REF TO zif_order_dao.

  • Production code (or a factory) passes zcl_order_dao.
  • The test passes ltd_order_dao (a test double).
  • The class only knows the interface.

━━━━━━━━━━━━━━━━━━━━
▶ 3. OPTIONAL INJECTION WITH A DEFAULT

Existing callers should not change. Make the parameter OPTIONAL; in the constructor: IF io_dao IS BOUND use it, ELSE create the real one. Callers stay compatible; tests inject.

━━━━━━━━━━━━━━━━━━━━
▶ 4. INJECTION VIA FACTORY / INJECTOR

For deep call chains, the factory holds a static attribute for the instance to return. A test-only injector class (FRIENDS of the factory, FOR TESTING) sets a fake into it. This is how SAP recommends isolating dependencies in larger designs.

━━━━━━━━━━━━━━━━━━━━
▶ 5. TEST DOUBLES

  • Stub: returns fixed data (order total 1,000).
  • Mock/spy: also records calls so the test can verify them (was save called once?).
  • Hand-written local class implementing the interface — simple and readable.
  • CL_ABAP_TESTDOUBLE: cl_abap_testdouble=>create( 'ZIF_ORDER_DAO' ) generates a double; configure_call( )->returning( … ) then call the method to set the expectation.

━━━━━━━━━━━━━━━━━━━━
▶ 6. ABAP UNIT REMINDERS

FOR TESTING RISK LEVEL HARMLESS DURATION SHORT; setup creates doubles and the class under test (f_cut); one behaviour per test method; cl_abap_unit_assert=>assert_equals.

━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP

  1. Run the tests in the code example (Ctrl+Shift+F10).
  2. Change the discount threshold in lcl_order_service and watch a test fail.
  3. Replace the hand-written stub with CL_ABAP_TESTDOUBLE as in the commented section.

✅ CHECKPOINT: your classes receive their dependencies, and your tests run without depending on database contents.

Code example

ABAP Code
* ===== Production code =====INTERFACE lif_order_dao.  METHODS get_order_total IMPORTING iv_vbeln TYPE vbeln_va RETURNING VALUE(rv_total) TYPE netwr_ak.  METHODS save_discount   IMPORTING iv_vbeln TYPE vbeln_va iv_discount TYPE netwr_ak.ENDINTERFACE. CLASS lcl_order_dao DEFINITION FINAL.  PUBLIC SECTION. INTERFACES lif_order_dao.ENDCLASS.CLASS lcl_order_dao IMPLEMENTATION.  METHOD lif_order_dao~get_order_total.    SELECT SINGLE netwr FROM vbak INTO rv_total WHERE vbeln = iv_vbeln.  ENDMETHOD.  METHOD lif_order_dao~save_discount.    " UPDATE of a Z table in real code  ENDMETHOD.ENDCLASS. CLASS lcl_order_service DEFINITION FINAL.  PUBLIC SECTION.    METHODS: constructor IMPORTING io_dao TYPE REF TO lif_order_dao OPTIONAL,  " injectable             apply_discount IMPORTING iv_vbeln TYPE vbeln_va                            RETURNING VALUE(rv_discount) TYPE netwr_ak.  PRIVATE SECTION.    DATA mo_dao TYPE REF TO lif_order_dao.ENDCLASS. CLASS lcl_order_service IMPLEMENTATION.  METHOD constructor.    IF io_dao IS BOUND.      mo_dao = io_dao.                       " test or caller supplies it    ELSE.      CREATE OBJECT mo_dao TYPE lcl_order_dao. " default for existing callers    ENDIF.  ENDMETHOD.   METHOD apply_discount.    DATA lv_total TYPE netwr_ak.    lv_total = mo_dao->get_order_total( iv_vbeln ).    IF lv_total >= 10000.      rv_discount = lv_total * '0.05'.       " 5% above 10,000      mo_dao->save_discount( iv_vbeln = iv_vbeln iv_discount = rv_discount ).    ENDIF.  ENDMETHOD.ENDCLASS. * ===== Test double (spy): returns fixed data and records calls =====CLASS ltd_order_dao DEFINITION FOR TESTING.  PUBLIC SECTION.    INTERFACES lif_order_dao.    DATA: mv_total      TYPE netwr_ak,          mv_save_calls TYPE i.ENDCLASS.CLASS ltd_order_dao IMPLEMENTATION.  METHOD lif_order_dao~get_order_total.    rv_total = mv_total.  ENDMETHOD.  METHOD lif_order_dao~save_discount.    mv_save_calls = mv_save_calls + 1.  ENDMETHOD.ENDCLASS. * ===== ABAP Unit =====CLASS ltc_order_service DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.  PRIVATE SECTION.    DATA: mo_dao TYPE REF TO ltd_order_dao,          f_cut  TYPE REF TO lcl_order_service.    METHODS: setup,             discount_above_threshold FOR TESTING,             no_discount_below        FOR TESTING.ENDCLASS. CLASS ltc_order_service IMPLEMENTATION.  METHOD setup.    CREATE OBJECT mo_dao.    CREATE OBJECT f_cut EXPORTING io_dao = mo_dao.      " inject the double  ENDMETHOD.   METHOD discount_above_threshold.    DATA lv_disc TYPE netwr_ak.    mo_dao->mv_total = 20000.    lv_disc = f_cut->apply_discount( '0000004711' ).    cl_abap_unit_assert=>assert_equals( act = lv_disc exp = 1000 msg = '5% discount expected' ).    cl_abap_unit_assert=>assert_equals( act = mo_dao->mv_save_calls exp = 1 msg = 'Discount must be saved once' ).  ENDMETHOD.   METHOD no_discount_below.    DATA lv_disc TYPE netwr_ak.    mo_dao->mv_total = 5000.    lv_disc = f_cut->apply_discount( '0000004712' ).    cl_abap_unit_assert=>assert_initial( act = lv_disc ).    cl_abap_unit_assert=>assert_equals( act = mo_dao->mv_save_calls exp = 0 msg = 'Nothing to save' ).  ENDMETHOD.ENDCLASS. * ===== Generated double with CL_ABAP_TESTDOUBLE (global interface needed) =====* DATA lo_dao TYPE REF TO zif_order_dao.* lo_dao ?= cl_abap_testdouble=>create( 'ZIF_ORDER_DAO' ).* cl_abap_testdouble=>configure_call( lo_dao )->returning( 20000 ).* lo_dao->get_order_total( '0000004711' ).     " records the expectation* CREATE OBJECT f_cut EXPORTING io_dao = lo_dao.

Real project scenario

A discount class created its own DAO internally, so its tests used real sales orders from the development system. After a client copy the orders changed, all tests failed, and the team started ignoring the test results.
They added an optional constructor parameter for the DAO, wrote a small spy double and rewrote the tests to control data completely. Tests became stable across systems, and a later change to the discount threshold was caught by a failing test before transport.

Common mistakes

  • Creating dependencies inside the class (CREATE OBJECT of concrete DAOs).
  • Static helper calls with SELECTs inside business logic.
  • Tests relying on database data that changes.
  • Typing dependencies as concrete classes instead of interfaces.
  • Test doubles that contain business logic themselves.

Best practices

  • Inject dependencies via the constructor, typed as interfaces.
  • Use OPTIONAL parameters with defaults to keep callers compatible.
  • Keep doubles simple: fixed return values, recorded calls.
  • Use CL_ABAP_TESTDOUBLE for global interfaces with many methods.
  • Run ABAP Unit in ATC before transport release.

Interview angle

Questions: "What is dependency injection?", "How do you test a class that reads the database?", "Stub vs mock?", "What is CL_ABAP_TESTDOUBLE?"
Strong answer: DI = a class receives its dependencies (typically via constructor, interface-typed) instead of creating them; tests pass stubs (fixed data) or spies/mocks (recorded calls); optional parameters keep callers compatible; factory injectors for deep chains; CL_ABAP_TESTDOUBLE generates doubles for global interfaces with configure_call/returning.

Practice this lesson

Quick check

1. Why is constructor injection of an interface-typed DAO useful for ABAP Unit?

2. A constructor parameter io_dao TYPE REF TO lif_order_dao is OPTIONAL and the constructor creates the real DAO if it is not bound. What is the benefit?

3. What distinguishes a spy (mock) from a simple stub?

Interview questions

1. A delivery service reads customer-risk data directly from the database inside its methods, so it cannot be unit tested. How do you make it testable?

Answer it in your own words first, then compare.

2. How do you apply Dependency Injection in ABAP?

Answer it in your own words first, then compare.

Practice more with Interview AI →

Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.

More lessons in this topic

Interview questions on this topic

Related SAP topics