Modularization
ABAP Developmentadvanced

Modularization Part 10: Modularization for Unit Testing and Regression Safety

Key takeaway

Make modular code testable: separate logic from database access through an interface, replace the DAO with a test double, and write ABAP Unit tests (FOR TESTING, setup, cl abap unit assert) that protect you from regressions.

Explanation

📌 KEY POINTS
✅ Code is testable when business logic does not directly SELECT or UPDATE — it calls a DAO interface.

✅ In a test, you pass a fake DAO (test double) that returns fixed data; no database needed.

✅ ABAP Unit tests are local classes FOR TESTING, run with Ctrl+Shift+F10 in SE80/ADT or from ATC.

⚠️ A FORM that reads the database, calculates and writes a list in one block cannot be unit-tested.

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

Part 9 built ZCL_FLIGHT_BOOKING_SERVICE with the interface ZIF_FLIGHT_DAO. Now we see why that design pays off: it can be tested automatically.

━━━━━━━━━━━━━━━━━━━━
▶ 1. WHY UNIT TESTS

Every change to a report or FM risks breaking existing behaviour (regression). Manual retesting is slow and incomplete. Unit tests run in seconds, repeatedly, before every transport.

━━━━━━━━━━━━━━━━━━━━
▶ 2. WHAT MAKES CODE TESTABLE

  • Small units with clear inputs and outputs (methods, FMs with typed parameters).
  • No hidden dependencies: database, time (sy-datum), user (sy-uname) passed in or behind interfaces.
  • Business rules separated from UI output (WRITE/ALV) and from database access.

Classic FORM routines using global data are hard to test; move logic into a class method first.

━━━━━━━━━━━━━━━━━━━━
▶ 3. THE DAO INTERFACE AND A TEST DOUBLE

The service receives a ZIF_FLIGHT_DAO in its constructor. Production passes ZCL_FLIGHT_DAO (real SELECTs). The test passes LCL_FAKE_DAO, which returns a flight with, for example, 10 free seats — exactly the situation we want to test.

━━━━━━━━━━━━━━━━━━━━
▶ 4. ANATOMY OF AN ABAP UNIT TEST CLASS

CLASS ltc_booking DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.

  • FOR TESTING — only runs in test mode, never in production.
  • RISK LEVEL HARMLESS — does not change persistent data.
  • setup — runs before each test method: create fake DAO and the class under test (cut).
  • Test methods — each checks one behaviour, using cl_abap_unit_assert=>assert_equals / assert_true / fail.

━━━━━━━━━━━━━━━━━━━━
▶ 5. WHAT TO TEST

  • Happy path: booking allowed when seats are free.
  • Edge: exactly zero free seats → exception.
  • Invalid input: unknown flight → exception.
  • Calculations: price with discount rounding.

━━━━━━━━━━━━━━━━━━━━
▶ 6. TESTING LEGACY CODE

  • Extract the calculation from a FORM into a method; call the method from the FORM (no behaviour change).
  • Write tests for the new method.
  • Test seams (TEST-SEAM/TEST-INJECTION) can replace a SELECT inside legacy code during tests when refactoring is not possible.

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

  1. Add the local test class to ZCL_FLIGHT_BOOKING_SERVICE (Local Test Classes include).
  2. Run the unit tests (Ctrl+Shift+F10).
  3. Break the seat check on purpose and watch a test fail.
  4. Include ABAP Unit in your ATC check variant so tests run before release.

💡 Modern syntax (7.40+): NEW for object creation and inline DATA(…) in test methods; cl_abap_testdouble for generated doubles.

✅ CHECKPOINT: you can isolate a service from the database with an interface and prove its rules with ABAP Unit.

Code example

ABAP Code
* ===== Local test class of ZCL_FLIGHT_BOOKING_SERVICE (Local Test Classes include) ===== * ----- Step 1: a fake DAO that returns fixed data -----CLASS lcl_fake_dao DEFINITION FOR TESTING.  PUBLIC SECTION.    INTERFACES zif_flight_dao.    DATA mv_free_seats TYPE i.          " test controls the situation    DATA mv_exists     TYPE abap_bool VALUE abap_true.ENDCLASS. CLASS lcl_fake_dao IMPLEMENTATION.  METHOD zif_flight_dao~read_flight.    IF mv_exists = abap_false.      RAISE EXCEPTION TYPE zcx_flight_error        EXPORTING textid = zcx_flight_error=>flight_not_found.    ENDIF.    rs_flight-carrid   = iv_carrid.    rs_flight-connid   = iv_connid.    rs_flight-fldate   = iv_fldate.    rs_flight-seatsmax = 100.    rs_flight-seatsocc = 100 - mv_free_seats.  ENDMETHOD.  METHOD zif_flight_dao~insert_booking.    " no database access in tests - just accept  ENDMETHOD.ENDCLASS. * ----- Step 2: the test class -----CLASS ltc_booking DEFINITION FOR TESTING      RISK LEVEL HARMLESS DURATION SHORT.  PRIVATE SECTION.    DATA: mo_dao TYPE REF TO lcl_fake_dao,          mo_cut TYPE REF TO zcl_flight_booking_service.   " class under test    METHODS: setup,             booking_ok_when_seats_free   FOR TESTING,             error_when_flight_full       FOR TESTING,             error_when_flight_unknown    FOR TESTING.ENDCLASS. CLASS ltc_booking IMPLEMENTATION.  METHOD setup.                                 " runs before every test method    CREATE OBJECT mo_dao.    CREATE OBJECT mo_cut EXPORTING io_dao = mo_dao.  ENDMETHOD.   METHOD booking_ok_when_seats_free.    DATA lv_ok TYPE abap_bool.    mo_dao->mv_free_seats = 10.    TRY.        mo_cut->book( iv_carrid = 'LH' iv_connid = '0400'                      iv_fldate = '20261201' iv_customid = '00000001' ).        lv_ok = abap_true.      CATCH zcx_flight_error.        lv_ok = abap_false.    ENDTRY.    cl_abap_unit_assert=>assert_equals( act = lv_ok exp = abap_true                                        msg = 'Booking should succeed with free seats' ).  ENDMETHOD.   METHOD error_when_flight_full.    mo_dao->mv_free_seats = 0.    TRY.        mo_cut->book( iv_carrid = 'LH' iv_connid = '0400'                      iv_fldate = '20261201' iv_customid = '00000001' ).        cl_abap_unit_assert=>fail( msg = 'Full flight must raise an exception' ).      CATCH zcx_flight_error.        " expected    ENDTRY.  ENDMETHOD.   METHOD error_when_flight_unknown.    mo_dao->mv_exists = abap_false.    TRY.        mo_cut->book( iv_carrid = 'XX' iv_connid = '0001'                      iv_fldate = '20261201' iv_customid = '00000001' ).        cl_abap_unit_assert=>fail( msg = 'Unknown flight must raise an exception' ).      CATCH zcx_flight_error.        " expected    ENDTRY.  ENDMETHOD.ENDCLASS. * ===== Legacy code without a DAO: test seam around the SELECT =====* In production code:*   TEST-SEAM read_flight.*     SELECT SINGLE * FROM sflight INTO ls_flight WHERE ...*   END-TEST-SEAM.* In the test method:*   TEST-INJECTION read_flight.*     ls_flight-seatsmax = 100. ls_flight-seatsocc = 100.*   END-TEST-INJECTION.

Real project scenario

A team changed the seat availability rule in a booking FM during a support fix and accidentally inverted a comparison. Manual testing only covered the happy path, so overbookings went live and had to be corrected manually for two days.
They then refactored the rule into a service class with a DAO interface and added ABAP Unit tests for free, full and unknown flights. The ATC check variant was extended to run unit tests before transport release; a similar mistake in a later change was caught in seconds.

Common mistakes

  • Business logic mixed with SELECTs and WRITE output — nothing to test in isolation.
  • Tests that depend on real database data (flaky, change with the system).
  • One huge test method checking everything.
  • Missing negative tests (full flight, invalid input).
  • Tests never run because they are not part of the ATC variant or release routine.

Best practices

  • Put database access behind an interface and inject it.
  • Create test doubles that return controlled data.
  • One behaviour per test method, with clear messages.
  • Use setup for fresh objects before each test.
  • Run ABAP Unit in ATC before transport release; use test seams only for legacy code.

Interview angle

Questions: "How do you unit test ABAP code that reads the database?", "What is a test double?", "What are RISK LEVEL and DURATION?"
Strong answer: separate logic from DB access via a DAO interface injected into the class; in tests pass a fake implementation with controlled data; write local FOR TESTING classes with setup and assert methods; RISK LEVEL/DURATION classify tests; use test seams for legacy code; run tests in ATC before release.

Practice this lesson

Quick check

1. What does the setup method in an ABAP Unit test class do?

2. Which ABAP Unit class definition is correct for a test that does not change persistent data?

3. Which construct enables ABAP Unit tests most easily?

Interview questions

1. A pricing rule was copied into five reports. A defect was fixed in one of them, and the others still produce wrong prices. How do you prevent this happening again?

Answer it in your own words first, then compare.

2. What is the cost of many small function-module calls in a loop?

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