How to create unit test
How to create a unit test for Dynamics AX Dynamics 365 for Finance and Operation (D365FO)?
There’s a simple pattern called AAA - Arrange, Act, Assert.
Arrange: Create the data and parameters you need to execute the code you want to test.
Act: Call the code you want to test.
Assert: Assert if the result is the expected.
I will show bellow how to create a unit test following this pattern and using SysTest framework.
SysTest Framework
In a nutshell: - Any data you generate during the execution will not persist. - Provide assertion methods. - You can execute your tests in a build pipeline.
Example
You want to create a new method in the CustTable to verify if the customer is active. Customer will be active if:
- Has any sales orders in the last 2 months
- Has any invoice in the last 2 months
- Has any payment in the last 2 months
Ignore the relevance, from business point of view, of the requirements. The important here is the approach.
Ok, first lets create the basic of our new method.
/// <summary>
/// Extension of the table <c>CustTable</c>.
/// </summary>
[ExtensionOf(tableStr(CustTable))]
final class MFCustTable_Extension
{
/// <summary>
/// Returns if the customer is active.
/// </summary>
/// <returns>True if customer is active, false otherwise.</returns>
public boolean isActive()
{
boolean ret;
return ret;
}
Notice, there’s no logic yet. The point here is to be able to compile. Now, let’s create our tests.
- Create a separated model for your Unit Tests
- Create a new class extending SysTestCase
- Use the attribute SysTestTargetAttribute to identify the code you are testing
/// <summary>
/// Unit test for <c>MFCustTable_Extension</c>
/// </summary>
[SysTestTargetAttribute(classStr(MFCustTable_Extension_Test))]
class MFCustTable_Extension_Test extends SysTestCase
{
}
To create a test, create a new method public void with the name starting with test. You can also use the attribute SysTestMethodAttribute to indicate it is a test.
Use the attribute SysTestGranularityAttribute to indicate what type of test it is.
First test:
Given a customer with a sales order created within last 2 months, we expect when we call the method isActive() the method will return true.
Let’s create the method first:
/// <summary>
/// Given A customer with a sales order created within last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerActiveWithSalesOrder()
{
//arrange
//act
//assert
}
Now, in the arrange we need a customer, and a sales order.
In the act we have to call our new method.
In the assert, we user the method assertTrue to validate the return of our new method.
That’s our first test complete
/// <summary>
/// Unit test for <c>MFCustTable_Extension</c>
/// </summary>
[SysTestTargetAttribute(classStr(MFCustTable_Extension_Test))]
class MFCustTable_Extension_Test extends SysTestCase
{
/// <summary>
/// Given A customer with a sales order created within last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerActiveWithSalesOrder()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createSalesTable(custAccount);
//act
boolean ret = custTable.isActive();
//assert
this.assertTrue(ret, "Customer should be active");
}
private CustTable createCustomer(AccountNum _accountNum)
{
CustTable custTable;
custTable.AccountNum = _accountNum;
custTable.doInsert();
return custTable;
}
private SalesTable createSalesTable(AccountNum _custAccount)
{
SalesTable salesTable;
salesTable.CustAccount = _custAccount;
salesTable.doInsert();
return salesTable;
}
}
In visual studio, open the test explorer in the menu: Test > Windows > Test explorer. Build your solution, and you should see your tests. Right click and select Run selected tests. Your test should fail.
Notice, the string you use in the assert method is used to indicate what has failed. This can help to identify and give some context on what is not working.
Cool, we can implement our method now:
/// <summary>
/// Extension of the table <c>CustTable</c>.
/// </summary>
[ExtensionOf(tableStr(CustTable))]
final class MFCustTable_Extension
{
/// <summary>
/// Returns if the customer is active.
/// </summary>
/// <returns>True if customer is active, false otherwise.</returns>
public boolean isActive()
{
boolean ret;
utcdatetime dateFrom = DateTimeUtil::addMonths(DateTimeUtil::getSystemDateTime(), -2);
utcdatetime dateTo = DateTimeUtil::getSystemDateTime();
ret = this.hasSalesOrder(dateFrom, dateTo);
return ret;
}
private boolean hasSalesOrder(utcdatetime _dateFrom, utcdatetime _dateTo)
{
SalesTable salesTable;
boolean ret;
select firstonly RecId from salesTable
where salesTable.custAccount == this.AccountNum
&& salesTable.CreatedDateTime >= _dateFrom
&& salesTable.CreatedDateTime <= _dateTo;
if (salesTable.RecId)
{
ret = true;
}
return ret;
}
If you build your solution and run your test it should pass now.
Bellow is the final example of all unit tests implemented:
/// <summary>
/// Unit test for <c>MFCustTable_Extension</c>
/// </summary>
[SysTestTargetAttribute(classStr(MFCustTable_Extension_Test))]
class MFCustTable_Extension_Test extends SysTestCase
{
/// <summary>
/// Given A customer with a sales order created within last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerActiveWithSalesOrder()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createSalesTable(custAccount);
//act
boolean ret = custTable.MFisActive();
//assert
this.assertTrue(ret, "Customer should be active");
}
/// <summary>
/// Given A customer with a sales order created before last 2 months.
/// When Check if active
/// Then Is not active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerNotActiveWithSalesOrder()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createSalesTable(custAccount, DateTimeUtil::addMonths(DateTimeUtil::getSystemDateTime(), -3));
//act
boolean ret = custTable.MFisActive();
//assert
this.assertFalse(ret, "Customer should not be active");
}
/// <summary>
/// Given A customer with a custInvoiceJour created within last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerActiveWithInvoiceJour()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createCustInvoiceJour(custAccount);
//act
boolean ret = custTable.MFisActive();
//assert
this.assertTrue(ret, "Customer should be active");
}
/// <summary>
/// Given A customer with a custInvoiceJour created before last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerNotActiveWithInvoiceJour()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createCustInvoiceJour(custAccount, DateTimeUtil::date(DateTimeUtil::addMonths(DateTimeUtil::getSystemDateTime(), -3)));
//act
boolean ret = custTable.MFisActive();
//assert
this.assertFalse(ret, "Customer should not be active");
}
/// <summary>
/// Given A customer with a settlement created within last 2 months.
/// When Check if active
/// Then Is active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerActiveWithSettlement()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createCustSettlement(custAccount);
//act
boolean ret = custTable.MFisActive();
//assert
this.assertTrue(ret, "Customer should be active");
}
/// <summary>
/// Given A customer with a settlement created before last 2 months.
/// When Check if active
/// Then Is not active
/// </summary>
[SysTestMethodAttribute,
SysTestGranularityAttribute(SysTestGranularity::Unit)]
public void testCustomerNotActiveWithSettlement()
{
//arrange
const str custAccount = '0001';
CustTable custTable = this.createCustomer(custAccount);
this.createCustSettlement(custAccount, DateTimeUtil::date(DateTimeUtil::addMonths(DateTimeUtil::getSystemDateTime(), -3)));
//act
boolean ret = custTable.MFisActive();
//assert
this.assertFalse(ret, "Customer should not be active");
}
private CustTable createCustomer(AccountNum _accountNum)
{
CustTable custTable;
custTable.AccountNum = _accountNum;
custTable.doInsert();
return custTable;
}
private SalesTable createSalesTable(AccountNum _custAccount, utcdatetime _createdDateTime = DateTimeUtil::getSystemDateTime())
{
SalesTable salesTable;
new OverwriteSystemfieldsPermission().assert();
salesTable.overwriteSystemfields(true);
salesTable.CustAccount = _custAccount;
salesTable.(fieldNum(SalesTable, CreatedDateTime)) = _createdDateTime;
salesTable.doInsert();
return salesTable;
}
private CustInvoiceJour createCustInvoiceJour(AccountNum _invoiceAccount, InvoiceDate _invoiceDate = today())
{
CustInvoiceJour custInvoiceJour;
custInvoiceJour.InvoiceAccount = _invoiceAccount;
custInvoiceJour.InvoiceDate = _invoiceDate;
custInvoiceJour.doInsert();
return custInvoiceJour;
}
private CustSettlement createCustSettlement(AccountNum _custAccount, TransDate _transdate = today())
{
CustSettlement custSettlement;
custSettlement.AccountNum = _custAccount;
custSettlement.TransDate = _transdate;
custSettlement.doInsert();
return custSettlement;
}
}
Final implementation of the Active method:
/// <summary>
/// Extension of the table <c>CustTable</c>.
/// </summary>
[ExtensionOf(tableStr(CustTable))]
final class MFCustTable_Extension
{
/// <summary>
/// Returns if the customer is active.
/// </summary>
/// <returns>True if customer is active, false otherwise.</returns>
public boolean MFisActive()
{
boolean ret;
utcdatetime dateFrom = DateTimeUtil::addMonths(DateTimeUtil::getSystemDateTime(), -2);
utcdatetime dateTo = DateTimeUtil::getSystemDateTime();
ret = this.hasSalesOrder(dateFrom, dateTo);
ret = ret || this.hasCustInvoiceJour(DateTimeUtil::date(dateFrom), DateTimeUtil::date(dateTo));
ret = ret || this.hasSettlement(DateTimeUtil::date(dateFrom), DateTimeUtil::date(dateTo));
return ret;
}
private boolean hasSalesOrder(utcdatetime _dateFrom, utcdatetime _dateTo)
{
SalesTable salesTable;
boolean ret;
select firstonly RecId from salesTable
where salesTable.custAccount == this.AccountNum
&& salesTable.CreatedDateTime >= _dateFrom
&& salesTable.CreatedDateTime <= _dateTo;
if (salesTable.RecId)
{
ret = true;
}
return ret;
}
private boolean hasCustInvoiceJour(InvoiceDate _dateFrom, InvoiceDate _dateTo)
{
CustInvoiceJour custInvoiceJour;
boolean ret;
select firstonly RecId from custInvoiceJour
where custInvoiceJour.InvoiceAccount == this.AccountNum
&& custInvoiceJour.InvoiceDate >= _dateFrom
&& custInvoiceJour.InvoiceDate <= _dateTo;
if (custInvoiceJour.RecId)
{
ret = true;
}
return ret;
}
private boolean hasSettlement(TransDate _dateFrom, TransDate _dateTo)
{
CustSettlement custSettlement;
boolean ret;
select firstonly RecId from custSettlement
where custSettlement.AccountNum == this.AccountNum
&& custSettlement.TransDate >= _dateFrom
&& custSettlement.TransDate <= _dateTo;
if (custSettlement.RecId)
{
ret = true;
}
return ret;
}
}
What you need to know?
- Each test is unique and not dependent on any other test.
- Keep it simple! Minimize the scope of your test. If you have to test multiple things break it in multiple tests.
- Keep it readable! Choose meaningful names. Prioritize the readability, not reusability of the code.
- A unit test will run in DAT company.
- You can run in a different company using the attribute SysTestCaseDataDependencyAttribute(“DAT”) in the class (not in the method).
- You can use the attribute SysTestCaseNumSeqModuleDependency() to generate number sequences (if you are running your test in a empty company.
- If you check in your unit test model, your tests will be executed in your build.
- Using a unit test to develop you don’t need to open the browser all the time to test during development.
- Writing your tests first helps you to think about the implementation you have to write and clarify requirements if needed.