10.3 SysTest Framework & Test Explorer
Key Takeaways
- The SysTest framework is the native X++ automated testing harness, built upon the SysTestCase base class and the [SysTestMethodAttribute] attribute.
- Individual test methods use assertion methods (such as this.assertEquals(), this.assertTrue(), and this.assertNotNull()) to validate business logic results.
- Test lifecycle methods setUp() and tearDown() execute before and after each individual test method, while setUpTestCase() and tearDownTestCase() execute once per test class.
- Visual Studio Test Explorer automatically discovers SysTestCase classes, allowing developers to execute, group, and debug test cases with breakpoints in X++ code.
- Developer Recordings created via Task Recorder can be exported as XML and converted into automated test code or integrated into CI build pipelines using Azure DevOps VSTest.
10.3 SysTest Framework & Test Explorer
Quick Answer: The SysTest framework is the automated unit and functional testing engine built directly into the X++ language and runtime. Developers author unit tests by extending the
SysTestCasebase class and decorating test methods with[SysTestMethodAttribute]. Test fixtures are managed through lifecycle methods:setUp()executes before each test method, andtearDown()executes immediately following it. Assertions are evaluated using methods such asthis.assertEquals(),this.assertTrue(), andthis.assertNotNull(). Tests are executed and debugged locally using Visual Studio Test Explorer. Furthermore, functional tests recorded via the client Task Recorder can be exported as developer XML recordings to generate automated test code, which is executed unattended in Azure DevOps CI/CD build pipelines using the VSTest runner task.
1. SysTest Architecture and SysTestCase Anatomy
Continuous quality assurance in Dynamics 365 development relies on automated regression testing. Rather than performing manual smoke tests after every package build, developers write automated test suites using the SysTest framework.
SysTestCase Lifecycle and Test Execution Order
┌────────────────────────────────────────────────────────┐
│ setUpTestCase() [Runs once per class] │
└───────────────────────────┬────────────────────────────┘
│
┌──────────────────┴──────────────────┐
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ setUp() [Runs before Test 1] │ │ setUp() [Runs before Test 2] │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ [SysTestMethodAttribute] │ │ [SysTestMethodAttribute] │
│ testCalculateDiscount() │ │ testCreditLimitExceeded() │
│ • this.assertEquals(...) │ │ • this.assertTrue(...) │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ tearDown() [Runs after Test 1│ │ tearDown() [Runs after Test 2│
└──────────────────────────────┘ └──────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ tearDownTestCase() [Runs once per class] │
└────────────────────────────────────────────────────────┘
Core Rules for SysTest Classes
- Base Class: The test class must extend
SysTestCase. - Method Attribute: Every executable test method must be decorated with
[SysTestMethodAttribute]. - Method Visibility: Test methods must be declared as
public void. - Naming Convention: Method names typically start with
test...(e.g.,testOrderTotalCalculation). - Category Attributes: Test classes or methods can be tagged with
[SysTestPriority(1)]and[SysTestCategory('CreditLimit')]for filtering in Test Explorer.
[SysTestCategory('SalesPricing')]
public class SalesPricingEngineTest extends SysTestCase
{
private SalesTable salesTable;
public void setUp()
{
super();
// Establish test fixtures prior to each test method
salesTable.clear();
salesTable.SalesId = 'TEST-0001';
salesTable.CustAccount = 'US-001';
salesTable.CurrencyCode = 'USD';
salesTable.initValue();
}
public void tearDown()
{
// Clean up test data and reset environmental parameters
salesTable.clear();
super();
}
[SysTestMethodAttribute]
public void testDiscountThresholdCalculation()
{
SalesLine salesLine;
salesLine.clear();
salesLine.SalesId = salesTable.SalesId;
salesLine.LineAmount = 1000.00;
AmountMST calculatedDiscount = SalesDiscountEngine::calculate(salesLine);
// Assert expected outcomes
this.assertEquals(100.00, calculatedDiscount, "Standard 10% discount should apply to $1000 lines.");
}
[SysTestMethodAttribute]
public void testZeroQuantityLineValidation()
{
SalesLine salesLine;
salesLine.SalesQty = 0;
boolean isValid = salesLine.validateWrite();
this.assertFalse(isValid, "Sales lines with zero quantity must fail validation.");
}
}
2. Assertion Methods and Exception Testing
The SysTestCase class provides rich assertion methods to evaluate expectations against actual runtime values:
| Assertion Method | Signature | Purpose |
|---|---|---|
assertEquals | this.assertEquals(expected, actual, [message]) | Asserts that two primitives or objects are equal |
assertNotEquals | this.assertNotEquals(expected, actual, [message]) | Asserts that two values are not equal |
assertTrue | this.assertTrue(condition, [message]) | Asserts that a boolean expression evaluates to true |
assertFalse | this.assertFalse(condition, [message]) | Asserts that a boolean expression evaluates to false |
assertNotNull | this.assertNotNull(object, [message]) | Asserts that an object reference or buffer is not null |
assertNull | this.assertNull(object, [message]) | Asserts that an object reference is null |
fail | this.fail(message) | Explicitly forces the test to fail with an explanatory message |
Testing Expected Exceptions
When verifying that invalid input correctly throws an exception, wrap the call in a try/catch block and fail if the exception is not raised:
[SysTestMethodAttribute]
public void testCreditLimitViolationThrowsError()
{
try
{
CreditManager::postInvoiceExceedingLimit(salesTable, 500000.00);
this.fail("Expected an error exception due to credit limit violation, but none was thrown.");
}
catch (Exception::Error)
{
// Test passes: expected error was caught
this.assertTrue(true);
}
}
3. Test Isolation, Transaction Rollbacks, and Mocking
A critical challenge in ERP unit testing is test isolation. If a unit test commits real transactional records to the database during its execution, subsequent test runs will fail due to primary key collisions or contaminated state.
Isolation Techniques
- Explicit Rollback with
ttsabort: Wrap test data generation inside attsbeginblock and issuettsabortin thetearDown()method to revert all database inserts. - Isolated Test Companies: Execute tests inside dedicated ephemeral test legal entities using
changecompany('DAT')or custom test companies. - Mock Objects and Interfaces: Instead of calling real external banking or shipping web services, inject mock classes implementing shared business interfaces.
public void testPaymentGatewayWithMock()
{
// Instantiate mock service that simulates external HTTP 200 response
IPaymentGateway mockGateway = new MockPaymentGatewaySuccess();
PaymentProcessor processor = new PaymentProcessor(mockGateway);
boolean result = processor.processPayment(salesTable, 250.00);
this.assertTrue(result, "Payment processing must succeed when gateway returns approved status.");
}
4. Visual Studio Test Explorer Integration
Visual Studio includes a native Test Explorer window (Test -> Test Explorer) that integrates directly with the X++ compiler and AOS metadata.
Visual Studio Test Explorer Workflow
┌────────────────────────────────────────────────────────┐
│ Build Model / Solution in Visual Studio │
└───────────────────────────┬────────────────────────────┘
│ Auto-discovery
▼
┌────────────────────────────────────────────────────────┐
│ Visual Studio Test Explorer Window │
│ ├── Group by: Project, Class, Category, Priority │
│ ├── Displays all [SysTestMethodAttribute] methods │
└──────────────┬──────────────────────────────┬──────────┘
│ Run Selected Tests │ Debug Selected Tests
▼ ▼
┌──────────────────────────────┐ ┌────────────────────────────┐
│ VSTest Execution Engine │ │ Visual Studio Debugger │
│ • Runs tests on local AOS │ │ • Hits X++ breakpoints │
│ • Reports Pass/Fail status │ │ • Inspects local variables│
└──────────────────────────────┘ └────────────────────────────┘
Test Explorer Capabilities
- Automatic Test Discovery: When a project containing
SysTestCaseclasses is built, Test Explorer scans the assembly metadata and populates the test tree. - Grouping and Filtering: Group tests by Traits (
[SysTestCategory]), Class, or Hierarchy. - Interactive Debugging: Right-click any test method and select Debug Selected Tests. The IDE attaches to the local AOS process (
iisexpress.exeorw3wp.exe) and hits breakpoints directly inside test methods or underlying business logic.
5. Task Recorder Integration and CI/CD Azure DevOps Pipelines
Beyond hand-coded unit tests, Dynamics 365 allows functional analysts to capture business processes using the client Task Recorder and export them for automated test engineering.
Testing Methodology Comparison
| Testing Methodology | Framework / Tool | Execution Layer | Data Persistence Strategy | Pipeline Integration |
|---|---|---|---|---|
| X++ Unit Testing | SysTestCase / SysTest Framework | Code-level execution on AOS via Visual Studio / VSTest | Transactional rollback (ttsabort) or tearDown() cleanup | VSTest@2 task running *Test*.dll assemblies |
| Developer Recordings | Task Recorder (XML export) | Browser UI recording capturing form control interactions | Recorded against standard test data environments | Generated X++ test cases or RSAT test parameter files |
| Functional Acceptance (RSAT) | Regression Suite Automation Tool + Selenium | End-to-end web browser automation against active AOS | Environment refresh or test customer/vendor master data | RSAT Azure DevOps pipeline tasks driving Excel-based parameters |
| Integration Testing | Postman / SoapUI / C# Console | OData REST / Custom Service JSON endpoints | Staged test datasets via Data Management Framework (DMF) | PowerShell scripts invoking external endpoint collections |
Developer Recordings
- In the D365 web client, start Task Recorder and execute the business process (e.g., creating and confirming a Purchase Order).
- Stop the recording and select Save as developer recording.
- The browser exports an XML file containing all form controls, user inputs, and validation assertions.
- In Visual Studio, developers import this recording to automatically generate executable X++ test classes or integrate them with the Regression Suite Automation Tool (RSAT).
Azure DevOps CI Build Pipeline Execution
In enterprise DevOps pipelines, automated tests execute during the nightly build or PR validation pipeline using the Visual Studio Test (VSTest) build task.
# Azure DevOps Pipeline snippet executing SysTest suites
- task: VSTest@2
displayName: 'Execute Automated SysTest Unit Tests'
inputs:
testSelector: 'testAssemblies'
testAssemblyVer2: |
**\*Test*.dll
!**\*TestAdapter*.dll
searchFolder: '$(System.DefaultWorkingDirectory)\bin'
testFiltercriteria: 'TestCategory=SalesPricing | Priority=1'
resultsFolder: '$(Agent.TempDirectory)\TestResults'
runInParallel: false
Pipeline Results
- The VSTest runner executes all tests matching the filter criteria.
- Results are captured in
.trx(XML) format. - The pipeline displays a Tests tab showing pass/fail percentages, execution duration, and stack traces for failed assertions, enabling automated build quality gates that reject failing pull requests.
6. Scenario Walk-Through: Order Credit Limit Validation Suite
Business Scenario
A distribution company requires that when an order is entered for a customer on credit hold (Blocked == CustVendorBlocked::All), SalesTable.validateWrite() must reject the order and return false. A developer must write a unit test to verify this rule and configure it to run in the nightly CI build pipeline.
Technical Implementation Walkthrough
- Author the Test Class: Extend
SysTestCaseand decorate with category[SysTestCategory('CreditHoldValidation')]. - Implement
setUp(): Create an in-memory customer buffer withBlocked = CustVendorBlocked::All. - Implement the Test Method:
[SysTestMethodAttribute] public void testBlockedCustomerOrderFailsValidation() { CustTable custTable; custTable.AccountNum = 'TEST_BLOCK_01'; custTable.Blocked = CustVendorBlocked::All; SalesTable sales; sales.clear(); sales.SalesId = 'SO_TEST_99'; sales.CustAccount = custTable.AccountNum; boolean validationResult = sales.validateWrite(); this.assertFalse(validationResult, "Orders for blocked customers must fail validateWrite."); } - Local Verification: Run the test in Visual Studio Test Explorer. Verify the test displays a green checkmark.
- DevOps Integration: Commit to Git/TFVC. The build pipeline executes
VSTest@2, filters byTestCategory=CreditHoldValidation, and records passing results in Azure DevOps.
7. Real-World Exam Traps: SysTest & Test Explorer
[!WARNING] Exam Trap 1: Omitting
[SysTestMethodAttribute]A developer creates a public method inside aSysTestCaseclass, but the method never appears in Visual Studio Test Explorer. The candidate is asked why. Test Explorer uses reflection to discover tests; any method lacking[SysTestMethodAttribute]is completely ignored by the test discovery engine.
[!WARNING] Exam Trap 2: Persistent Database Contamination Between Test Runs Inserting transactional data inside a test method without cleaning up in
tearDown()or rolling back via transactional aborts leaves permanent test records in the development database. When the test runs a second time, it throws duplicate key exceptions. Tests must be isolated and idempotent.
[!WARNING] Exam Trap 3: Confusing RSAT vs. SysTest Execution Engines RSAT (Regression Suite Automation Tool) executes end-to-end user interface tests via Selenium and POS against an active environment using Excel parameter files. In contrast,
SysTestCaseis an X++ code-level unit testing framework executed by the VSTest runner directly on the AOS. Do not select RSAT when the requirement specifies X++ class-level unit testing.
[!WARNING] Exam Trap 4: Hardcoding Environment-Specific RecIds in Test Code Writing tests that query specific hardcoded
RecIdnumbers (e.g.,CustTable::find(5637144576)) causes immediate failures in CI build environments where data is generated dynamically. Always create test data programmatically or use standard test data fixtures.
A developer writes a custom test class extending SysTestCase to test sales tax calculation logic. The developer wants to ensure that a fresh, uncommitted customer buffer is prepared before each individual test method runs, and that all test records are cleaned up after each test method completes. Which pair of SysTestCase lifecycle methods should be overridden?
An enterprise development team is setting up an automated Continuous Integration (CI) build pipeline in Azure DevOps for their Dynamics 365 Finance and Operations codebase. Which build pipeline task should be configured to discover and execute automated X++ unit test assemblies (.dll) and publish test run results?
A developer authors several new test methods inside a class extending SysTestCase. However, after compiling the solution in Visual Studio, none of the newly authored test methods appear in the Visual Studio Test Explorer window. What is the most likely cause of this issue?
To prevent automated unit tests from contaminating production or shared development databases with dummy transaction data, how should test data isolation and cleanup typically be managed inside an X++ SysTestCase?