bc-code-intelligence-mcp 1.5.6 → 1.5.8
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/LICENSE +20 -20
- package/README.md +165 -419
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/layers/embedded-layer.js +29 -29
- package/dist/layers/project-layer.js +33 -33
- package/dist/sdk/bc-code-intel-client.d.ts.map +1 -1
- package/dist/sdk/bc-code-intel-client.js +1 -1
- package/dist/sdk/bc-code-intel-client.js.map +1 -1
- package/dist/services/knowledge-service.d.ts +1 -1
- package/dist/services/knowledge-service.d.ts.map +1 -1
- package/dist/services/knowledge-service.js +71 -3
- package/dist/services/knowledge-service.js.map +1 -1
- package/dist/services/methodology-service.js +14 -14
- package/dist/services/multi-content-layer-service.d.ts +15 -0
- package/dist/services/multi-content-layer-service.d.ts.map +1 -1
- package/dist/services/multi-content-layer-service.js +62 -0
- package/dist/services/multi-content-layer-service.js.map +1 -1
- package/dist/streamlined-handlers.d.ts +0 -7
- package/dist/streamlined-handlers.d.ts.map +1 -1
- package/dist/streamlined-handlers.js +80 -60
- package/dist/streamlined-handlers.js.map +1 -1
- package/dist/tools/core-tools.d.ts.map +1 -1
- package/dist/tools/core-tools.js +4 -0
- package/dist/tools/core-tools.js.map +1 -1
- package/dist/tools/onboarding-tools.d.ts +8 -0
- package/dist/tools/onboarding-tools.d.ts.map +1 -1
- package/dist/tools/onboarding-tools.js +111 -1
- package/dist/tools/onboarding-tools.js.map +1 -1
- package/dist/types/bc-knowledge.d.ts +4 -4
- package/embedded-knowledge/.github/ISSUE_TEMPLATE/bug-report.md +23 -23
- package/embedded-knowledge/.github/ISSUE_TEMPLATE/content-improvement.md +23 -23
- package/embedded-knowledge/.github/ISSUE_TEMPLATE/knowledge-request.md +29 -29
- package/embedded-knowledge/AGENTS.md +177 -177
- package/embedded-knowledge/CONTRIBUTING.md +57 -57
- package/embedded-knowledge/LICENSE +20 -20
- package/embedded-knowledge/README.md +31 -31
- package/embedded-knowledge/domains/alex-architect/samples/testability-design-patterns.md +223 -0
- package/embedded-knowledge/domains/alex-architect/testability-design-patterns.md +77 -0
- package/embedded-knowledge/domains/casey-copilot/long-running-session-instructions.md +263 -0
- package/embedded-knowledge/domains/casey-copilot/samples/long-running-session-instructions.md +323 -0
- package/embedded-knowledge/domains/eva-errors/codeunit-run-pattern.md +159 -0
- package/embedded-knowledge/domains/eva-errors/samples/codeunit-run-pattern.md +239 -0
- package/embedded-knowledge/domains/eva-errors/samples/try-function-usage.md +195 -0
- package/embedded-knowledge/domains/eva-errors/try-function-usage.md +129 -0
- package/embedded-knowledge/domains/morgan-market/partner-readiness-analysis.md +201 -0
- package/embedded-knowledge/domains/morgan-market/samples/partner-readiness-checklist.md +288 -0
- package/embedded-knowledge/domains/quinn-tester/isolation-testing-patterns.md +82 -0
- package/embedded-knowledge/domains/quinn-tester/samples/isolation-testing-patterns.md +424 -0
- package/embedded-knowledge/domains/roger-reviewer/samples/testability-code-smells.md +256 -0
- package/embedded-knowledge/domains/roger-reviewer/testability-code-smells.md +67 -0
- package/embedded-knowledge/domains/shared/al-file-naming-conventions.md +145 -145
- package/embedded-knowledge/methodologies/index.json +80 -80
- package/embedded-knowledge/methodologies/phases/analysis-full.md +207 -207
- package/embedded-knowledge/methodologies/phases/analysis-quick.md +43 -43
- package/embedded-knowledge/methodologies/phases/analysis.md +181 -181
- package/embedded-knowledge/methodologies/phases/execution-validation-full.md +173 -173
- package/embedded-knowledge/methodologies/phases/execution-validation-quick.md +30 -30
- package/embedded-knowledge/methodologies/phases/execution-validation.md +173 -173
- package/embedded-knowledge/methodologies/phases/performance-full.md +210 -210
- package/embedded-knowledge/methodologies/phases/performance-quick.md +31 -31
- package/embedded-knowledge/methodologies/phases/performance.md +210 -210
- package/embedded-knowledge/methodologies/phases/verification-full.md +161 -161
- package/embedded-knowledge/methodologies/phases/verification-quick.md +47 -47
- package/embedded-knowledge/methodologies/phases/verification.md +145 -145
- package/embedded-knowledge/methodologies/workflow-enforcement.md +141 -141
- package/embedded-knowledge/methodologies/workflows/code-review-workflow.md +98 -98
- package/package.json +81 -81
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Isolation Testing Pattern Samples
|
|
2
|
+
|
|
3
|
+
## Complete Test Double Library
|
|
4
|
+
|
|
5
|
+
### Dummy - Does Nothing
|
|
6
|
+
```al
|
|
7
|
+
codeunit 80001 "Dummy Currency Converter" implements ICurrencyConverter
|
|
8
|
+
{
|
|
9
|
+
procedure Convert(AtDate: Date; FromCurrency: Code[10]; ToCurrency: Code[10]; Amount: Decimal): Decimal
|
|
10
|
+
begin
|
|
11
|
+
// Does nothing - just satisfies interface requirement
|
|
12
|
+
// Use when converter won't be called in your test path
|
|
13
|
+
end;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
codeunit 80002 "Dummy Logger" implements ILogger
|
|
17
|
+
{
|
|
18
|
+
procedure Log(FromCurrency: Code[10]; ToCurrency: Code[10]; FromAmount: Decimal; ToAmount: Decimal; UserID: Text[50])
|
|
19
|
+
begin
|
|
20
|
+
// Does nothing
|
|
21
|
+
end;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
codeunit 80003 "Dummy Permission Checker" implements IPermissionChecker
|
|
25
|
+
{
|
|
26
|
+
procedure CanConvert(FromCurrency: Code[10]; ToCurrency: Code[10]; UserID: Text[50]): Boolean
|
|
27
|
+
begin
|
|
28
|
+
// Default: allow everything
|
|
29
|
+
exit(true);
|
|
30
|
+
end;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Stub - Returns Predetermined Values
|
|
35
|
+
```al
|
|
36
|
+
codeunit 80010 "Stub Currency Converter" implements ICurrencyConverter
|
|
37
|
+
{
|
|
38
|
+
var
|
|
39
|
+
ReturnValue: Decimal;
|
|
40
|
+
|
|
41
|
+
procedure Convert(AtDate: Date; FromCurrency: Code[10]; ToCurrency: Code[10]; Amount: Decimal): Decimal
|
|
42
|
+
begin
|
|
43
|
+
exit(ReturnValue);
|
|
44
|
+
end;
|
|
45
|
+
|
|
46
|
+
procedure SetReturnValue(Value: Decimal)
|
|
47
|
+
begin
|
|
48
|
+
ReturnValue := Value;
|
|
49
|
+
end;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
codeunit 80011 "Stub Permission Checker" implements IPermissionChecker
|
|
53
|
+
{
|
|
54
|
+
var
|
|
55
|
+
AllowConversion: Boolean;
|
|
56
|
+
|
|
57
|
+
procedure CanConvert(FromCurrency: Code[10]; ToCurrency: Code[10]; UserID: Text[50]): Boolean
|
|
58
|
+
begin
|
|
59
|
+
exit(AllowConversion);
|
|
60
|
+
end;
|
|
61
|
+
|
|
62
|
+
procedure SetAllowed(Allowed: Boolean)
|
|
63
|
+
begin
|
|
64
|
+
AllowConversion := Allowed;
|
|
65
|
+
end;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Spy - Records What Happened
|
|
70
|
+
```al
|
|
71
|
+
codeunit 80020 "Spy Logger" implements ILogger
|
|
72
|
+
{
|
|
73
|
+
var
|
|
74
|
+
WasInvoked: Boolean;
|
|
75
|
+
InvokeCount: Integer;
|
|
76
|
+
LastFromCurrency: Code[10];
|
|
77
|
+
LastToCurrency: Code[10];
|
|
78
|
+
LastFromAmount: Decimal;
|
|
79
|
+
LastToAmount: Decimal;
|
|
80
|
+
LastUserID: Text[50];
|
|
81
|
+
|
|
82
|
+
procedure Log(FromCurrency: Code[10]; ToCurrency: Code[10]; FromAmount: Decimal; ToAmount: Decimal; UserID: Text[50])
|
|
83
|
+
begin
|
|
84
|
+
WasInvoked := true;
|
|
85
|
+
InvokeCount += 1;
|
|
86
|
+
LastFromCurrency := FromCurrency;
|
|
87
|
+
LastToCurrency := ToCurrency;
|
|
88
|
+
LastFromAmount := FromAmount;
|
|
89
|
+
LastToAmount := ToAmount;
|
|
90
|
+
LastUserID := UserID;
|
|
91
|
+
end;
|
|
92
|
+
|
|
93
|
+
// Inspection methods
|
|
94
|
+
procedure IsInvoked(): Boolean
|
|
95
|
+
begin
|
|
96
|
+
exit(WasInvoked);
|
|
97
|
+
end;
|
|
98
|
+
|
|
99
|
+
procedure GetInvokeCount(): Integer
|
|
100
|
+
begin
|
|
101
|
+
exit(InvokeCount);
|
|
102
|
+
end;
|
|
103
|
+
|
|
104
|
+
procedure GetLastFromCurrency(): Code[10]
|
|
105
|
+
begin
|
|
106
|
+
exit(LastFromCurrency);
|
|
107
|
+
end;
|
|
108
|
+
|
|
109
|
+
procedure GetLastToCurrency(): Code[10]
|
|
110
|
+
begin
|
|
111
|
+
exit(LastToCurrency);
|
|
112
|
+
end;
|
|
113
|
+
|
|
114
|
+
procedure GetLastFromAmount(): Decimal
|
|
115
|
+
begin
|
|
116
|
+
exit(LastFromAmount);
|
|
117
|
+
end;
|
|
118
|
+
|
|
119
|
+
procedure GetLastToAmount(): Decimal
|
|
120
|
+
begin
|
|
121
|
+
exit(LastToAmount);
|
|
122
|
+
end;
|
|
123
|
+
|
|
124
|
+
procedure GetLastUserID(): Text[50]
|
|
125
|
+
begin
|
|
126
|
+
exit(LastUserID);
|
|
127
|
+
end;
|
|
128
|
+
|
|
129
|
+
procedure Reset()
|
|
130
|
+
begin
|
|
131
|
+
WasInvoked := false;
|
|
132
|
+
InvokeCount := 0;
|
|
133
|
+
Clear(LastFromCurrency);
|
|
134
|
+
Clear(LastToCurrency);
|
|
135
|
+
Clear(LastFromAmount);
|
|
136
|
+
Clear(LastToAmount);
|
|
137
|
+
Clear(LastUserID);
|
|
138
|
+
end;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
codeunit 80021 "Spy Currency Converter" implements ICurrencyConverter
|
|
142
|
+
{
|
|
143
|
+
var
|
|
144
|
+
WasInvoked: Boolean;
|
|
145
|
+
CapturedDate: Date;
|
|
146
|
+
CapturedFromCurrency: Code[10];
|
|
147
|
+
CapturedToCurrency: Code[10];
|
|
148
|
+
CapturedAmount: Decimal;
|
|
149
|
+
ReturnValue: Decimal;
|
|
150
|
+
|
|
151
|
+
procedure Convert(AtDate: Date; FromCurrency: Code[10]; ToCurrency: Code[10]; Amount: Decimal): Decimal
|
|
152
|
+
begin
|
|
153
|
+
WasInvoked := true;
|
|
154
|
+
CapturedDate := AtDate;
|
|
155
|
+
CapturedFromCurrency := FromCurrency;
|
|
156
|
+
CapturedToCurrency := ToCurrency;
|
|
157
|
+
CapturedAmount := Amount;
|
|
158
|
+
exit(ReturnValue);
|
|
159
|
+
end;
|
|
160
|
+
|
|
161
|
+
procedure SetReturnValue(Value: Decimal)
|
|
162
|
+
begin
|
|
163
|
+
ReturnValue := Value;
|
|
164
|
+
end;
|
|
165
|
+
|
|
166
|
+
procedure IsInvoked(): Boolean
|
|
167
|
+
begin
|
|
168
|
+
exit(WasInvoked);
|
|
169
|
+
end;
|
|
170
|
+
|
|
171
|
+
procedure GetCapturedDate(): Date
|
|
172
|
+
begin
|
|
173
|
+
exit(CapturedDate);
|
|
174
|
+
end;
|
|
175
|
+
|
|
176
|
+
procedure GetCapturedFromCurrency(): Code[10]
|
|
177
|
+
begin
|
|
178
|
+
exit(CapturedFromCurrency);
|
|
179
|
+
end;
|
|
180
|
+
|
|
181
|
+
procedure GetCapturedToCurrency(): Code[10]
|
|
182
|
+
begin
|
|
183
|
+
exit(CapturedToCurrency);
|
|
184
|
+
end;
|
|
185
|
+
|
|
186
|
+
procedure GetCapturedAmount(): Decimal
|
|
187
|
+
begin
|
|
188
|
+
exit(CapturedAmount);
|
|
189
|
+
end;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Throwing Test Double - Simulates Errors
|
|
194
|
+
```al
|
|
195
|
+
codeunit 80030 "Throwing Converter" implements ICurrencyConverter
|
|
196
|
+
{
|
|
197
|
+
var
|
|
198
|
+
ErrorMessage: Text;
|
|
199
|
+
|
|
200
|
+
procedure Convert(AtDate: Date; FromCurrency: Code[10]; ToCurrency: Code[10]; Amount: Decimal): Decimal
|
|
201
|
+
begin
|
|
202
|
+
if ErrorMessage <> '' then
|
|
203
|
+
Error(ErrorMessage);
|
|
204
|
+
Error('Simulated conversion failure');
|
|
205
|
+
end;
|
|
206
|
+
|
|
207
|
+
procedure SetErrorMessage(Message: Text)
|
|
208
|
+
begin
|
|
209
|
+
ErrorMessage := Message;
|
|
210
|
+
end;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Complete Test Codeunit Example
|
|
217
|
+
|
|
218
|
+
```al
|
|
219
|
+
codeunit 80100 "Currency Conversion Tests"
|
|
220
|
+
{
|
|
221
|
+
Subtype = Test;
|
|
222
|
+
|
|
223
|
+
var
|
|
224
|
+
Assert: Codeunit Assert;
|
|
225
|
+
CurrencyConversion: Codeunit "Currency Conversion";
|
|
226
|
+
|
|
227
|
+
[Test]
|
|
228
|
+
procedure Test_01_SuccessfulConversion()
|
|
229
|
+
var
|
|
230
|
+
StubPermission: Codeunit "Stub Permission Checker";
|
|
231
|
+
StubConverter: Codeunit "Stub Currency Converter";
|
|
232
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
233
|
+
Result: Decimal;
|
|
234
|
+
begin
|
|
235
|
+
// [SCENARIO] Successful currency conversion logs the result
|
|
236
|
+
|
|
237
|
+
// [GIVEN] Permission granted
|
|
238
|
+
StubPermission.SetAllowed(true);
|
|
239
|
+
|
|
240
|
+
// [GIVEN] Converter returns known value
|
|
241
|
+
StubConverter.SetReturnValue(100);
|
|
242
|
+
|
|
243
|
+
// [WHEN] Conversion is performed
|
|
244
|
+
Result := CurrencyConversion.Convert(50, 'USD', 'EUR', StubPermission, StubConverter, SpyLogger);
|
|
245
|
+
|
|
246
|
+
// [THEN] Result is converter's return value
|
|
247
|
+
Assert.AreEqual(100, Result, 'Should return converter result');
|
|
248
|
+
|
|
249
|
+
// [THEN] Logger was invoked with correct values
|
|
250
|
+
Assert.IsTrue(SpyLogger.IsInvoked(), 'Logger should be invoked');
|
|
251
|
+
Assert.AreEqual('USD', SpyLogger.GetLastFromCurrency(), 'From currency');
|
|
252
|
+
Assert.AreEqual('EUR', SpyLogger.GetLastToCurrency(), 'To currency');
|
|
253
|
+
Assert.AreEqual(50, SpyLogger.GetLastFromAmount(), 'From amount');
|
|
254
|
+
Assert.AreEqual(100, SpyLogger.GetLastToAmount(), 'To amount (converter result)');
|
|
255
|
+
end;
|
|
256
|
+
|
|
257
|
+
[Test]
|
|
258
|
+
procedure Test_02_PermissionDenied_NoConversion()
|
|
259
|
+
var
|
|
260
|
+
StubPermission: Codeunit "Stub Permission Checker";
|
|
261
|
+
DummyConverter: Codeunit "Dummy Currency Converter";
|
|
262
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
263
|
+
begin
|
|
264
|
+
// [SCENARIO] Permission denied prevents conversion and logging
|
|
265
|
+
|
|
266
|
+
// [GIVEN] Permission denied
|
|
267
|
+
StubPermission.SetAllowed(false);
|
|
268
|
+
|
|
269
|
+
// [WHEN] Conversion is attempted
|
|
270
|
+
asserterror CurrencyConversion.Convert(50, 'USD', 'EUR', StubPermission, DummyConverter, SpyLogger);
|
|
271
|
+
|
|
272
|
+
// [THEN] Error is raised
|
|
273
|
+
Assert.ExpectedError('not allowed');
|
|
274
|
+
|
|
275
|
+
// [THEN] Logger was NOT invoked
|
|
276
|
+
Assert.IsFalse(SpyLogger.IsInvoked(), 'Logger should not be invoked on permission failure');
|
|
277
|
+
end;
|
|
278
|
+
|
|
279
|
+
[Test]
|
|
280
|
+
procedure Test_03_ConverterParameters()
|
|
281
|
+
var
|
|
282
|
+
StubPermission: Codeunit "Stub Permission Checker";
|
|
283
|
+
SpyConverter: Codeunit "Spy Currency Converter";
|
|
284
|
+
DummyLogger: Codeunit "Dummy Logger";
|
|
285
|
+
begin
|
|
286
|
+
// [SCENARIO] Converter receives correct parameters
|
|
287
|
+
|
|
288
|
+
// [GIVEN] Permission granted
|
|
289
|
+
StubPermission.SetAllowed(true);
|
|
290
|
+
SpyConverter.SetReturnValue(999);
|
|
291
|
+
|
|
292
|
+
// [WHEN] Conversion with specific values
|
|
293
|
+
CurrencyConversion.Convert(123.45, 'GBP', 'JPY', StubPermission, SpyConverter, DummyLogger);
|
|
294
|
+
|
|
295
|
+
// [THEN] Converter received correct parameters
|
|
296
|
+
Assert.IsTrue(SpyConverter.IsInvoked(), 'Converter should be called');
|
|
297
|
+
Assert.AreEqual('GBP', SpyConverter.GetCapturedFromCurrency(), 'From currency');
|
|
298
|
+
Assert.AreEqual('JPY', SpyConverter.GetCapturedToCurrency(), 'To currency');
|
|
299
|
+
Assert.AreEqual(123.45, SpyConverter.GetCapturedAmount(), 'Amount');
|
|
300
|
+
Assert.AreEqual(WorkDate(), SpyConverter.GetCapturedDate(), 'Date should be WorkDate');
|
|
301
|
+
end;
|
|
302
|
+
|
|
303
|
+
[Test]
|
|
304
|
+
procedure Test_04_ConverterFailure_Propagates()
|
|
305
|
+
var
|
|
306
|
+
StubPermission: Codeunit "Stub Permission Checker";
|
|
307
|
+
ThrowingConverter: Codeunit "Throwing Converter";
|
|
308
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
309
|
+
begin
|
|
310
|
+
// [SCENARIO] Converter errors propagate correctly
|
|
311
|
+
|
|
312
|
+
// [GIVEN] Permission granted
|
|
313
|
+
StubPermission.SetAllowed(true);
|
|
314
|
+
|
|
315
|
+
// [GIVEN] Converter will fail
|
|
316
|
+
ThrowingConverter.SetErrorMessage('External service unavailable');
|
|
317
|
+
|
|
318
|
+
// [WHEN] Conversion is attempted
|
|
319
|
+
asserterror CurrencyConversion.Convert(50, 'USD', 'EUR', StubPermission, ThrowingConverter, SpyLogger);
|
|
320
|
+
|
|
321
|
+
// [THEN] Converter error propagates
|
|
322
|
+
Assert.ExpectedError('service unavailable');
|
|
323
|
+
|
|
324
|
+
// [THEN] Logger was NOT invoked (conversion failed)
|
|
325
|
+
Assert.IsFalse(SpyLogger.IsInvoked(), 'Logger should not be invoked on conversion failure');
|
|
326
|
+
end;
|
|
327
|
+
|
|
328
|
+
[Test]
|
|
329
|
+
procedure Test_05_MultipleConversions()
|
|
330
|
+
var
|
|
331
|
+
StubPermission: Codeunit "Stub Permission Checker";
|
|
332
|
+
StubConverter: Codeunit "Stub Currency Converter";
|
|
333
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
334
|
+
begin
|
|
335
|
+
// [SCENARIO] Multiple conversions each logged separately
|
|
336
|
+
|
|
337
|
+
// [GIVEN] Permission granted
|
|
338
|
+
StubPermission.SetAllowed(true);
|
|
339
|
+
StubConverter.SetReturnValue(100);
|
|
340
|
+
|
|
341
|
+
// [WHEN] Two conversions performed
|
|
342
|
+
CurrencyConversion.Convert(10, 'USD', 'EUR', StubPermission, StubConverter, SpyLogger);
|
|
343
|
+
CurrencyConversion.Convert(20, 'GBP', 'JPY', StubPermission, StubConverter, SpyLogger);
|
|
344
|
+
|
|
345
|
+
// [THEN] Logger invoked twice
|
|
346
|
+
Assert.AreEqual(2, SpyLogger.GetInvokeCount(), 'Logger should be invoked twice');
|
|
347
|
+
|
|
348
|
+
// [THEN] Last call has correct values
|
|
349
|
+
Assert.AreEqual('GBP', SpyLogger.GetLastFromCurrency(), 'Last from currency');
|
|
350
|
+
Assert.AreEqual('JPY', SpyLogger.GetLastToCurrency(), 'Last to currency');
|
|
351
|
+
end;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Testing Production Implementations Separately
|
|
358
|
+
|
|
359
|
+
```al
|
|
360
|
+
codeunit 80110 "Database Permission Checker Tests"
|
|
361
|
+
{
|
|
362
|
+
Subtype = Test;
|
|
363
|
+
|
|
364
|
+
var
|
|
365
|
+
Assert: Codeunit Assert;
|
|
366
|
+
|
|
367
|
+
[Test]
|
|
368
|
+
procedure Test_PermissionExists_ReturnsTrue()
|
|
369
|
+
var
|
|
370
|
+
Permission: Record "Currency Exchange Permission";
|
|
371
|
+
PermissionChecker: Codeunit "Database Permission Checker";
|
|
372
|
+
begin
|
|
373
|
+
// [GIVEN] Permission record exists
|
|
374
|
+
Permission."User ID" := 'TESTUSER';
|
|
375
|
+
Permission."From Currency Code" := 'USD';
|
|
376
|
+
Permission."To Currency Code" := 'EUR';
|
|
377
|
+
Permission.Insert(false);
|
|
378
|
+
|
|
379
|
+
// [WHEN] Check permission
|
|
380
|
+
// [THEN] Returns true
|
|
381
|
+
Assert.IsTrue(
|
|
382
|
+
PermissionChecker.CanConvert('USD', 'EUR', 'TESTUSER'),
|
|
383
|
+
'Should allow when permission exists'
|
|
384
|
+
);
|
|
385
|
+
end;
|
|
386
|
+
|
|
387
|
+
[Test]
|
|
388
|
+
procedure Test_NoPermission_ReturnsFalse()
|
|
389
|
+
var
|
|
390
|
+
PermissionChecker: Codeunit "Database Permission Checker";
|
|
391
|
+
begin
|
|
392
|
+
// [GIVEN] No permission records for this user/currency combination
|
|
393
|
+
|
|
394
|
+
// [WHEN] Check permission
|
|
395
|
+
// [THEN] Returns false
|
|
396
|
+
Assert.IsFalse(
|
|
397
|
+
PermissionChecker.CanConvert('XXX', 'YYY', 'UNKNOWNUSER'),
|
|
398
|
+
'Should deny when no permission exists'
|
|
399
|
+
);
|
|
400
|
+
end;
|
|
401
|
+
|
|
402
|
+
[Test]
|
|
403
|
+
procedure Test_WildcardPermission_AllowsAnyCurrency()
|
|
404
|
+
var
|
|
405
|
+
Permission: Record "Currency Exchange Permission";
|
|
406
|
+
PermissionChecker: Codeunit "Database Permission Checker";
|
|
407
|
+
begin
|
|
408
|
+
// [GIVEN] Wildcard permission (empty currency codes)
|
|
409
|
+
Permission."User ID" := 'SUPERUSER';
|
|
410
|
+
Permission."From Currency Code" := ''; // Any
|
|
411
|
+
Permission."To Currency Code" := ''; // Any
|
|
412
|
+
Permission.Insert(false);
|
|
413
|
+
|
|
414
|
+
// [WHEN] Check any currency combination
|
|
415
|
+
// [THEN] Returns true for any currencies
|
|
416
|
+
Assert.IsTrue(
|
|
417
|
+
PermissionChecker.CanConvert('ABC', 'XYZ', 'SUPERUSER'),
|
|
418
|
+
'Wildcard should allow any currencies'
|
|
419
|
+
);
|
|
420
|
+
end;
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Key Point**: Test your production implementations directly with simple, focused tests. Then use test doubles when testing business logic that USES those implementations.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Testability Code Smell Examples
|
|
2
|
+
|
|
3
|
+
## Code Smell #1: Direct BC Table Access
|
|
4
|
+
|
|
5
|
+
### ❌ Problematic Code
|
|
6
|
+
```al
|
|
7
|
+
procedure CalculateCustomerDiscount(CustomerNo: Code[20]; Amount: Decimal): Decimal
|
|
8
|
+
var
|
|
9
|
+
Customer: Record Customer;
|
|
10
|
+
CustomerPriceGroup: Record "Customer Price Group";
|
|
11
|
+
DiscountPct: Decimal;
|
|
12
|
+
begin
|
|
13
|
+
// Direct access to BC tables
|
|
14
|
+
Customer.Get(CustomerNo);
|
|
15
|
+
Customer.TestField("Customer Price Group");
|
|
16
|
+
|
|
17
|
+
CustomerPriceGroup.Get(Customer."Customer Price Group");
|
|
18
|
+
DiscountPct := CustomerPriceGroup."Discount %";
|
|
19
|
+
|
|
20
|
+
if Customer."Allow Line Disc." then
|
|
21
|
+
DiscountPct += 5;
|
|
22
|
+
|
|
23
|
+
exit(Amount * DiscountPct / 100);
|
|
24
|
+
end;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Why It's Problematic
|
|
28
|
+
To test this, you need:
|
|
29
|
+
- A Customer record with specific fields populated
|
|
30
|
+
- A Customer Price Group record
|
|
31
|
+
- Proper relationships between them
|
|
32
|
+
|
|
33
|
+
Test becomes:
|
|
34
|
+
```al
|
|
35
|
+
[Test]
|
|
36
|
+
procedure Test_CustomerDiscount()
|
|
37
|
+
begin
|
|
38
|
+
// [GIVEN] Customer with price group
|
|
39
|
+
LibrarySales.CreateCustomer(Customer);
|
|
40
|
+
Customer."Allow Line Disc." := true;
|
|
41
|
+
Customer.Modify();
|
|
42
|
+
|
|
43
|
+
// [GIVEN] Price group with discount
|
|
44
|
+
CreateCustomerPriceGroup(PriceGroup);
|
|
45
|
+
PriceGroup."Discount %" := 10;
|
|
46
|
+
PriceGroup.Modify();
|
|
47
|
+
|
|
48
|
+
Customer."Customer Price Group" := PriceGroup.Code;
|
|
49
|
+
Customer.Modify();
|
|
50
|
+
|
|
51
|
+
// You're testing BC table relationships, not your discount logic!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### ✅ Better: Separate Data Access from Logic
|
|
55
|
+
```al
|
|
56
|
+
// Interface for getting discount configuration
|
|
57
|
+
interface IDiscountConfiguration
|
|
58
|
+
{
|
|
59
|
+
procedure GetBaseDiscount(CustomerNo: Code[20]): Decimal;
|
|
60
|
+
procedure AllowsLineDiscount(CustomerNo: Code[20]): Boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Business logic - testable
|
|
64
|
+
procedure CalculateDiscount(Amount: Decimal; BaseDiscount: Decimal; AllowLineDiscount: Boolean): Decimal
|
|
65
|
+
var
|
|
66
|
+
TotalDiscount: Decimal;
|
|
67
|
+
begin
|
|
68
|
+
TotalDiscount := BaseDiscount;
|
|
69
|
+
if AllowLineDiscount then
|
|
70
|
+
TotalDiscount += 5;
|
|
71
|
+
exit(Amount * TotalDiscount / 100);
|
|
72
|
+
end;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Code Smell #2: Tests Full of Unrelated GIVENs
|
|
78
|
+
|
|
79
|
+
### ❌ Problematic Test
|
|
80
|
+
```al
|
|
81
|
+
[Test]
|
|
82
|
+
procedure Test_CalculateShippingCost()
|
|
83
|
+
var
|
|
84
|
+
Customer: Record Customer;
|
|
85
|
+
Item: Record Item;
|
|
86
|
+
SalesHeader: Record "Sales Header";
|
|
87
|
+
SalesLine: Record "Sales Line";
|
|
88
|
+
ShippingAgent: Record "Shipping Agent";
|
|
89
|
+
ShippingService: Record "Shipping Agent Services";
|
|
90
|
+
begin
|
|
91
|
+
// [GIVEN] Customer
|
|
92
|
+
LibrarySales.CreateCustomer(Customer);
|
|
93
|
+
|
|
94
|
+
// [GIVEN] Shipping Agent with service
|
|
95
|
+
LibraryInventory.CreateShippingAgent(ShippingAgent);
|
|
96
|
+
LibraryInventory.CreateShippingAgentService(ShippingAgent.Code, ShippingService);
|
|
97
|
+
ShippingService."Shipping Cost" := 10;
|
|
98
|
+
ShippingService.Modify();
|
|
99
|
+
|
|
100
|
+
// [GIVEN] Item with weight
|
|
101
|
+
LibraryInventory.CreateItem(Item);
|
|
102
|
+
Item."Gross Weight" := 5;
|
|
103
|
+
Item.Modify();
|
|
104
|
+
|
|
105
|
+
// [GIVEN] Sales Order
|
|
106
|
+
LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No.");
|
|
107
|
+
SalesHeader."Shipping Agent Code" := ShippingAgent.Code;
|
|
108
|
+
SalesHeader."Shipping Agent Service Code" := ShippingService.Code;
|
|
109
|
+
SalesHeader.Modify();
|
|
110
|
+
|
|
111
|
+
// [GIVEN] Sales Line
|
|
112
|
+
LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 10);
|
|
113
|
+
|
|
114
|
+
// [WHEN] Calculate shipping
|
|
115
|
+
ShippingCalculator.Calculate(SalesHeader);
|
|
116
|
+
|
|
117
|
+
// [THEN] Cost is correct
|
|
118
|
+
SalesHeader.Find();
|
|
119
|
+
Assert.AreEqual(50, SalesHeader."Shipping Cost", 'Expected 5kg * 10 units * $1/kg');
|
|
120
|
+
end;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Why It's Problematic
|
|
124
|
+
- 25 lines of GIVEN for 2 lines of WHEN/THEN
|
|
125
|
+
- Most GIVENs satisfy BC table requirements, not business logic
|
|
126
|
+
- If BC changes Customer or Item structure, test breaks
|
|
127
|
+
- Slow due to database operations
|
|
128
|
+
|
|
129
|
+
### ✅ Better: Test the Calculation Directly
|
|
130
|
+
```al
|
|
131
|
+
[Test]
|
|
132
|
+
procedure Test_CalculateShippingCost()
|
|
133
|
+
var
|
|
134
|
+
Calculator: Codeunit "Shipping Calculator";
|
|
135
|
+
Weight: Decimal;
|
|
136
|
+
CostPerKg: Decimal;
|
|
137
|
+
ExpectedCost: Decimal;
|
|
138
|
+
begin
|
|
139
|
+
// [GIVEN] Known weight and cost rate
|
|
140
|
+
Weight := 50; // 5kg * 10 units
|
|
141
|
+
CostPerKg := 1;
|
|
142
|
+
|
|
143
|
+
// [WHEN] Calculate shipping cost
|
|
144
|
+
ActualCost := Calculator.CalculateCost(Weight, CostPerKg);
|
|
145
|
+
|
|
146
|
+
// [THEN] Cost is weight * rate
|
|
147
|
+
Assert.AreEqual(50, ActualCost, 'Weight * CostPerKg');
|
|
148
|
+
end;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Code Smell #3: Assertions Testing BC Behavior
|
|
154
|
+
|
|
155
|
+
### ❌ Problematic Test
|
|
156
|
+
```al
|
|
157
|
+
[Test]
|
|
158
|
+
procedure Test_CurrencyConversion()
|
|
159
|
+
begin
|
|
160
|
+
// [GIVEN] Currencies and exchange rates
|
|
161
|
+
LibraryERM.CreateCurrency(FromCurrency);
|
|
162
|
+
LibraryERM.CreateCurrency(ToCurrency);
|
|
163
|
+
LibraryERM.CreateExchangeRate(FromCurrency.Code, WorkDate(), 10, 10);
|
|
164
|
+
LibraryERM.CreateExchangeRate(ToCurrency.Code, WorkDate(), 0.1, 0.1);
|
|
165
|
+
|
|
166
|
+
// [WHEN] Convert 1 unit
|
|
167
|
+
Amount := Converter.Convert(1, FromCurrency.Code, ToCurrency.Code);
|
|
168
|
+
|
|
169
|
+
// [THEN] Amount is converted correctly
|
|
170
|
+
Assert.AreEqual(0.01, Amount, 'Conversion incorrect');
|
|
171
|
+
// ^^^ Do you even know WHY this should be 0.01?
|
|
172
|
+
end;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Why It's Problematic
|
|
176
|
+
- The expected value (0.01) comes from BC's calculation, not your knowledge
|
|
177
|
+
- You're testing BC's exchange rate math, not your code
|
|
178
|
+
- If BC changes rounding rules, your test breaks (but your code might be fine)
|
|
179
|
+
|
|
180
|
+
### ✅ Better: Use a Stub with Known Values
|
|
181
|
+
```al
|
|
182
|
+
[Test]
|
|
183
|
+
procedure Test_ConversionProcessFlow()
|
|
184
|
+
var
|
|
185
|
+
StubConverter: Codeunit "Stub Currency Converter";
|
|
186
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
187
|
+
begin
|
|
188
|
+
// [GIVEN] Converter that returns known value
|
|
189
|
+
StubConverter.SetReturnValue(42);
|
|
190
|
+
|
|
191
|
+
// [WHEN] Conversion performed
|
|
192
|
+
Amount := MyProcess.Convert(100, 'USD', 'EUR', StubConverter, SpyLogger);
|
|
193
|
+
|
|
194
|
+
// [THEN] Process returns converter's result
|
|
195
|
+
Assert.AreEqual(42, Amount, 'Should return converter result');
|
|
196
|
+
|
|
197
|
+
// [THEN] Logger captured correct input
|
|
198
|
+
Assert.AreEqual(100, SpyLogger.GetCapturedInputAmount(), 'Input logged');
|
|
199
|
+
end;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Code Smell #4: DeleteAll in Test Setup
|
|
205
|
+
|
|
206
|
+
### ❌ Problematic Test
|
|
207
|
+
```al
|
|
208
|
+
[Test]
|
|
209
|
+
procedure Test_CreateLogEntry()
|
|
210
|
+
var
|
|
211
|
+
IntegrationLog: Record "Integration Log";
|
|
212
|
+
begin
|
|
213
|
+
// [GIVEN] No existing entries
|
|
214
|
+
IntegrationLog.DeleteAll(); // ❌ Modifying shared state
|
|
215
|
+
|
|
216
|
+
// [WHEN] Process runs
|
|
217
|
+
IntegrationProcess.Run();
|
|
218
|
+
|
|
219
|
+
// [THEN] One entry created
|
|
220
|
+
Assert.AreEqual(1, IntegrationLog.Count(), 'Expected one entry');
|
|
221
|
+
end;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Why It's Problematic
|
|
225
|
+
- Modifies database state that other tests might depend on
|
|
226
|
+
- "No existing entries" is NOT a realistic business scenario
|
|
227
|
+
- Test depends on counting records, not verifying behavior
|
|
228
|
+
|
|
229
|
+
### ✅ Better: Use a Spy Logger
|
|
230
|
+
```al
|
|
231
|
+
[Test]
|
|
232
|
+
procedure Test_ProcessLogsExecution()
|
|
233
|
+
var
|
|
234
|
+
SpyLogger: Codeunit "Spy Logger";
|
|
235
|
+
begin
|
|
236
|
+
// [WHEN] Process runs with spy logger
|
|
237
|
+
IntegrationProcess.Run(SpyLogger);
|
|
238
|
+
|
|
239
|
+
// [THEN] Logger was invoked
|
|
240
|
+
Assert.IsTrue(SpyLogger.WasInvoked(), 'Should log execution');
|
|
241
|
+
Assert.AreEqual('IntegrationProcess', SpyLogger.GetLastOperation(), 'Operation name');
|
|
242
|
+
end;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Quick Reference: Smells and Fixes
|
|
248
|
+
|
|
249
|
+
| Smell | Symptom | Fix |
|
|
250
|
+
|-------|---------|-----|
|
|
251
|
+
| Direct BC table access | Can't test without database | Interface abstraction |
|
|
252
|
+
| Too many GIVENs | Test setup >> test logic | Dependency injection |
|
|
253
|
+
| Asserting BC values | You calculated expected from BC | Stub with known values |
|
|
254
|
+
| DeleteAll in setup | Cleaning shared state | Spy/Mock instead |
|
|
255
|
+
| Setup switch | `case Setup.Type of` | Interface polymorphism |
|
|
256
|
+
| Mixed data + logic | Single proc does both | Separate into layers |
|