Unit testing in AX – Basics

In the previous post, I talked about what unit testing is and how such tests should be designed. Let’s jump straight into X++ code this time.

Unit tests are methods and we need a class to hold them. Such a container of test methods is called a test case and it’s simply a class extending SysTestCase.

class LedgerJournalTransTest extends SysTestCase
{ }

It can be a bit more complicated, but it doesn’t have to. This is a completely valid test class.

Test methods have stricter rules – they must:

  • be public
  • have no parameters
  • return void
  • be decorated with SysTestMethodAttribute.

The class may contain any number of helper method that don’t follow these rules; they apply to test methods only.

Test methods are typically split into three distinct steps: arrange, act and assert (the AAA pattern). I found it very helpful for keeping tests readable, therefore I’ll follow it here as well.

My first test deals with one scenario in amoundCur2DebCred() method of LedgerJournalTrans table:

[SysTestMethodAttribute]
public void positiveNoCorrection()
{
    // ARRANGE
    LedgerJournalTrans trans;
 
    // Set to non-zero values, because one side should be zeroed
    // and we want to test if it happens.
    trans.AmountCurDebit = 1.234;
    trans.AmountCurCredit = 1.234;
 
    // ACT
    trans.amountCur2DebCred(99);
 
    // ASSERT
    this.assertEquals(99, trans.AmountCurDebit);
    this.assertEquals(0, trans.AmountCurCredit);
}

You can easily see the three phases. I prepare everything needed for the test, run some logic (either returning a value, or, as in this case, changing state of an object) and then verify that the result is what expected.

Now run the test. Right-click the class and choose Add-Ins > Run tests. You should see a unit test toolbar showing that one test ran and succeeded. The toolbar can be also opened from Tools > Unit test > Show toolbar.

TestToobar

Now add one more method:

[SysTestMethodAttribute]
public void negativeNoCorrection()
{
    // ARRANGE
    LedgerJournalTrans trans;
 
    // Set to non-zero values, because one side should be zeroed
    // and we want to test if it happes.
    trans.AmountCurDebit = 1.234;
    trans.AmountCurCredit = 1.234;
 
    // ACT
    trans.amountCur2DebCred(-99);
 
    // ASSERT
    this.assertEquals(0, trans.AmountCurDebit);
    this.assertEquals(99, trans.AmountCurCredit);
}

It works nicely, nevertheless we duplicated code preparing the record for test. We can refactor the code by moving the initialization to a separate method. One solution is using a custom method and calling it on the beginning of each test, but the framework already offers a method for the same purpose: setup(). Let’s use it.

class LedgerJournalTransTest extends SysTestCase
{
    LedgerJournalTrans trans;
}
 
public void setUp()
{
    super();
 
    // Set to non-zero values, because it one side should be zeroed
    // and we want to test if it happes.
    trans.AmountCurDebit = 1.234;
    trans.AmountCurCredit = 1.234;
}
 
[SysTestMethodAttribute]
public void negativeNoCorrection()
{
    // ACT
    trans.amountCur2DebCred(-99);
 
    // ASSERT
    this.assertEquals(0, trans.AmountCurDebit);
    this.assertEquals(99, trans.AmountCurCredit);
}
 
[SysTestMethodAttribute]
public void positiveNoCorrection()
{
    // ACT
    trans.amountCur2DebCred(99);
 
    // ASSERT
    this.assertEquals(99, trans.AmountCurDebit);
    this.assertEquals(0, trans.AmountCurCredit);
}

It works exactly as before and it still has all three steps, we just moved the Arrange part somewhere else. It helps keeping test methods shorter and more readable and allow maintaining initialization code at a single place. On the other hand, it might make less obvious how the system is initialized and why.

All tests above use assertEquals() method, which is a very common assertion, but not the only one available. If you look at other methods named assert*, you’ll find find several others such as assertTrue() and assertNotNull(). Also notice that each of them has an additional parameter for a custom message on failure.

Let me show one scenario that works differently that other assertions and it may be a little bit counterintuitive. I’ll call getAssetCompany() with a transaction invalid for this particular method, therefore it should throw an exception – and I want to test if it really works as designed.

[SysTestMethodAttribute]
public void getAssetCompanyNotAssetType()
{
    trans.AccountType       = LedgerJournalACType::Cust;
    trans.OffsetAccountType = LedgerJournalACType::Bank;
 
    trans.getAssetCompany();
}

Yes, the exception is thrown correctly, but it causes the test to fail! We have to tell the framework that this is actually the correct behaviour. We can achieve that by adding this.parmExceptionExpected(true).

[SysTestMethodAttribute]
public void getAssetCompanyNotAssetType()
{
    trans.AccountType       = LedgerJournalACType::Cust;
    trans.OffsetAccountType = LedgerJournalACType::Bank;
 
    this.parmExceptionExpected(true);
 
    trans.getAssetCompany();
}

Now the framework knows that the test should throw an exception and if it happens, it’s considered successful. If it didn’t throw the error, we would get “Failure: An exception was expected to be thrown”.

As you see, writing unit tests is not necessarily something difficult and time-consuming, as you may have been told. In many cases, it’s the fastest and safest way of development, because you gradually add tests and functionality and every run of tests (which costs you nothing) verifies that all tests cases work as expected. Manual testing would take much longer. And of course, you’ve got regression tests for years ahead.

Of course, these are simple examples and you’re probably thinking of many more complicated situations, especially those when the tested object refers to other objects and database tables. That surely is trickier (and I’ll look at some cases in the next post), but it shouldn’t discourage you. First of all, that you’re not able to write unit tests for all code doesn’t mean you can’t and shouldn’t write them at all. Choose those cases you can handle and leave more complicated scenarios for later, when you have more experience with unit testing. You’ll never cover all code anyway. And as you’ll see, the solution usually isn’t in writing some extremely sophisticated unit tests. It’s in refactoring hard-to-test code to something more friendly to unit testing.

4 Comments

  1. Hi Martin,

    Great post! There’s not much written about testing for Dynamics, even though the benefits are numerous. I was wondering if you had any tips on running test under specific security contexts (i.e. testing that specific roles and permissions are working correctly). I’ve been trying to use the SysTestSecurityAttribute class decoration and SysTestSecurityContext class but to no avail. Any advice or insight you have would be greatly appreciated!

    Cheers,
    James

  2. Hey Martin,

    New to D365 and have found your posts and community feedback to be spectacular 😉

    I’m writing a costing system – lots of X++ classes, etc. I want to write some unit / implementation tests for the system, but uncertain how you can abstract the data layer from unit tests (i.e. so the unit tests are not dependent on the data within the database). The system analyzes things from D365 (such as trade agreements) to calculate a price and other user maintained tables.

    Does D365 / X++ have a data mocking feature?
    Or, should test data be insert into the database (i.e. test item / test trade agreement with expected values), tested against, and then rolled back (ttscommit / ttsabort around the data insertion)?

    I imagine you’re pretty busy, much appreciated any input that you might have.

    Curtis

      • There is still no way how to completely mock the database layer. You can use the techniques described in the third blog post to isolate dependencies by yourself, you can use transactional isolation, you can test queries against temporary data sets and use other things like that. But if you must write tests for legacy code not designed for unit testing, you’ll likely have to use the actual DB (with all the negative consequences such as problems with test isolation, performance etc.). That’s what Microsoft did in their sample solution – Fleet Management; they have code in their unit tests to drop and regenerate data in plenty of tables. That’s not what I like, but it may be a pragmatic decision in some cases.

Leave a Reply

Your email address will not be published.