A unit test
is a piece of code that exercises another piece of code and ascertains
that it behaves correctly. The developer who implements the unit to be
tested typically writes the unit test. Thought leaders in this area
recommend writing unit tests as early as possible, even before writing a
single line of the unit’s code. This principle is called test-driven development.
(You can read more about test-driven development on MSDN and more about
unit testing in the Unit Test Framework section of the Microsoft
Dynamics AX 2009 SDK.)
Writing unit tests early
forces you to consider how your code will be consumed; this in turn
makes your APIs easier to use and understand, and it results in
constructs that are more likely to be robust and long lasting. With this
technique, you must have at least one unit test for each requirement; a
failing unit test indicates an unfulfilled requirement. Development
efforts should be targeted at making the failing unit test succeed—no
more, no less.
To
reap the full benefits of unit testing, you should execute test cases
regularly, preferably each time code is changed. The Unit Test framework
in Dynamics AX supports you regardless of your approach to writing unit
tests. For example, the unit test capabilities are fully embedded in
MorphX, and you can easily toggle between writing test cases and writing
business logic.
If you’re managing
an implementation project for Dynamics AX, you should advocate testing
and support your team members in any way required. At first glance, unit
testing might seem like more work, but the investment is well worth the
effort. If you’re a team member on a project that doesn’t do unit
testing, you should convince your manager of its benefits. Plenty of
recent literature describes the benefits in great detail.
When implementing unit tests, you write a test class, also referred to as a test case.
Each test case has several test methods that exercise the object being
tested in a particular way. As you build your library of test cases,
you’ll find that you need to organize them into groups. You can group
test cases into test suites. The simplest way to do this is to use test
projects, which are simply special kinds of AOT projects.
Test Cases
To implement a unit test case, you must create a new class that extends the SysTestCase
class, which is part of the Unit Test framework. You should give the
class the same name as the class it is testing, suffixed with Test. This is illustrated in the following example, where a unit test for the Stack class is declared.
class StackTest extends SysTestCase { }
|
If you were to run the unit test at this point, you would find that zero tests were run and zero tests failed.
This default
naming convention tells the Unit Test framework which test class to
collect code coverage data for. If the default test class name doesn’t
suit your needs, you can override the testsElementName method. You can also override the testsElementType method to set the kind of element for which the framework collects code coverage data.
To create a useful test, you must add one or more test methods to the class. All test method names must start with test. The test methods must return void and take no parameters. In the following code, a test method is added to the StackTest class.
void testPushPop() { //Create an instance of the class to test. Stack stack = new Stack(); ; //Push 123 to the top of the stack. stack.push([123]); //Pop the value from the stack and assert that it is 123. this.assertEquals([123], stack.pop()); }
|
Within each test
method, you should exercise the object you test and confirm that it
behaves correctly. Running the unit test at this point tells you that
one test was run and zero tests failed.
Your testing needs should be met by the assertion methods available on SysTestCase (which extends SysTestAssert), as shown in Table 1.
Table 1. Assertion Methods on the SysTestCase Class
Method | Parameters | Action |
---|
assertEquals | (anyType, anyType) | Asserts that two values are equal. When the argument is of type object, the equal method is called to compare them. |
assertFalse | (boolean) | Asserts that the value is false. |
assertNotEqual | (anyType, anyType) | Asserts that two values are different. |
assertNotNull | (object) | Asserts that the value is not null. |
assertNotSame | (object, object) | Asserts that the objects referenced are not the same. |
assertNull | (object) | Asserts that the value is null. |
assertRealEquals | (real, real [, real delta]) | Asserts that real values differ no more than the delta. |
assertSame | (object, object) | Asserts that the objects referenced are the same. |
assertTrue | (boolean) | Asserts that the value is true. |
If an assertion fails,
the test method fails. You can configure the framework to stop at first
failure or continue with the next test method in the Unit Test
Parameters dialog box: from the Microsoft Dynamics AX drop-down menu,
point to Tools\ Development Tools\Unit Test\ Parameters. The following
code adds a new failing test method.
//Test the qty method, which returns the quantity of values on the stack. void testQty() { //Create an instance of the class to test. Stack stack = new Stack(); ; //Push 123 to the top of the stack. stack.push([123]); //Pop the value from the stack and assert that it is 0. this.assertEquals(0, stack.qty()); }
|
Running the unit test
at this point shows that two tests were executed and one failed. The
failing test appears in the Infolog. Clicking Edit opens the X++ code
editor on the assert call that failed.
You might have noticed
code redundancy in the test methods shown so far. In many cases,
initialization code is required before the test method can run. Instead
of duplicating this code in all test methods, you can refactor it into
the setUp method. If teardown logic is required, you can place it in the tearDown method. When the framework runs a test method, it instantiates a new test case class, which is followed by calls to setUp and test methods, and finally a call to the tearDown
method. This prevents in-memory data from one test method from
affecting another test method. Test suites, which are covered in the
next section, provide ways to isolate data persisted in the database
between test cases and methods. The following code uses the setUp method to refactor the sample code.
class StackTest extends SysTestCase { Stack stack;
public void setUp() {; super(); //Create an instance of the class to test. stack = new Stack(); } void testPushPop() {; stack.push([123]); this.assertEquals([123], stack.pop()); } ... }
|
The Unit Test framework also
supports testing of exceptions. If a method is expected to throw an
exception, you can instruct the framework to expect an exception to be
thrown. If you expect an exception and none is thrown, the framework
reports the test case as failed. You inform the framework that an
exception is expected by calling parmExceptionExpected ([boolean, str]).
You can specify an exception text that must exactly match the text
thrown with the exception, or the test case will fail. You shouldn’t
write more asserts after the method call expected to throw an exception
because execution should never get that far. The following code adds a
test method that expects an exception message to be thrown.
void testFailingPop() {; //Assert that an exception is expected. this.parmExceptionExpected(true, "Stack is empty!");
//Call the method expected to throw an exception. stack.pop(); }
|
The sample test case now has three test methods. By following these steps, you can run the test case from MorphX:
1. | Right-click the method, point to Add-Ins, and then click Run Tests.
|
2. | Type the name in the Test toolbar, and then click Run.
|
3. | Start the Dynamics AX client with the following command line:
StartupCmd=RunTestProject_<Name of test case class>
|
If you wanted to run the
test case programmatically, you could use a test runner class. To do
this, you would typically place the following logic in your test class’s
main method, which is invoked when you press F5 in the X++ code editor.
static void main(args _args) { SysTestRunner runner = new SysTestRunner(classStr(StackTest)); SysTestListenerXML listener = new SysTestListenerXML(@"c:\tmp\StackTest.xml"); ; runner.getResult().addListener(listener); runner.run(); }
|
Notice that you also
register a listener. If you didn’t register a listener, you wouldn’t
know the result of the test.