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.
Files changed (69) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +165 -419
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/layers/embedded-layer.js +29 -29
  7. package/dist/layers/project-layer.js +33 -33
  8. package/dist/sdk/bc-code-intel-client.d.ts.map +1 -1
  9. package/dist/sdk/bc-code-intel-client.js +1 -1
  10. package/dist/sdk/bc-code-intel-client.js.map +1 -1
  11. package/dist/services/knowledge-service.d.ts +1 -1
  12. package/dist/services/knowledge-service.d.ts.map +1 -1
  13. package/dist/services/knowledge-service.js +71 -3
  14. package/dist/services/knowledge-service.js.map +1 -1
  15. package/dist/services/methodology-service.js +14 -14
  16. package/dist/services/multi-content-layer-service.d.ts +15 -0
  17. package/dist/services/multi-content-layer-service.d.ts.map +1 -1
  18. package/dist/services/multi-content-layer-service.js +62 -0
  19. package/dist/services/multi-content-layer-service.js.map +1 -1
  20. package/dist/streamlined-handlers.d.ts +0 -7
  21. package/dist/streamlined-handlers.d.ts.map +1 -1
  22. package/dist/streamlined-handlers.js +80 -60
  23. package/dist/streamlined-handlers.js.map +1 -1
  24. package/dist/tools/core-tools.d.ts.map +1 -1
  25. package/dist/tools/core-tools.js +4 -0
  26. package/dist/tools/core-tools.js.map +1 -1
  27. package/dist/tools/onboarding-tools.d.ts +8 -0
  28. package/dist/tools/onboarding-tools.d.ts.map +1 -1
  29. package/dist/tools/onboarding-tools.js +111 -1
  30. package/dist/tools/onboarding-tools.js.map +1 -1
  31. package/dist/types/bc-knowledge.d.ts +4 -4
  32. package/embedded-knowledge/.github/ISSUE_TEMPLATE/bug-report.md +23 -23
  33. package/embedded-knowledge/.github/ISSUE_TEMPLATE/content-improvement.md +23 -23
  34. package/embedded-knowledge/.github/ISSUE_TEMPLATE/knowledge-request.md +29 -29
  35. package/embedded-knowledge/AGENTS.md +177 -177
  36. package/embedded-knowledge/CONTRIBUTING.md +57 -57
  37. package/embedded-knowledge/LICENSE +20 -20
  38. package/embedded-knowledge/README.md +31 -31
  39. package/embedded-knowledge/domains/alex-architect/samples/testability-design-patterns.md +223 -0
  40. package/embedded-knowledge/domains/alex-architect/testability-design-patterns.md +77 -0
  41. package/embedded-knowledge/domains/casey-copilot/long-running-session-instructions.md +263 -0
  42. package/embedded-knowledge/domains/casey-copilot/samples/long-running-session-instructions.md +323 -0
  43. package/embedded-knowledge/domains/eva-errors/codeunit-run-pattern.md +159 -0
  44. package/embedded-knowledge/domains/eva-errors/samples/codeunit-run-pattern.md +239 -0
  45. package/embedded-knowledge/domains/eva-errors/samples/try-function-usage.md +195 -0
  46. package/embedded-knowledge/domains/eva-errors/try-function-usage.md +129 -0
  47. package/embedded-knowledge/domains/morgan-market/partner-readiness-analysis.md +201 -0
  48. package/embedded-knowledge/domains/morgan-market/samples/partner-readiness-checklist.md +288 -0
  49. package/embedded-knowledge/domains/quinn-tester/isolation-testing-patterns.md +82 -0
  50. package/embedded-knowledge/domains/quinn-tester/samples/isolation-testing-patterns.md +424 -0
  51. package/embedded-knowledge/domains/roger-reviewer/samples/testability-code-smells.md +256 -0
  52. package/embedded-knowledge/domains/roger-reviewer/testability-code-smells.md +67 -0
  53. package/embedded-knowledge/domains/shared/al-file-naming-conventions.md +145 -145
  54. package/embedded-knowledge/methodologies/index.json +80 -80
  55. package/embedded-knowledge/methodologies/phases/analysis-full.md +207 -207
  56. package/embedded-knowledge/methodologies/phases/analysis-quick.md +43 -43
  57. package/embedded-knowledge/methodologies/phases/analysis.md +181 -181
  58. package/embedded-knowledge/methodologies/phases/execution-validation-full.md +173 -173
  59. package/embedded-knowledge/methodologies/phases/execution-validation-quick.md +30 -30
  60. package/embedded-knowledge/methodologies/phases/execution-validation.md +173 -173
  61. package/embedded-knowledge/methodologies/phases/performance-full.md +210 -210
  62. package/embedded-knowledge/methodologies/phases/performance-quick.md +31 -31
  63. package/embedded-knowledge/methodologies/phases/performance.md +210 -210
  64. package/embedded-knowledge/methodologies/phases/verification-full.md +161 -161
  65. package/embedded-knowledge/methodologies/phases/verification-quick.md +47 -47
  66. package/embedded-knowledge/methodologies/phases/verification.md +145 -145
  67. package/embedded-knowledge/methodologies/workflow-enforcement.md +141 -141
  68. package/embedded-knowledge/methodologies/workflows/code-review-workflow.md +98 -98
  69. 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 |