Lesson 3 of 5
Designing for testability
10 min
Listen to this lesson
Kenji's first test passed, then failed the next morning. Someone had changed the agreement for Great Lakes Fleet Services in CMD, and the deposit class reads the agreement straight from the custom table ZCMC_CORE_AGR. The test was really testing the contents of the development database. That is the most common reason ABAP Unit adoption stalls: the code under test reaches out to things the test cannot control. The fix is design, not more test code.
Find the dependencies
Start by listing what the method touches besides its own parameters and attributes. In ABAP the usual suspects are database reads and writes, CDS views, function modules and BAPIs, other classes created with NEW inside the method, authority checks, the system date and time, and number ranges. Each is a depended-on component, and each needs a way to be replaced in a test.
Depend on interfaces, inject the implementation
The pattern SAP's Clean ABAP guide recommends is dependency inversion: the class depends on an interface, not on a concrete class or table, and the concrete implementation is handed to it from outside. Production code passes the real reader; the test passes a double. Kenji extracts an interface ZIF_CMC_AGREEMENT_READER, moves the database read into a class that implements it, and gives ZCL_CMC_CORE_DEPOSIT a reference to the interface instead of a SELECT.
| Injection technique | How the double gets in | When to use it |
|---|---|---|
| Constructor injection | Passed as an (optional) parameter of the constructor; production code passes nothing and gets the default | The default choice for required dependencies |
| Setter injection | Passed through a setter method after creation | Optional dependencies or late configuration |
| Parameter injection | Passed as an optional importing parameter of the tested method | A single method needs a different collaborator |
| Back-door injection | The test class is a LOCAL FRIEND and sets a private attribute directly | Legacy classes you cannot change much yet |
| Factory with injector | A factory returns implementations; a test-only injector class sets doubles in the factory | Many consumers create the same dependency |
Injection techniques named in SAP's ABAP cheat sheets and ABAP Unit sessions.
Constructor injection with a default is the easiest to read: METHODS constructor IMPORTING reader TYPE REF TO zif_cmc_agreement_reader OPTIONAL. In the constructor, if no reader was passed, the class creates the production reader. Production callers do not change. If the constructor is private or the class has many consumers, a factory plus an injector class (declared FOR TESTING) keeps the test hook out of production code paths.
Test seams: a temporary workaround
Sometimes you must test legacy code before you can restructure it. ABAP offers test seams: production code wraps a block in TEST-SEAM name ... END-TEST-SEAM, and a test class replaces it with TEST-INJECTION name ... END-TEST-INJECTION. Without an injection, the original code runs. Seams cannot be nested, and injections must sit in test classes in the test include, so in practice they work in class pools and function groups. Clean ABAP recommends seams only as a temporary workaround because they are invasive and tangle production and test code. Check SAP's list of language elements per ABAP language version before relying on them in code written for ABAP Cloud.
- Can you change the class to depend on an interface?Yes → constructor injection with a default · No → next question
- Do many callers create the dependency?Yes → factory plus test injector · No → next question
- Is the dependency a table, CDS view or function module?Yes → a framework double (next lesson) may need no design change · No → next question
- Is this legacy code you must pin down before refactoring?Yes → test seam or LOCAL FRIENDS back door, with a ticket to remove it · No → refactor first
Other things that make tests flaky
- System date and time: read them through an injectable clock or pass them in, so a test about expiry dates does not break at month end.
- Number ranges: let the test supply the number or use a double for the number provider.
- Authority checks: isolate them, or use SAP's authority check test helper to run the test with a defined set of authorizations.
- Shared static state: reset buffers and singletons in setup, or avoid them in the code under test.
What would you do?
Kenji's deposit class has a private method that reads ZCMC_CORE_AGR with a SELECT, called from the public method calculate. A teammate proposes making the private method public so the test can call it directly. What should Kenji do?
- Tests stall when code reaches out to dependencies the test cannot control.
- Depend on interfaces and inject implementations; constructor injection with a default is the usual choice.
- Test public behavior; a need to test private methods hints at a missing class.
- Test seams and LOCAL FRIENDS back doors are temporary tools for legacy code.
Check your understanding · not graded
Lesson check
1. What made Kenji's first deposit test fail the next morning?
2. Which injection technique passes the test double through an optional constructor parameter?
3. How does SAP's Clean ABAP guide position test seams?
4. A test checks an expiry rule and fails only at month end. What is the most likely design issue?