abapgit-agent 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abap/guidelines/00_index.md +1 -0
- package/abap/guidelines/01_sql.md +16 -0
- package/abap/guidelines/02_exceptions.md +92 -0
- package/abap/guidelines/03_testing.md +36 -0
- package/abap/guidelines/04_cds.md +16 -0
- package/abap/guidelines/05_classes.md +119 -0
- package/abap/guidelines/06_objects.md +46 -86
- package/abap/guidelines/07_json.md +2 -0
- package/abap/guidelines/08_abapgit.md +40 -0
- package/abap/guidelines/09_unit_testable_code.md +589 -0
- package/bin/abapgit-agent +176 -32
- package/package.json +5 -1
- package/src/abap-client.js +46 -0
- package/src/agent.js +48 -0
- package/src/config.js +1 -1
- package/src/ref-search.js +92 -44
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# ABAP Unit Testable Code Guidelines
|
|
2
|
+
|
|
3
|
+
This document provides guidelines for creating ABAP OO classes/interfaces that can be easily unit tested with test doubles. These guidelines help AI coding tools understand how to design classes that are testable without requiring real dependencies.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When ABAP classes are not designed for testability, unit tests cannot mock dependencies. This leads to:
|
|
8
|
+
|
|
9
|
+
- Tests that depend on real external systems (databases, APIs, file systems)
|
|
10
|
+
- Tests that fail in different environments
|
|
11
|
+
- Tests that are slow and unreliable
|
|
12
|
+
- Impossible to test error conditions
|
|
13
|
+
|
|
14
|
+
**Example of untestable code:**
|
|
15
|
+
|
|
16
|
+
```abap
|
|
17
|
+
" BAD - Hardcoded dependency, cannot be replaced in tests
|
|
18
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
19
|
+
METHOD execute.
|
|
20
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
21
|
+
ls_result = lo_agent->pull( ... ). " Calls real system
|
|
22
|
+
ENDMETHOD.
|
|
23
|
+
ENDCLASS.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The unit test will instantiate the REAL `zcl_abgagt_agent` which tries to connect to abapGit and a real git repository, causing test failures.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Principles
|
|
31
|
+
|
|
32
|
+
### 1. Dependency Inversion (Dependency Injection)
|
|
33
|
+
|
|
34
|
+
**Pass dependencies through constructor instead of creating them internally.**
|
|
35
|
+
|
|
36
|
+
```abap
|
|
37
|
+
" GOOD - Dependency injected via constructor
|
|
38
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
39
|
+
PUBLIC SECTION.
|
|
40
|
+
INTERFACES zif_abgagt_command.
|
|
41
|
+
|
|
42
|
+
" Constructor injection
|
|
43
|
+
METHODS constructor
|
|
44
|
+
IMPORTING
|
|
45
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
46
|
+
|
|
47
|
+
PRIVATE SECTION.
|
|
48
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent.
|
|
49
|
+
|
|
50
|
+
ENDCLASS.
|
|
51
|
+
|
|
52
|
+
CLASS zcl_abgagt_command_pull IMPLEMENTATION.
|
|
53
|
+
|
|
54
|
+
METHOD constructor.
|
|
55
|
+
super->constructor( ).
|
|
56
|
+
mo_agent = io_agent.
|
|
57
|
+
ENDMETHOD.
|
|
58
|
+
|
|
59
|
+
METHOD execute.
|
|
60
|
+
" Use injected dependency
|
|
61
|
+
ls_result = mo_agent->pull( ... ).
|
|
62
|
+
ENDMETHOD.
|
|
63
|
+
|
|
64
|
+
ENDCLASS.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**In production code:**
|
|
68
|
+
```abap
|
|
69
|
+
DATA(lo_command) = NEW zcl_abgagt_command_pull(
|
|
70
|
+
io_agent = NEW zcl_abgagt_agent( ) ).
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**In test code:**
|
|
74
|
+
```abap
|
|
75
|
+
" Create test double
|
|
76
|
+
CLASS ltd_mock_agent DEFINITION FOR TESTING.
|
|
77
|
+
PUBLIC SECTION.
|
|
78
|
+
INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
|
|
79
|
+
ENDCLASS.
|
|
80
|
+
|
|
81
|
+
CLASS ltd_mock_agent IMPLEMENTATION.
|
|
82
|
+
METHOD zif_abgagt_agent~pull.
|
|
83
|
+
" Return test data instead of calling real system
|
|
84
|
+
rs_result-success = abap_true.
|
|
85
|
+
rs_result-message = 'Test success'.
|
|
86
|
+
ENDMETHOD.
|
|
87
|
+
ENDCLASS.
|
|
88
|
+
|
|
89
|
+
" Test uses test double
|
|
90
|
+
CLASS ltcl_test DEFINITION FOR TESTING.
|
|
91
|
+
METHOD test_execute.
|
|
92
|
+
DATA(lo_mock) = NEW ltd_mock_agent( ).
|
|
93
|
+
DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
|
|
94
|
+
|
|
95
|
+
DATA(lv_result) = lo_cut->execute( ... ).
|
|
96
|
+
|
|
97
|
+
" Assert expected results
|
|
98
|
+
ENDMETHOD.
|
|
99
|
+
ENDCLASS.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Always Use Interfaces for Dependencies
|
|
103
|
+
|
|
104
|
+
**Never depend on concrete classes - depend on interfaces.**
|
|
105
|
+
|
|
106
|
+
```abap
|
|
107
|
+
" GOOD - Depend on interface
|
|
108
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent. " Interface!
|
|
109
|
+
|
|
110
|
+
" BAD - Depends on concrete class
|
|
111
|
+
DATA mo_agent TYPE REF TO zcl_abgagt_agent. " Concrete class!
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This allows you to replace the implementation with test doubles.
|
|
115
|
+
|
|
116
|
+
### Define Types in Interface
|
|
117
|
+
|
|
118
|
+
Define types needed by the interface directly in the interface to keep it self-contained:
|
|
119
|
+
|
|
120
|
+
```abap
|
|
121
|
+
INTERFACE zif_my_handler.
|
|
122
|
+
" Define types needed by the interface
|
|
123
|
+
TYPES: BEGIN OF ty_response,
|
|
124
|
+
success TYPE abap_bool,
|
|
125
|
+
message TYPE string,
|
|
126
|
+
END OF ty_response.
|
|
127
|
+
|
|
128
|
+
" Use the type in method signatures
|
|
129
|
+
METHODS process
|
|
130
|
+
IMPORTING iv_data TYPE string
|
|
131
|
+
RETURNING VALUE(rs_response) TYPE ty_response.
|
|
132
|
+
ENDINTERFACE.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This makes it easier for test doubles to implement the interface without needing separate type definitions.
|
|
136
|
+
|
|
137
|
+
### 3. Make Dependencies Injectable via Constructor
|
|
138
|
+
|
|
139
|
+
**Use constructor injection, not setter injection.**
|
|
140
|
+
|
|
141
|
+
```abap
|
|
142
|
+
" GOOD - Constructor injection (required dependency)
|
|
143
|
+
METHODS constructor
|
|
144
|
+
IMPORTING
|
|
145
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
146
|
+
|
|
147
|
+
" BAD - Setter injection (optional, can be forgotten)
|
|
148
|
+
METHODS set_agent
|
|
149
|
+
IMPORTING
|
|
150
|
+
io_agent TYPE REF TO zif_abgagt_agent.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Constructor injection:
|
|
154
|
+
- Makes dependency explicit
|
|
155
|
+
- Ensures object is always in valid state
|
|
156
|
+
- Cannot forget to inject
|
|
157
|
+
|
|
158
|
+
### 4. Avoid Static Calls
|
|
159
|
+
|
|
160
|
+
**Static method calls cannot be mocked/test-doubled.**
|
|
161
|
+
|
|
162
|
+
```abap
|
|
163
|
+
" BAD - Static call cannot be replaced
|
|
164
|
+
DATA(li_repo) = zcl_abapgit_repo_srv=>get_instance( )->get_repo_from_url( ... ).
|
|
165
|
+
|
|
166
|
+
" GOOD - Instance method via injected dependency
|
|
167
|
+
DATA(li_repo) = mo_repo_srv->get_repo_from_url( ... ).
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
If you must call static methods, wrap them in an instance method of an injected class.
|
|
171
|
+
|
|
172
|
+
### 5. Keep Constructor Simple
|
|
173
|
+
|
|
174
|
+
**Constructor should only assign dependencies, not perform complex logic.**
|
|
175
|
+
|
|
176
|
+
```abap
|
|
177
|
+
" GOOD - Simple constructor
|
|
178
|
+
METHOD constructor.
|
|
179
|
+
mo_agent = io_agent.
|
|
180
|
+
mo_logger = io_logger.
|
|
181
|
+
ENDMETHOD.
|
|
182
|
+
|
|
183
|
+
" BAD - Complex logic in constructor
|
|
184
|
+
METHOD constructor.
|
|
185
|
+
mo_agent = io_agent.
|
|
186
|
+
" Don't do this here:
|
|
187
|
+
mo_agent->connect( ). " Network call in constructor!
|
|
188
|
+
DATA(ls_config) = read_config( ). " File I/O in constructor!
|
|
189
|
+
ENDMETHOD.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Injection Techniques
|
|
195
|
+
|
|
196
|
+
### Constructor Injection (Recommended)
|
|
197
|
+
|
|
198
|
+
```abap
|
|
199
|
+
CLASS zcl_my_class DEFINITION PUBLIC.
|
|
200
|
+
PUBLIC SECTION.
|
|
201
|
+
METHODS constructor
|
|
202
|
+
IMPORTING
|
|
203
|
+
io_dependency TYPE REF TO zif_my_interface.
|
|
204
|
+
PRIVATE SECTION.
|
|
205
|
+
DATA mo_dependency TYPE REF TO zif_my_interface.
|
|
206
|
+
ENDCLASS.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Back Door Injection (for existing code)
|
|
210
|
+
|
|
211
|
+
When you cannot modify the constructor, use friendship:
|
|
212
|
+
|
|
213
|
+
```abap
|
|
214
|
+
" In test class
|
|
215
|
+
CLASS zcl_my_class DEFINITION LOCAL FRIENDS ltcl_test.
|
|
216
|
+
|
|
217
|
+
CLASS ltcl_test IMPLEMENTATION.
|
|
218
|
+
METHOD test_with_mock.
|
|
219
|
+
" Directly set private attribute via friendship
|
|
220
|
+
CREATE OBJECT mo_cut.
|
|
221
|
+
mo_cut->mo_dependency = lo_mock. " Access private attribute
|
|
222
|
+
ENDMETHOD.
|
|
223
|
+
ENDCLASS.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Test Seams (last resort)
|
|
227
|
+
|
|
228
|
+
For legacy code that cannot be refactored:
|
|
229
|
+
|
|
230
|
+
```abap
|
|
231
|
+
" In production code
|
|
232
|
+
METHOD get_data.
|
|
233
|
+
TEST-SEAM db_select.
|
|
234
|
+
SELECT * FROM dbtab INTO TABLE @DATA(lt_data).
|
|
235
|
+
END-TEST-SEAM.
|
|
236
|
+
ENDMETHOD.
|
|
237
|
+
|
|
238
|
+
" In test class
|
|
239
|
+
METHOD test_get_data.
|
|
240
|
+
TEST-INJECTION db_select.
|
|
241
|
+
lt_data = VALUE #( ( id = '1' ) ( id = '2' ) ).
|
|
242
|
+
END-TEST-INJECTION.
|
|
243
|
+
|
|
244
|
+
DATA(lt_result) = mo_cut->get_data( ).
|
|
245
|
+
ENDMETHOD.
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Test Double Patterns
|
|
251
|
+
|
|
252
|
+
### Manual Test Double (Local Class)
|
|
253
|
+
|
|
254
|
+
```abap
|
|
255
|
+
" Create test double class
|
|
256
|
+
CLASS ltd_mock_reader DEFINITION FOR TESTING.
|
|
257
|
+
PUBLIC SECTION.
|
|
258
|
+
INTERFACES zif_data_reader PARTIALLY IMPLEMENTED.
|
|
259
|
+
METHODS set_result_data
|
|
260
|
+
IMPORTING it_data TYPE ANY TABLE.
|
|
261
|
+
PRIVATE SECTION.
|
|
262
|
+
DATA mt_data TYPE ANY TABLE.
|
|
263
|
+
ENDCLASS.
|
|
264
|
+
|
|
265
|
+
CLASS ltd_mock_reader IMPLEMENTATION.
|
|
266
|
+
METHOD set_result_data.
|
|
267
|
+
mt_data = it_data.
|
|
268
|
+
ENDMETHOD.
|
|
269
|
+
|
|
270
|
+
METHOD zif_data_reader~read_all.
|
|
271
|
+
rt_data = mt_data.
|
|
272
|
+
ENDMETHOD.
|
|
273
|
+
ENDCLASS.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Using ABAP Test Double Framework
|
|
277
|
+
|
|
278
|
+
```abap
|
|
279
|
+
" Step 1: Declare with correct interface type, then assign
|
|
280
|
+
DATA lo_mock TYPE REF TO zif_my_interface.
|
|
281
|
+
lo_mock ?= cl_abap_testdouble=>create( 'ZIF_MY_INTERFACE' ).
|
|
282
|
+
|
|
283
|
+
" Step 2: Configure return value - use returning() not IMPORTING
|
|
284
|
+
cl_abap_testdouble=>configure_call( lo_mock )->returning( lo_mock_result ).
|
|
285
|
+
|
|
286
|
+
" Step 3: Call method to register configuration (MUST use same params in test)
|
|
287
|
+
lo_mock->my_method(
|
|
288
|
+
EXPORTING
|
|
289
|
+
iv_param1 = 'value1'
|
|
290
|
+
iv_param2 = 'value2' ).
|
|
291
|
+
|
|
292
|
+
" Step 4: In test, call with SAME parameters as registered above
|
|
293
|
+
DATA(ls_result) = lo_mock->my_method(
|
|
294
|
+
EXPORTING
|
|
295
|
+
iv_param1 = 'value1'
|
|
296
|
+
iv_param2 = 'value2' ).
|
|
297
|
+
|
|
298
|
+
" To raise exception:
|
|
299
|
+
DATA(lx_error) = NEW zcx_my_exception( ).
|
|
300
|
+
cl_abap_testdouble=>configure_call( lo_mock )->raise_exception( lx_error ).
|
|
301
|
+
lo_mock->my_method( ... ).
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Important Notes:**
|
|
305
|
+
- Parameters in configure_call registration MUST match parameters in test execution
|
|
306
|
+
- Always declare variable with interface type first: `DATA lo_mock TYPE REF TO zif_xxx`
|
|
307
|
+
- Use `returning(value = ...)` not `IMPORTING`
|
|
308
|
+
- Call method after configure_call to register the configuration
|
|
309
|
+
|
|
310
|
+
### Mocking EXPORT Parameters
|
|
311
|
+
|
|
312
|
+
Some methods use EXPORT parameters instead of returning values. Use `set_parameter`:
|
|
313
|
+
|
|
314
|
+
```abap
|
|
315
|
+
" Mock EXPORT parameter EI_REPO
|
|
316
|
+
cl_abap_testdouble=>configure_call( lo_repo_srv )->set_parameter(
|
|
317
|
+
EXPORTING
|
|
318
|
+
name = 'EI_REPO'
|
|
319
|
+
value = lo_repo_double ).
|
|
320
|
+
|
|
321
|
+
" Register the method call
|
|
322
|
+
lo_repo_srv->get_repo_from_url(
|
|
323
|
+
EXPORTING iv_url = 'https://github.com/test/repo.git' ).
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Mocking Inherited Methods
|
|
327
|
+
|
|
328
|
+
When an interface extends another interface, use the parent interface prefix:
|
|
329
|
+
|
|
330
|
+
```abap
|
|
331
|
+
" zif_abapgit_repo_online extends zif_abapgit_repo
|
|
332
|
+
" Call inherited method with prefix
|
|
333
|
+
lo_repo->zif_abapgit_repo~get_package( ).
|
|
334
|
+
lo_repo->zif_abapgit_repo~refresh( ).
|
|
335
|
+
lo_repo->zif_abapgit_repo~get_files_local( ).
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Mocking Methods with No Parameters
|
|
339
|
+
|
|
340
|
+
When source code calls a method with no parameters:
|
|
341
|
+
|
|
342
|
+
```abap
|
|
343
|
+
" Configure returning (no method name)
|
|
344
|
+
cl_abap_testdouble=>configure_call( lo_mock )->returning( lt_data ).
|
|
345
|
+
|
|
346
|
+
" Register with no parameters (matches source code)
|
|
347
|
+
lo_mock->get_files_local( ).
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Common Mistakes
|
|
351
|
+
|
|
352
|
+
| Mistake | Correction |
|
|
353
|
+
|---------|------------|
|
|
354
|
+
| Using `IMPORTING` in configure_call | Use `returning()` or `set_parameter()` |
|
|
355
|
+
| Calling method inside configure_call | Call method separately after configure_call |
|
|
356
|
+
| Wrong parameter count | Match exactly what source code calls |
|
|
357
|
+
| Forgot to mock a method | Mock ALL methods the code under test calls |
|
|
358
|
+
| Interface prefix not used | Use `zif_parent~method()` for inherited methods |
|
|
359
|
+
| Didn't check source code first | ALWAYS read source code to see how method is called |
|
|
360
|
+
| Cannot add RAISING to interface method | Use TRY..CATCH to handle exceptions in implementation |
|
|
361
|
+
|
|
362
|
+
### Handling Exceptions in Interface Implementation
|
|
363
|
+
|
|
364
|
+
When implementing an interface method that calls other methods raising exceptions:
|
|
365
|
+
|
|
366
|
+
- **DO NOT** add RAISING to the interface method - you cannot change the interface
|
|
367
|
+
- **USE** TRY..CATCH to catch and handle exceptions within the implementation
|
|
368
|
+
|
|
369
|
+
```abap
|
|
370
|
+
" Interface method does NOT declare RAISING
|
|
371
|
+
METHOD zif_abgagt_command~execute.
|
|
372
|
+
|
|
373
|
+
" Method being called can raise exception
|
|
374
|
+
TRY.
|
|
375
|
+
get_user( )->set_repo_git_user_name( ... ).
|
|
376
|
+
CATCH zcx_abapgit_exception INTO DATA(lx_error).
|
|
377
|
+
rv_result = '{"error":"' && lx_error->get_text( ) && '"}'.
|
|
378
|
+
RETURN.
|
|
379
|
+
ENDTRY.
|
|
380
|
+
|
|
381
|
+
ENDMETHOD.
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Important: Read Source Code First
|
|
385
|
+
|
|
386
|
+
**ALWAYS check the source code to see HOW a method is called before writing tests:**
|
|
387
|
+
|
|
388
|
+
1. Check what parameters are passed (none, some, or all)
|
|
389
|
+
2. Check if optional parameters are used
|
|
390
|
+
3. Check if parameters have default values
|
|
391
|
+
4. Check for type casts (e.g., `li_repo_online ?= li_repo`)
|
|
392
|
+
|
|
393
|
+
```abap
|
|
394
|
+
" Source code line 122:
|
|
395
|
+
lt_files = li_repo->get_files_local( ).
|
|
396
|
+
|
|
397
|
+
" Test MUST match - no parameters!
|
|
398
|
+
cl_abap_testdouble=>configure_call( lo_repo )->returning( lt_empty_files ).
|
|
399
|
+
lo_repo->get_files_local( ). " No parameters!
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
If the source code calls `get_files_local( )` with no parameters, your test registration must also have no parameters. Even if the method signature has optional parameters, if the source doesn't pass them, your mock registration must not pass them either.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Guidelines for AI Coding Tools
|
|
407
|
+
|
|
408
|
+
When creating new ABAP classes, ALWAYS follow these rules:
|
|
409
|
+
|
|
410
|
+
### DO:
|
|
411
|
+
|
|
412
|
+
1. **Use interfaces for ALL external dependencies**
|
|
413
|
+
- Database access → interface
|
|
414
|
+
- External API calls → interface
|
|
415
|
+
- File I/O → interface
|
|
416
|
+
- Other services → interface
|
|
417
|
+
|
|
418
|
+
2. **Pass dependencies via constructor**
|
|
419
|
+
```abap
|
|
420
|
+
METHODS constructor
|
|
421
|
+
IMPORTING
|
|
422
|
+
io_agent TYPE REF TO zif_abgagt_agent
|
|
423
|
+
io_logger TYPE REF TO zif_logger.
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
3. **Define interfaces for all service classes**
|
|
427
|
+
```abap
|
|
428
|
+
" Instead of using class directly
|
|
429
|
+
DATA mo_agent TYPE REF TO zcl_abgagt_agent. " BAD
|
|
430
|
+
|
|
431
|
+
" Use interface
|
|
432
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent. " GOOD
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
4. **Keep classes FINAL if they don't need mocking**
|
|
436
|
+
- If a class has no dependencies and doesn't need test doubles, make it FINAL
|
|
437
|
+
- If a class needs to be mocked in tests, don't make it FINAL
|
|
438
|
+
|
|
439
|
+
5. **Use dependency injection in command classes**
|
|
440
|
+
```abap
|
|
441
|
+
" Good pattern for command classes
|
|
442
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
443
|
+
PUBLIC SECTION.
|
|
444
|
+
INTERFACES zif_abgagt_command.
|
|
445
|
+
METHODS constructor
|
|
446
|
+
IMPORTING io_agent TYPE REF TO zif_abgagt_agent.
|
|
447
|
+
ENDCLASS.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### DON'T:
|
|
451
|
+
|
|
452
|
+
1. **Never create dependencies inside methods**
|
|
453
|
+
```abap
|
|
454
|
+
" BAD
|
|
455
|
+
METHOD execute.
|
|
456
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
457
|
+
ENDMETHOD.
|
|
458
|
+
|
|
459
|
+
" GOOD
|
|
460
|
+
METHOD execute.
|
|
461
|
+
ls_result = mo_agent->pull( ... ). " Use injected
|
|
462
|
+
ENDMETHOD.
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
2. **Don't use static method calls for testable code**
|
|
466
|
+
```abap
|
|
467
|
+
" BAD
|
|
468
|
+
DATA(lo_srv) = zcl_some_srv=>get_instance( ).
|
|
469
|
+
|
|
470
|
+
" GOOD - inject the service
|
|
471
|
+
DATA(lo_srv) = mo_service_provider.
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
3. **Don't make classes FINAL if they need test doubles**
|
|
475
|
+
- If you need to mock a class in tests, don't declare it FINAL
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Example: Refactoring for Testability
|
|
480
|
+
|
|
481
|
+
### Before (Not Testable)
|
|
482
|
+
|
|
483
|
+
```abap
|
|
484
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
485
|
+
METHOD execute.
|
|
486
|
+
DATA lo_agent TYPE REF TO zcl_abgagt_agent.
|
|
487
|
+
lo_agent = NEW zcl_abgagt_agent( ). " Hardcoded!
|
|
488
|
+
|
|
489
|
+
ls_result = lo_agent->pull(
|
|
490
|
+
iv_url = ls_params-url
|
|
491
|
+
iv_branch = ls_params-branch ).
|
|
492
|
+
ENDMETHOD.
|
|
493
|
+
ENDCLASS.
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### After (Testable)
|
|
497
|
+
|
|
498
|
+
```abap
|
|
499
|
+
" Interface for agent
|
|
500
|
+
INTERFACE zif_abgagt_agent PUBLIC.
|
|
501
|
+
METHODS pull ... RAISING zcx_abapgit_exception.
|
|
502
|
+
ENDINTERFACE.
|
|
503
|
+
|
|
504
|
+
" Command class with constructor injection
|
|
505
|
+
CLASS zcl_abgagt_command_pull DEFINITION PUBLIC.
|
|
506
|
+
PUBLIC SECTION.
|
|
507
|
+
INTERFACES zif_abgagt_command.
|
|
508
|
+
|
|
509
|
+
METHODS constructor
|
|
510
|
+
IMPORTING
|
|
511
|
+
io_agent TYPE REF TO zif_abgagt_agent OPTIONAL. " Optional for backward compat
|
|
512
|
+
|
|
513
|
+
PRIVATE SECTION.
|
|
514
|
+
DATA mo_agent TYPE REF TO zif_abgagt_agent.
|
|
515
|
+
|
|
516
|
+
METHODS get_agent
|
|
517
|
+
RETURNING VALUE(ro_agent) TYPE REF TO zif_abgagt_agent.
|
|
518
|
+
ENDCLASS.
|
|
519
|
+
|
|
520
|
+
CLASS zcl_abgagt_command_pull IMPLEMENTATION.
|
|
521
|
+
|
|
522
|
+
METHOD constructor.
|
|
523
|
+
mo_agent = io_agent.
|
|
524
|
+
ENDMETHOD.
|
|
525
|
+
|
|
526
|
+
METHOD get_agent.
|
|
527
|
+
" Lazy creation if not injected (for production)
|
|
528
|
+
IF mo_agent IS NOT BOUND.
|
|
529
|
+
mo_agent = NEW zcl_abgagt_agent( ).
|
|
530
|
+
ENDIF.
|
|
531
|
+
ro_agent = mo_agent.
|
|
532
|
+
ENDMETHOD.
|
|
533
|
+
|
|
534
|
+
METHOD execute.
|
|
535
|
+
DATA(lo_agent) = get_agent( ).
|
|
536
|
+
ls_result = lo_agent->pull( ... ).
|
|
537
|
+
ENDMETHOD.
|
|
538
|
+
|
|
539
|
+
ENDCLASS.
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Production usage:**
|
|
543
|
+
```abap
|
|
544
|
+
DATA(lo_command) = NEW zcl_abgagt_command_pull(
|
|
545
|
+
io_agent = NEW zcl_abgagt_agent( ) ).
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Test usage:**
|
|
549
|
+
```abap
|
|
550
|
+
CLASS ltd_mock_agent DEFINITION FOR TESTING.
|
|
551
|
+
PUBLIC SECTION.
|
|
552
|
+
INTERFACES zif_abgagt_agent PARTIALLY IMPLEMENTED.
|
|
553
|
+
ENDCLASS.
|
|
554
|
+
|
|
555
|
+
CLASS ltd_mock_agent IMPLEMENTATION.
|
|
556
|
+
METHOD zif_abgagt_agent~pull.
|
|
557
|
+
rs_result-success = abap_true.
|
|
558
|
+
rs_result-message = 'Mocked success'.
|
|
559
|
+
ENDMETHOD.
|
|
560
|
+
ENDCLASS.
|
|
561
|
+
|
|
562
|
+
CLASS ltcl_test DEFINITION FOR TESTING.
|
|
563
|
+
METHOD test_pull_success.
|
|
564
|
+
DATA(lo_mock) = NEW ltd_mock_agent( ).
|
|
565
|
+
DATA(lo_cut) = NEW zcl_abgagt_command_pull( io_agent = lo_mock ).
|
|
566
|
+
|
|
567
|
+
DATA(lv_result) = lo_cut->execute( ... ).
|
|
568
|
+
|
|
569
|
+
" Assert mocked behavior
|
|
570
|
+
ENDMETHOD.
|
|
571
|
+
ENDCLASS.
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Key Takeaways
|
|
577
|
+
|
|
578
|
+
1. **Always use interfaces** for dependencies
|
|
579
|
+
2. **Use constructor injection** to pass dependencies
|
|
580
|
+
3. **Never hardcode `NEW` for dependencies** - pass them in
|
|
581
|
+
4. **Avoid static calls** - use instance methods with injected dependencies
|
|
582
|
+
5. **Keep constructors simple** - only assign dependencies
|
|
583
|
+
|
|
584
|
+
Following these guidelines ensures that:
|
|
585
|
+
- Unit tests can mock all dependencies
|
|
586
|
+
- Tests run fast without external systems
|
|
587
|
+
- Tests are reliable and repeatable
|
|
588
|
+
- Error conditions can be tested easily
|
|
589
|
+
- Code is modular and loosely coupled
|