Quantcast
Viewing all articles
Browse latest Browse all 53708

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]publicvoid 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.

Image may be NSFW.
Clik here to view.
TestToobar

Now add one more method:

[SysTestMethodAttribute]publicvoid 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;
} 
publicvoid 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]publicvoid negativeNoCorrection(){// ACT
    trans.amountCur2DebCred(-99);
 
    // ASSERT
    this.assertEquals(0, trans.AmountCurDebit);
    this.assertEquals(99, trans.AmountCurCredit);
} 
[SysTestMethodAttribute]publicvoid 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]publicvoid 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]publicvoid 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.

Image may be NSFW.
Clik here to view.

Viewing all articles
Browse latest Browse all 53708

Trending Articles