bc-code-intelligence-mcp 1.5.6 → 1.5.7
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/README.md +153 -407
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- 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/types/bc-knowledge.d.ts +4 -4
- 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/package.json +2 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Try Function Usage Guidelines"
|
|
3
|
+
domain: "eva-errors"
|
|
4
|
+
difficulty: "intermediate"
|
|
5
|
+
bc_versions: "14+"
|
|
6
|
+
tags: ["try-function", "error-handling", "transactions", "external-systems", "best-practices"]
|
|
7
|
+
samples: "samples/try-function-usage.md"
|
|
8
|
+
related_topics:
|
|
9
|
+
- "codeunit-run-pattern.md"
|
|
10
|
+
- "testfield-error-handling.md"
|
|
11
|
+
---
|
|
12
|
+
# Try Function Usage Guidelines
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Try functions in Business Central provide a mechanism to catch runtime errors without terminating execution. While powerful, they have significant constraints around transaction handling that must be understood to use them correctly.
|
|
17
|
+
|
|
18
|
+
**Core Principle**: Try functions are designed for error detection and graceful handling—not for suppressing errors during write operations.
|
|
19
|
+
|
|
20
|
+
## When to Use Try Functions
|
|
21
|
+
|
|
22
|
+
### External System Calls
|
|
23
|
+
Try functions are ideal for wrapping calls to external systems where failures are expected and recoverable:
|
|
24
|
+
- Web service calls that may timeout or fail
|
|
25
|
+
- External API integrations
|
|
26
|
+
- File system operations
|
|
27
|
+
- HTTP client requests
|
|
28
|
+
|
|
29
|
+
External systems are inherently unreliable, and wrapping these calls allows your code to detect failures and respond appropriately rather than crashing.
|
|
30
|
+
|
|
31
|
+
### Validation Without Termination
|
|
32
|
+
Use try functions when you need to validate data or test conditions without stopping execution:
|
|
33
|
+
- Pre-flight checks before processing
|
|
34
|
+
- Batch validation scenarios
|
|
35
|
+
- User input validation with graceful feedback
|
|
36
|
+
|
|
37
|
+
### Read-Only Operations
|
|
38
|
+
Try functions work well for read operations that might fail:
|
|
39
|
+
- Record lookups that may not find results
|
|
40
|
+
- Configuration checks
|
|
41
|
+
- Data existence verification
|
|
42
|
+
|
|
43
|
+
## When NOT to Use Try Functions
|
|
44
|
+
|
|
45
|
+
### Write Operations in the Call Stack
|
|
46
|
+
**Critical Rule**: Never perform database write operations (INSERT, MODIFY, DELETE) inside a try function or any procedure called from within a try function.
|
|
47
|
+
|
|
48
|
+
This is prohibited because:
|
|
49
|
+
- The transaction behavior becomes unpredictable
|
|
50
|
+
- Partial writes cannot be properly rolled back
|
|
51
|
+
- Data integrity cannot be guaranteed
|
|
52
|
+
- The AL runtime explicitly prevents this pattern
|
|
53
|
+
|
|
54
|
+
### Suppressing Legitimate Errors
|
|
55
|
+
Don't use try functions to hide errors that indicate real problems:
|
|
56
|
+
- Business rule violations should be reported, not hidden
|
|
57
|
+
- Data validation failures need user attention
|
|
58
|
+
- Configuration errors require administrator action
|
|
59
|
+
|
|
60
|
+
### Complex Transaction Scenarios
|
|
61
|
+
Avoid try functions in the middle of multi-step transaction processes where you need consistent state management.
|
|
62
|
+
|
|
63
|
+
## Transaction Behavior
|
|
64
|
+
|
|
65
|
+
### Try Function Transaction Rules
|
|
66
|
+
When a try function catches an error:
|
|
67
|
+
- Any pending database changes within the try scope are rolled back
|
|
68
|
+
- The calling code continues execution
|
|
69
|
+
- GetLastErrorText() captures the error message
|
|
70
|
+
|
|
71
|
+
### Write Operation Restrictions
|
|
72
|
+
The Business Central runtime enforces that write transactions cannot occur within a try function's call stack. Attempting this will result in a runtime error indicating the operation is not allowed.
|
|
73
|
+
|
|
74
|
+
## Implementation Patterns
|
|
75
|
+
|
|
76
|
+
### External Service Wrapper
|
|
77
|
+
Create try functions specifically for external calls, keeping all database operations outside:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
// Correct pattern:
|
|
81
|
+
1. Prepare data (outside try)
|
|
82
|
+
2. Call try function for external operation
|
|
83
|
+
3. Check result
|
|
84
|
+
4. Write to database based on result (outside try)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Error Detection and Logging
|
|
88
|
+
Use try functions to detect errors, capture details, then handle the situation outside the try scope:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
// Pattern:
|
|
92
|
+
1. Call try function
|
|
93
|
+
2. If failed: capture GetLastErrorText()
|
|
94
|
+
3. Log error or notify user
|
|
95
|
+
4. Take appropriate action
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Best Practices
|
|
99
|
+
|
|
100
|
+
### Keep Try Functions Focused
|
|
101
|
+
- Single responsibility: one external call or validation per try function
|
|
102
|
+
- Minimal code inside the try function
|
|
103
|
+
- No side effects beyond the intended operation
|
|
104
|
+
|
|
105
|
+
### Handle Errors Appropriately
|
|
106
|
+
- Always check the return value of try function calls
|
|
107
|
+
- Use GetLastErrorText() to capture error details
|
|
108
|
+
- Provide meaningful feedback or logging
|
|
109
|
+
|
|
110
|
+
### Separate Concerns
|
|
111
|
+
- Database operations: outside try functions
|
|
112
|
+
- External calls: inside try functions
|
|
113
|
+
- Error handling logic: after try function returns
|
|
114
|
+
|
|
115
|
+
## Common Mistakes
|
|
116
|
+
|
|
117
|
+
### Database Writes Inside Try
|
|
118
|
+
Attempting to modify, insert, or delete records inside a try function or its call stack will fail at runtime.
|
|
119
|
+
|
|
120
|
+
### Ignoring Return Values
|
|
121
|
+
Calling a try function without checking its boolean return defeats the purpose of error handling.
|
|
122
|
+
|
|
123
|
+
### Over-Reliance on Try Functions
|
|
124
|
+
Using try functions as a general error suppression mechanism rather than for specific, expected failure scenarios creates fragile code.
|
|
125
|
+
|
|
126
|
+
## Related Patterns
|
|
127
|
+
|
|
128
|
+
For scenarios requiring transaction isolation with error handling, see the Codeunit.Run() pattern which provides proper transaction boundaries with error capture capabilities.
|
|
129
|
+
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Partner Readiness Analysis for AppSource Apps"
|
|
3
|
+
domain: "morgan-market"
|
|
4
|
+
difficulty: "advanced"
|
|
5
|
+
bc_versions: "18+"
|
|
6
|
+
tags: ["appsource", "partner-readiness", "extensibility", "telemetry", "access-modifiers", "events", "isv"]
|
|
7
|
+
samples: "samples/partner-readiness-checklist.md"
|
|
8
|
+
related_topics:
|
|
9
|
+
- "../jordan-bridge/event-architecture.md"
|
|
10
|
+
- "../alex-architect/api-interface-design-patterns.md"
|
|
11
|
+
- "../dean-debug/telemetry-fundamentals.md"
|
|
12
|
+
- "../seth-security/access-modifier-strategy.md"
|
|
13
|
+
---
|
|
14
|
+
# Partner Readiness Analysis for AppSource Apps
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
Publishing an app to AppSource involves more than passing technical validation. A truly partner-ready app considers the full lifecycle: how partners will extend it, how you'll support it in production, how customers will integrate with it, and how the codebase will evolve over time.
|
|
19
|
+
|
|
20
|
+
**Core Principle**: AppSource validation is the minimum bar. Partner readiness is the path to marketplace success and sustainable business.
|
|
21
|
+
|
|
22
|
+
## The Partner Lifecycle Mindset
|
|
23
|
+
|
|
24
|
+
When you publish to AppSource, you're not just shipping code—you're entering a partnership ecosystem:
|
|
25
|
+
|
|
26
|
+
- **Partners (PTEs)** will build on top of your app
|
|
27
|
+
- **Customers** will integrate your app into their processes
|
|
28
|
+
- **You** will need to diagnose issues remotely
|
|
29
|
+
- **Microsoft** will evolve the platform under your app
|
|
30
|
+
|
|
31
|
+
Each of these relationships requires intentional design decisions that go beyond "does it work?"
|
|
32
|
+
|
|
33
|
+
## Partner Readiness Checklist
|
|
34
|
+
|
|
35
|
+
### 1. Event Architecture for Extensibility
|
|
36
|
+
|
|
37
|
+
**Why It Matters**: Partners and PTEs need to extend your app without modifying your code. Without proper events, they're stuck—or worse, they'll find workarounds that break on your next update.
|
|
38
|
+
|
|
39
|
+
**Key Questions**:
|
|
40
|
+
- [ ] Do critical business processes raise events before and after key operations?
|
|
41
|
+
- [ ] Can partners inject validation logic before you commit data?
|
|
42
|
+
- [ ] Can partners react to state changes without polling?
|
|
43
|
+
- [ ] Are events granular enough to be useful but not so numerous as to be noisy?
|
|
44
|
+
|
|
45
|
+
**What to Expose**:
|
|
46
|
+
- Document posting and state transitions
|
|
47
|
+
- Master data creation and modification
|
|
48
|
+
- Integration touchpoints (before/after external calls)
|
|
49
|
+
- Validation phases where partners might add business rules
|
|
50
|
+
|
|
51
|
+
**Anti-Patterns**:
|
|
52
|
+
- Raising events with insufficient context (missing key fields)
|
|
53
|
+
- Events that fire but can't influence the outcome
|
|
54
|
+
- Undocumented events that partners discover by accident
|
|
55
|
+
|
|
56
|
+
**Specialist Referral**: → **Jordan Bridge** for event architecture patterns and publisher/subscriber design
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### 2. Telemetry for Production Support
|
|
61
|
+
|
|
62
|
+
**Why It Matters**: When something goes wrong at a customer site, you won't have debugger access. Telemetry is your eyes into production behavior—and your evidence when diagnosing "it doesn't work" reports.
|
|
63
|
+
|
|
64
|
+
**Key Questions**:
|
|
65
|
+
- [ ] Do you emit custom telemetry for key business operations?
|
|
66
|
+
- [ ] Can you identify which customer, which document, which operation from telemetry alone?
|
|
67
|
+
- [ ] Do you capture timing information for performance-sensitive operations?
|
|
68
|
+
- [ ] Are errors logged with enough context to diagnose without reproduction?
|
|
69
|
+
|
|
70
|
+
**What to Instrument**:
|
|
71
|
+
- External service calls (start, end, success/failure, duration)
|
|
72
|
+
- Long-running operations with intermediate checkpoints
|
|
73
|
+
- Error conditions with business context (not just technical stack traces)
|
|
74
|
+
- Configuration-dependent behavior branches
|
|
75
|
+
|
|
76
|
+
**Anti-Patterns**:
|
|
77
|
+
- Logging so verbose it becomes noise
|
|
78
|
+
- Missing correlation IDs across related operations
|
|
79
|
+
- Telemetry that exposes PII or sensitive business data
|
|
80
|
+
|
|
81
|
+
**Specialist Referral**: → **Dean Debug** for telemetry implementation patterns and KQL query strategies
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### 3. Access Modifier Strategy
|
|
86
|
+
|
|
87
|
+
**Why It Matters**: Your `public` and `internal` decisions define your support surface. Everything marked `public` becomes a contract you must maintain. Everything marked `internal` gives you freedom to refactor.
|
|
88
|
+
|
|
89
|
+
**Key Questions**:
|
|
90
|
+
- [ ] Is every public procedure intentionally public, or just "default"?
|
|
91
|
+
- [ ] Are internal implementation details protected from external callers?
|
|
92
|
+
- [ ] Do codeunits have appropriate access modifiers (Public vs Internal)?
|
|
93
|
+
- [ ] Are procedures that partners SHOULD call clearly marked and documented?
|
|
94
|
+
|
|
95
|
+
**Strategic Decisions**:
|
|
96
|
+
- **Public Codeunits/Procedures**: Partner-facing API surface—document, version, maintain
|
|
97
|
+
- **Internal Codeunits/Procedures**: Implementation freedom—refactor without breaking partners
|
|
98
|
+
- **Protected Tables**: Control who can write to your data
|
|
99
|
+
- **Access = Public on Tables/Pages**: Required for extensibility, but increases your contract surface
|
|
100
|
+
|
|
101
|
+
**Anti-Patterns**:
|
|
102
|
+
- Making everything public "just in case"
|
|
103
|
+
- No documentation of what the public API actually is
|
|
104
|
+
- Changing internal behavior that partners somehow depended on
|
|
105
|
+
|
|
106
|
+
**Specialist Referral**: → **Seth Security** for access modifier strategy and encapsulation patterns
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 4. Interface Architecture
|
|
111
|
+
|
|
112
|
+
**Why It Matters**: Interfaces enable dependency inversion—partners can provide implementations you call without tight coupling. This is essential for apps that need pluggable behavior.
|
|
113
|
+
|
|
114
|
+
**Key Questions**:
|
|
115
|
+
- [ ] Do you use interfaces where partner-provided implementations make sense?
|
|
116
|
+
- [ ] Are interfaces well-documented with clear implementation contracts?
|
|
117
|
+
- [ ] Can partners register their implementations without modifying your code?
|
|
118
|
+
- [ ] Is interface discovery and registration straightforward?
|
|
119
|
+
|
|
120
|
+
**Good Interface Candidates**:
|
|
121
|
+
- External system connectors (shipping, payment, tax services)
|
|
122
|
+
- Calculation engines with customer-specific logic
|
|
123
|
+
- Document generation with format variations
|
|
124
|
+
- Notification handlers with channel flexibility
|
|
125
|
+
|
|
126
|
+
**Anti-Patterns**:
|
|
127
|
+
- Interfaces without documentation of expected behavior
|
|
128
|
+
- No default implementation for optional interfaces
|
|
129
|
+
- Registration mechanisms that require insider knowledge
|
|
130
|
+
|
|
131
|
+
**Specialist Referral**: → **Alex Architect** for interface design patterns and facade architecture
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### 5. Upgrade and Migration Considerations
|
|
136
|
+
|
|
137
|
+
**Why It Matters**: Your app will evolve. Partners will have extensions depending on your schema and APIs. Breaking changes have real business impact.
|
|
138
|
+
|
|
139
|
+
**Key Questions**:
|
|
140
|
+
- [ ] Do you have a versioning strategy for your public API?
|
|
141
|
+
- [ ] Are breaking changes communicated before releases?
|
|
142
|
+
- [ ] Do upgrade codeunits handle data migration for schema changes?
|
|
143
|
+
- [ ] Is there a deprecation process for obsolete functionality?
|
|
144
|
+
|
|
145
|
+
**Best Practices**:
|
|
146
|
+
- Use `ObsoleteState` and `ObsoleteReason` to communicate deprecation timeline
|
|
147
|
+
- Provide upgrade codeunits for schema migrations
|
|
148
|
+
- Document breaking changes in release notes
|
|
149
|
+
- Give partners advance notice for significant changes
|
|
150
|
+
|
|
151
|
+
**Specialist Referral**: → **Logan Legacy** for upgrade codeunit patterns and migration strategies
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### 6. Documentation for Partner Success
|
|
156
|
+
|
|
157
|
+
**Why It Matters**: Partners can't use what they can't discover. Documentation transforms your app from "code that works" to "platform partners can build on."
|
|
158
|
+
|
|
159
|
+
**Key Questions**:
|
|
160
|
+
- [ ] Are public APIs documented with usage examples?
|
|
161
|
+
- [ ] Are events discoverable with clear documentation of when they fire?
|
|
162
|
+
- [ ] Is there guidance for common extension scenarios?
|
|
163
|
+
- [ ] Do partners know how to get help when they're stuck?
|
|
164
|
+
|
|
165
|
+
**Documentation Assets**:
|
|
166
|
+
- API reference (procedures, parameters, return values)
|
|
167
|
+
- Event catalog (event, context, typical use cases)
|
|
168
|
+
- Integration guide (common partner scenarios)
|
|
169
|
+
- Troubleshooting guide (common issues and resolution)
|
|
170
|
+
|
|
171
|
+
**Specialist Referral**: → **Taylor Docs** for documentation structure and partner communication
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Analysis Workflow
|
|
176
|
+
|
|
177
|
+
When analyzing an app for partner readiness, work through each category systematically:
|
|
178
|
+
|
|
179
|
+
1. **Inventory**: What events, public APIs, and telemetry currently exist?
|
|
180
|
+
2. **Gap Analysis**: What's missing for each category based on the checklists?
|
|
181
|
+
3. **Priority Assessment**: Which gaps have the highest partner impact?
|
|
182
|
+
4. **Remediation Plan**: What changes are needed, and in what order?
|
|
183
|
+
5. **Specialist Handoffs**: Which specialists should address specific gaps?
|
|
184
|
+
|
|
185
|
+
## Continuous Improvement
|
|
186
|
+
|
|
187
|
+
Partner readiness isn't a one-time checklist—it's an ongoing discipline:
|
|
188
|
+
|
|
189
|
+
- **Gather Feedback**: Listen to partners using your app
|
|
190
|
+
- **Monitor Telemetry**: What operations cause the most issues?
|
|
191
|
+
- **Review Gaps**: After each release, what extension scenarios weren't supported?
|
|
192
|
+
- **Iterate**: Improve event coverage and documentation with each version
|
|
193
|
+
|
|
194
|
+
## Summary
|
|
195
|
+
|
|
196
|
+
AppSource validation asks: "Does this app work?"
|
|
197
|
+
|
|
198
|
+
Partner readiness asks: "Can partners succeed with this app?"
|
|
199
|
+
|
|
200
|
+
The difference is the foundation of a sustainable AppSource business.
|
|
201
|
+
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Partner Readiness Checklist Examples
|
|
2
|
+
|
|
3
|
+
## Event Architecture Review
|
|
4
|
+
|
|
5
|
+
### Good: Well-Designed Event Pattern
|
|
6
|
+
```al
|
|
7
|
+
codeunit 50100 "My Document Processor"
|
|
8
|
+
{
|
|
9
|
+
// Partners can validate before processing
|
|
10
|
+
[IntegrationEvent(false, false)]
|
|
11
|
+
local procedure OnBeforeProcessDocument(var MyDocument: Record "My Document"; var IsHandled: Boolean)
|
|
12
|
+
begin
|
|
13
|
+
end;
|
|
14
|
+
|
|
15
|
+
// Partners can react after processing
|
|
16
|
+
[IntegrationEvent(false, false)]
|
|
17
|
+
local procedure OnAfterProcessDocument(var MyDocument: Record "My Document"; Success: Boolean)
|
|
18
|
+
begin
|
|
19
|
+
end;
|
|
20
|
+
|
|
21
|
+
// Partners can add line-level logic
|
|
22
|
+
[IntegrationEvent(false, false)]
|
|
23
|
+
local procedure OnBeforeProcessDocumentLine(
|
|
24
|
+
var MyDocument: Record "My Document";
|
|
25
|
+
var MyDocumentLine: Record "My Document Line";
|
|
26
|
+
var IsHandled: Boolean)
|
|
27
|
+
begin
|
|
28
|
+
end;
|
|
29
|
+
|
|
30
|
+
procedure ProcessDocument(var MyDocument: Record "My Document")
|
|
31
|
+
var
|
|
32
|
+
IsHandled: Boolean;
|
|
33
|
+
begin
|
|
34
|
+
OnBeforeProcessDocument(MyDocument, IsHandled);
|
|
35
|
+
if IsHandled then
|
|
36
|
+
exit;
|
|
37
|
+
|
|
38
|
+
ProcessLines(MyDocument);
|
|
39
|
+
FinalizeDocument(MyDocument);
|
|
40
|
+
|
|
41
|
+
OnAfterProcessDocument(MyDocument, true);
|
|
42
|
+
end;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Poor: Missing Event Context
|
|
47
|
+
```al
|
|
48
|
+
// ❌ Partners can't do anything useful with this
|
|
49
|
+
[IntegrationEvent(false, false)]
|
|
50
|
+
local procedure OnProcess()
|
|
51
|
+
begin
|
|
52
|
+
end;
|
|
53
|
+
|
|
54
|
+
// ❌ No IsHandled - partners can't prevent default behavior
|
|
55
|
+
[IntegrationEvent(false, false)]
|
|
56
|
+
local procedure OnBeforePost(DocumentNo: Code[20])
|
|
57
|
+
begin
|
|
58
|
+
end;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Telemetry Implementation
|
|
64
|
+
|
|
65
|
+
### Good: Comprehensive Operation Telemetry
|
|
66
|
+
```al
|
|
67
|
+
codeunit 50101 "External Service Connector"
|
|
68
|
+
{
|
|
69
|
+
var
|
|
70
|
+
TelemetryCategory: Label 'MyApp-ExternalService', Locked = true;
|
|
71
|
+
|
|
72
|
+
procedure CallExternalService(DocumentNo: Code[20]): Boolean
|
|
73
|
+
var
|
|
74
|
+
StartTime: DateTime;
|
|
75
|
+
Duration: Duration;
|
|
76
|
+
CustomDimensions: Dictionary of [Text, Text];
|
|
77
|
+
begin
|
|
78
|
+
StartTime := CurrentDateTime();
|
|
79
|
+
|
|
80
|
+
CustomDimensions.Add('DocumentNo', DocumentNo);
|
|
81
|
+
CustomDimensions.Add('Operation', 'CallExternalService');
|
|
82
|
+
CustomDimensions.Add('Endpoint', GetEndpointName());
|
|
83
|
+
|
|
84
|
+
Session.LogMessage(
|
|
85
|
+
'MYAPP-0001',
|
|
86
|
+
'External service call started',
|
|
87
|
+
Verbosity::Normal,
|
|
88
|
+
DataClassification::SystemMetadata,
|
|
89
|
+
TelemetryScope::ExtensionPublisher,
|
|
90
|
+
CustomDimensions);
|
|
91
|
+
|
|
92
|
+
if not TryCallService(DocumentNo) then begin
|
|
93
|
+
Duration := CurrentDateTime() - StartTime;
|
|
94
|
+
CustomDimensions.Add('Duration', Format(Duration));
|
|
95
|
+
CustomDimensions.Add('ErrorMessage', GetLastErrorText());
|
|
96
|
+
|
|
97
|
+
Session.LogMessage(
|
|
98
|
+
'MYAPP-0002',
|
|
99
|
+
'External service call failed',
|
|
100
|
+
Verbosity::Error,
|
|
101
|
+
DataClassification::SystemMetadata,
|
|
102
|
+
TelemetryScope::ExtensionPublisher,
|
|
103
|
+
CustomDimensions);
|
|
104
|
+
exit(false);
|
|
105
|
+
end;
|
|
106
|
+
|
|
107
|
+
Duration := CurrentDateTime() - StartTime;
|
|
108
|
+
CustomDimensions.Add('Duration', Format(Duration));
|
|
109
|
+
|
|
110
|
+
Session.LogMessage(
|
|
111
|
+
'MYAPP-0003',
|
|
112
|
+
'External service call completed',
|
|
113
|
+
Verbosity::Normal,
|
|
114
|
+
DataClassification::SystemMetadata,
|
|
115
|
+
TelemetryScope::ExtensionPublisher,
|
|
116
|
+
CustomDimensions);
|
|
117
|
+
|
|
118
|
+
exit(true);
|
|
119
|
+
end;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Poor: Insufficient Telemetry
|
|
124
|
+
```al
|
|
125
|
+
// ❌ No telemetry at all for external call
|
|
126
|
+
procedure CallExternalService(DocumentNo: Code[20]): Boolean
|
|
127
|
+
begin
|
|
128
|
+
exit(TryCallService(DocumentNo));
|
|
129
|
+
end;
|
|
130
|
+
|
|
131
|
+
// ❌ Telemetry without context
|
|
132
|
+
Session.LogMessage('MYAPP-0001', 'Service called', Verbosity::Normal,
|
|
133
|
+
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Access Modifier Strategy
|
|
139
|
+
|
|
140
|
+
### Good: Intentional Access Control
|
|
141
|
+
```al
|
|
142
|
+
// Public codeunit - this is the partner-facing API
|
|
143
|
+
codeunit 50102 "My Document API"
|
|
144
|
+
{
|
|
145
|
+
Access = Public;
|
|
146
|
+
|
|
147
|
+
// Public procedure - documented contract for partners
|
|
148
|
+
procedure CreateDocument(CustomerNo: Code[20]): Code[20]
|
|
149
|
+
var
|
|
150
|
+
DocProcessor: Codeunit "My Document Processor Internal";
|
|
151
|
+
begin
|
|
152
|
+
exit(DocProcessor.CreateDocumentInternal(CustomerNo));
|
|
153
|
+
end;
|
|
154
|
+
|
|
155
|
+
// Public procedure - partners can call this
|
|
156
|
+
procedure GetDocumentStatus(DocumentNo: Code[20]): Enum "My Document Status"
|
|
157
|
+
begin
|
|
158
|
+
exit(GetStatusInternal(DocumentNo));
|
|
159
|
+
end;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Internal codeunit - implementation details hidden from partners
|
|
163
|
+
codeunit 50103 "My Document Processor Internal"
|
|
164
|
+
{
|
|
165
|
+
Access = Internal;
|
|
166
|
+
|
|
167
|
+
// Internal procedure - free to refactor
|
|
168
|
+
procedure CreateDocumentInternal(CustomerNo: Code[20]): Code[20]
|
|
169
|
+
begin
|
|
170
|
+
// Implementation that can change without breaking partners
|
|
171
|
+
end;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Poor: Unintentional Public Surface
|
|
176
|
+
```al
|
|
177
|
+
// ❌ Everything public by default - massive support surface
|
|
178
|
+
codeunit 50104 "My Codeunit"
|
|
179
|
+
{
|
|
180
|
+
// All these become partner contracts accidentally
|
|
181
|
+
procedure DoTheThing()
|
|
182
|
+
procedure HelperMethod1()
|
|
183
|
+
procedure HelperMethod2()
|
|
184
|
+
procedure InternalCalculation()
|
|
185
|
+
procedure DebugOutput()
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Interface Architecture
|
|
192
|
+
|
|
193
|
+
### Good: Well-Documented Interface Pattern
|
|
194
|
+
```al
|
|
195
|
+
// Clear interface contract
|
|
196
|
+
interface "IShipping Provider"
|
|
197
|
+
{
|
|
198
|
+
/// <summary>
|
|
199
|
+
/// Calculates shipping rate for the given shipment parameters.
|
|
200
|
+
/// </summary>
|
|
201
|
+
/// <param name="Weight">Total weight in base unit of measure</param>
|
|
202
|
+
/// <param name="DestinationCountry">ISO country code</param>
|
|
203
|
+
/// <returns>Shipping cost in LCY</returns>
|
|
204
|
+
procedure CalculateRate(Weight: Decimal; DestinationCountry: Code[10]): Decimal;
|
|
205
|
+
|
|
206
|
+
/// <summary>
|
|
207
|
+
/// Creates a shipping label and returns the tracking number.
|
|
208
|
+
/// </summary>
|
|
209
|
+
/// <param name="ShipmentNo">The shipment document number</param>
|
|
210
|
+
/// <returns>Carrier tracking number</returns>
|
|
211
|
+
procedure CreateLabel(ShipmentNo: Code[20]): Text[50];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Default implementation partners can reference
|
|
215
|
+
codeunit 50110 "Default Shipping Provider" implements "IShipping Provider"
|
|
216
|
+
{
|
|
217
|
+
procedure CalculateRate(Weight: Decimal; DestinationCountry: Code[10]): Decimal
|
|
218
|
+
begin
|
|
219
|
+
// Default flat-rate calculation
|
|
220
|
+
exit(Weight * 0.50);
|
|
221
|
+
end;
|
|
222
|
+
|
|
223
|
+
procedure CreateLabel(ShipmentNo: Code[20]): Text[50]
|
|
224
|
+
begin
|
|
225
|
+
// Default - no label creation
|
|
226
|
+
exit('');
|
|
227
|
+
end;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Registration mechanism
|
|
231
|
+
table 50100 "Shipping Provider Setup"
|
|
232
|
+
{
|
|
233
|
+
fields
|
|
234
|
+
{
|
|
235
|
+
field(1; "Primary Key"; Code[10]) { }
|
|
236
|
+
field(2; "Provider"; Enum "Shipping Provider") { }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
enum 50100 "Shipping Provider" implements "IShipping Provider"
|
|
241
|
+
{
|
|
242
|
+
Extensible = true;
|
|
243
|
+
|
|
244
|
+
value(0; Default)
|
|
245
|
+
{
|
|
246
|
+
Implementation = "IShipping Provider" = "Default Shipping Provider";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Partner Readiness Summary Report Template
|
|
254
|
+
|
|
255
|
+
```al
|
|
256
|
+
// Generate a readiness report for your app
|
|
257
|
+
codeunit 50199 "Partner Readiness Report"
|
|
258
|
+
{
|
|
259
|
+
procedure GenerateReport()
|
|
260
|
+
var
|
|
261
|
+
Report: TextBuilder;
|
|
262
|
+
begin
|
|
263
|
+
Report.AppendLine('# Partner Readiness Report');
|
|
264
|
+
Report.AppendLine('Generated: ' + Format(CurrentDateTime()));
|
|
265
|
+
Report.AppendLine('');
|
|
266
|
+
|
|
267
|
+
Report.AppendLine('## Event Coverage');
|
|
268
|
+
Report.AppendLine('- Integration Events: ' + Format(CountIntegrationEvents()));
|
|
269
|
+
Report.AppendLine('- Business Events: ' + Format(CountBusinessEvents()));
|
|
270
|
+
Report.AppendLine('');
|
|
271
|
+
|
|
272
|
+
Report.AppendLine('## Access Modifier Summary');
|
|
273
|
+
Report.AppendLine('- Public Codeunits: ' + Format(CountPublicCodeunits()));
|
|
274
|
+
Report.AppendLine('- Internal Codeunits: ' + Format(CountInternalCodeunits()));
|
|
275
|
+
Report.AppendLine('');
|
|
276
|
+
|
|
277
|
+
Report.AppendLine('## Telemetry Coverage');
|
|
278
|
+
Report.AppendLine('- LogMessage Calls: ' + Format(CountTelemetryCalls()));
|
|
279
|
+
Report.AppendLine('');
|
|
280
|
+
|
|
281
|
+
Report.AppendLine('## Interface Definitions');
|
|
282
|
+
Report.AppendLine('- Interfaces: ' + Format(CountInterfaces()));
|
|
283
|
+
Report.AppendLine('- Implementations: ' + Format(CountImplementations()));
|
|
284
|
+
|
|
285
|
+
Message(Report.ToText());
|
|
286
|
+
end;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Isolation Testing Patterns with Test Doubles"
|
|
3
|
+
domain: "quinn-tester"
|
|
4
|
+
difficulty: "advanced"
|
|
5
|
+
bc_versions: "18+"
|
|
6
|
+
tags: ["testing", "isolation", "test-doubles", "mocks", "spies", "dependency-injection", "unit-testing"]
|
|
7
|
+
samples: "samples/isolation-testing-patterns.md"
|
|
8
|
+
related_topics:
|
|
9
|
+
- "../alex-architect/testability-design-patterns.md"
|
|
10
|
+
- "../roger-reviewer/testability-code-smells.md"
|
|
11
|
+
source: "Adapted from Vjeko.com: Testing in isolation (Dec 2023)"
|
|
12
|
+
---
|
|
13
|
+
# Isolation Testing Patterns with Test Doubles
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
Testing in isolation means testing your code independent from its dependencies. When code is designed with interfaces and dependency injection, you substitute real implementations with "test doubles"—fake implementations giving complete control during testing.
|
|
18
|
+
|
|
19
|
+
**Core Principle**: Test YOUR code, not your dependencies. Use test doubles to isolate what you're testing from what you're depending on.
|
|
20
|
+
|
|
21
|
+
## Types of Test Doubles
|
|
22
|
+
|
|
23
|
+
### Dummy
|
|
24
|
+
A placeholder passed but never used. Satisfies parameter requirements when the dependency isn't exercised in your test path.
|
|
25
|
+
|
|
26
|
+
### Stub
|
|
27
|
+
Returns predetermined values. Provides canned answers to calls made during test. Use when you need the dependency to return specific values.
|
|
28
|
+
|
|
29
|
+
### Spy
|
|
30
|
+
Records what happened during execution. Allows assertions about how dependencies were called—was it invoked? With what parameters?
|
|
31
|
+
|
|
32
|
+
### Mock
|
|
33
|
+
Pre-programmed with expectations. Controls dependency behavior AND verifies interaction patterns.
|
|
34
|
+
|
|
35
|
+
### Throwing Double
|
|
36
|
+
Simulates errors. Use for testing error handling paths when dependencies fail.
|
|
37
|
+
|
|
38
|
+
## When to Use Each
|
|
39
|
+
|
|
40
|
+
| Scenario | Double | Why |
|
|
41
|
+
|----------|--------|-----|
|
|
42
|
+
| Dependency not used in test path | Dummy | Just satisfies signature |
|
|
43
|
+
| Need specific return value | Stub | Control the response |
|
|
44
|
+
| Need to verify it was called | Spy | Record and inspect |
|
|
45
|
+
| Need to control AND verify | Mock | Full control |
|
|
46
|
+
| Dependency might throw errors | Throwing | Test error handling |
|
|
47
|
+
|
|
48
|
+
## Testing Patterns
|
|
49
|
+
|
|
50
|
+
**Happy Path**: Permission granted, conversion succeeds, logging occurs. Use stub for converter, spy for logger.
|
|
51
|
+
|
|
52
|
+
**Permission Denial**: Permission denied, process fails before conversion. Use dummy converter (won't be called), spy to verify no logging.
|
|
53
|
+
|
|
54
|
+
**Parameter Verification**: Verify dependencies receive correct parameters. Use spy that captures arguments for assertion.
|
|
55
|
+
|
|
56
|
+
**Error Propagation**: Converter fails, error propagates correctly. Use throwing double, verify logger not invoked.
|
|
57
|
+
|
|
58
|
+
## Structuring Test Doubles
|
|
59
|
+
|
|
60
|
+
**Naming**: `[Type] [Interface Name]` - e.g., "Spy Logger", "Stub Converter"
|
|
61
|
+
|
|
62
|
+
**Organization**: Keep test doubles in test app, organized by interface they implement.
|
|
63
|
+
|
|
64
|
+
## Test Production Implementations Separately
|
|
65
|
+
|
|
66
|
+
Test your actual implementations (database permission checker, database logger) directly with simple, focused tests. Then use test doubles when testing business logic that USES those implementations.
|
|
67
|
+
|
|
68
|
+
## Benefits
|
|
69
|
+
|
|
70
|
+
- **Speed**: No database = fast tests (hundreds in seconds)
|
|
71
|
+
- **Reliability**: No external dependencies = no flaky tests
|
|
72
|
+
- **Focus**: Test exactly what you mean to test
|
|
73
|
+
- **Maintainability**: Dependency changes don't break business logic tests
|
|
74
|
+
|
|
75
|
+
## When You Still Need Integration Tests
|
|
76
|
+
|
|
77
|
+
Isolation tests verify your code works given certain dependency behaviors. Integration tests verify dependencies are wired correctly and end-to-end scenarios function.
|
|
78
|
+
|
|
79
|
+
**Rule**: Many isolation tests, few integration tests. The pyramid, not the ice cream cone.
|
|
80
|
+
|
|
81
|
+
See samples for complete test double implementations and test codeunit examples.
|
|
82
|
+
|