Welcome to MSDN Blogs Sign in | Join | Help

Moving blog to a new address

I am moving my blog to a new address so that I have a better control of the blog and the spam. The new blog is available at http://www.davidpokluda.com/blog and the RSS feed is at http://feeds2.feedburner.com/dpokluda.

All the current content will remain here but I am not going to answer more comments or post new posts on this site any more.

Posted by dpokluda | 1 Comments
Filed under:

SysTest part VI.: Code coverage

In this post I want to describe code coverage module in SysTest framework. Let's have a simple calculator class (calculating area):

 public class SimpleCalc
 {
    public int square(int _a)
    {
        int square = 0;
        ;
        if (_a > 0)
        {
            square = _a * _a;
        }
        return square;
    }
    public int rectangle(int _a, int _b)
    {
        int rectangle = 0;
        ;
        if (_a > 0 && _b > 0)
        {
            rectangle = _a * _b;
        }
        return rectangle;
    }
 }

Now let's have the following TestClass:

 public class SimpleCalcTest extends SysTestCase
 {
    public void testRectangle()
    {
        SimpleCalc calculator = new SimpleCalc();
        ;
        this.assertEquals(0, calculator.rectangle(-1, -1));
        this.assertEquals(0, calculator.rectangle(0, -1));
        this.assertEquals(1, calculator.rectangle(1, 1));
        this.assertEquals(6, calculator.rectangle(2, 3));
    }
    public void testSquare()
    {
        SimpleCalc calculator = new SimpleCalc();
        ;
        this.assertEquals(0, calculator.square(-1));
        this.assertEquals(0, calculator.square(0));
        this.assertEquals(1, calculator.square(1));
        this.assertEquals(4, calculator.square(2));
    }
 }

To run this test we use the following job:

    SysTestSuite suite = new SysTestSuite(classstr(SimpleCalcTest));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    result.addListener(new SysTestListenerDB());
    suite.run(result);
    print result.getSummary();

Notice the line where we register SysTestListenerDB listener. This listener writes all the execution data into SysTest database. You can browse this database either directly (not recommended) or using our UI from Tools > Development tools > Unit test > Test jobs. Locate your test job (the highest Test job ID -- there is a bug in the form and the form is not sorted correctly), and click Tests button. You see all test classes and their test methods, their result and code coverage. Note that the code coverage is 0%.

Now let's make a simple tweak and add one line of code to this job:

    SysTestSuite suite = new SysTestSuite(classstr(SimpleCalcTest));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    result.addListener(new SysTestListenerDB());
    result.parmCodeCoverageEnabled(true);		<-- CODE COVERAGE ENABLED
    suite.run(result);
    print result.getSummary();

Run the job again and note that in SysTest tables we now see 100% code coverage. See that if you select a test class or a test method and click on Coverage button, you can see the lines that were executed. Now you might ask two questions:

  1. Is there a way to turn code coverage on/off somewhere in UI?
  2. How does the framework know that the test class is supposed to test exactly our SimpleCalc class?

Answer to the first question is easy using Tools > Development tools > Unit test > Parameters where you can check Record code coverage checkbox. Answer to the second question is more difficult. The framework of course doesn't know if you don't specify the target class name. On the other hand the framework might try to guess. The framework sees that your test class is named SimpleCalcTest. If you don't specify anything then the framework will try to see if there is a class named SimpleCalc (the test name without "Test" suffix). If there is a class with this name then the code coverage will be calculated against this class.

Let's now assume that we have the same class but our tests should be named ScienceCalcTest. We now need to tell the framework that even though the test is named ScienceCalcTest the code coverage should be calculated for class SimpleCalc. To do so we have to override two methods:

    public identifiername testsElementName()
    {
        return classstr(SimpleCalc);
    }
    public UtilElementType testsElementType()
    {
        return UtilElementType::Class;
    }

The first method specifies that the target is named SimpleCalc and the second method specifies that it is a class. You can also target a table if you want to. Now update the job and run it.

    SysTestSuite suite = new SysTestSuite(classstr(ScienceCalcTest));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    result.addListener(new SysTestListenerDB());
    result.parmCodeCoverageEnabled(true);
    suite.run(result);
    print result.getSummary();

If you go to the SysTest table you should see that reported code coverage is again 100%. That's all.

Before I finish this post I would like to note that code coverage works by turning on DynamicsAX profiler. This causes the execution to be much slower. To demonstrate that let's run the following sample job:

    SysTestRunner test = null;
    int startTime = 0;
    ;
    print 'CodeCoverage: OFF';
    startTime = WinAPI::getTickCount();
    test = SysTestRunner::createTestRunner(classstr(SimpleCalcTest));
    test.getResult().addListener(new SysTestListenerDB());
    test.getResult().parmCodeCoverageEnabled(false);
    print test.run().getSummary();
    print strfmt('Elapsed time: %1 ms', int2str(WinAPI::getTickCount() - startTime));
    print '';
    
    print 'CodeCoverage: ON';
    startTime = WinAPI::getTickCount();
    test = SysTestRunner::createTestRunner(classstr(SimpleCalcTest));
    test.getResult().addListener(new SysTestListenerDB());
    test.getResult().parmCodeCoverageEnabled(true);
    print test.run().getSummary();
    print strfmt('Elapsed time: %1 ms', int2str(WinAPI::getTickCount() - startTime));
    print '';

Notice that the code coverage slows down the execution considerably. Therefore if you do test driven development, refactoring or any other development where you execute your tests very frequently, turn the code coverage off.


You can download these samples: Part06: CodeCoverage.xpo


Posted by dpokluda | 4 Comments
Filed under: ,

I'm back

I'm very sorry for being quiet for such a long time. We had a coding milestone and I spent most of that time designing and implementing new importing functionality that we are going to use in the next release of Dynamics AX.

I will soon finish the SysTest series with the post about CodeCoverage. Later I will continue describing other tools we built on top of SysTest for our internal development.

Posted by dpokluda | 2 Comments
Filed under: ,

SysTest part V.: Test execution (results, runners and listeners)

In the series of previous posts I've covered TestClasses and TestSuites. You should already know how to create a TestClass and how to group your tests in TestSuites. In this post I'll try to cover the test execution environment.

Results and Runners

You probably remember the following code:

 static void RunSimpleTests(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(SimpleTests));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();

    pause;
 }

This code creates a runtime suite for all test methods from SimpleTests class. The following diagram displays all objects that are interacting during test execution.

SysTestRunner is a new component that is used to glue your test and result object. It takes care of the suite creation as well as the result object creation. The following sample is doing the same thing as the previous one.

 static void RunSimpleTestsUsingRunner(Args _args)
 {
    SysTestRunner runner = SysTestRunner::createTestRunner(classstr(SimpleTests));
    ;
    runner.getResult().addListener(new SysTestListenerPrint());
    runner.run();
    print runner.getSummary();

    pause;
 }

Actually it does more than that. Let's do a little experiment. Go to Tools > Development Tools > Unit Test > Parameters. Navigate to Listeners tab and select Infolog listener (move it from right to left using the [<] button). Now rerun the previous job and after pressing Yes on the pause dialog notice that an Infolog is displayed. This means that SysTestRunner creates a result object for you and by default configures the result object based on the settings from Parameters form (listeners, code coverage, etc.). 

Listeners

There is one more component in the diagram above, SysTestListener. Result object uses Observer pattern for reporting results. SysTestResult object acts as the subject and observers are "listeners". Listeners are implementing SysTestListener interface. To be more flexible it is possible to implement either "push model" or "pull model". All out of the box listeners in DynamicsAX use the push model. To support the push model the framework contains SysTestListenerData interface that is used to pass change value object to all registered observers.

DynamicsAX contains out of the box a wide range of listeners.

To use different listeners, just call addListener on your result object. You can have multiple listeners registered at the same time.

 static void UsingListeners(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(SimpleTests));
    SysTestResult result = null;
    ;
    print 'Registered listeners: NO';
    result = new SysTestResult();
    print suite.run(result).getSummary();
    print '';
    pause;

    print 'Registered listeners: print';
    result = new SysTestResult();
    result.addListener(new SysTestListenerPrint());
    print suite.run(result).getSummary();
    print '';
    pause;

    print 'Registered listeners: infolog';
    result = new SysTestResult();
    result.addListener(new SysTestListenerInfolog());
    print suite.run(result).getSummary();
    // Give infolog chance to be displayed
    infolog.setTimeOut('notify', 200);
    infolog.wait();
    print '';
    pause;
    infolog.closeInfolog();

    print 'Registered listeners: print, database';
    result = new SysTestResult();
    result.addListener(new SysTestListenerPrint());
    result.addListener(new SysTestListenerDB());
    print suite.run(result).getSummary();
    print 'Because we used database listener we can load results using unit test form.';
    pause;
 }

The last part of the previous sample uses SysTestListenerDB listener. This listener persists all the test results in the database. You can browse the results with the Test jobs form by going to Tools > Development Tools > Unit Test > Test jobs. Navigate to the last test job (I know there is a bug with sorting of the jobs based on the ID). You should see our SimpleTests in there. Select this record and click on Tests button. A form is displayed with details about all test classes and test methods that were run.

Implementing a custom listener

If you need to implement your own listener (logging to a different database, to a file in a certain file format, using a COM or .NET object, etc.) then all you need to do is to implement the SysTestListener interface. The following example demonstrates how to create a “failure” memory listener. The listener will contain only information about failures. To get messages from the memory it will publish getMessages method.

 public class FailureListener implements SysTestListener
 {
    List failures;
     
    public void new()
    {
        ;
        failures = new List(Types::String);
    }
    public List getMessages()
    {
        return failures;
    }
 
    public void addFailure(SysTestListenerData _data)
    {
        str message = strfmt('%1: %2', _data.parmTest().getName(), _data.parmMessage())
        ;
        failures.addEnd(message);
    }
    public void addInformation(SysTestListenerData _data)
    {
    }
    public void close()
    {
    }
    public void endSuite(SysTestListenerData _data)
    {
    }
    public void endTest(SysTestListenerData _data)
    {
    }
    public void open()
    {
    }
    public void startSuite(SysTestListenerData _data)
    {
    }
    public void startTest(SysTestListenerData _data)
    {
    }
 }

The following sample demonstrates the use of the new listener:

 static void UsingFailureListener(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(SimpleTests));
    SysTestResult result = new SysTestResult();
    FailureListener listener = new FailureListener();
    ListIterator iterator = null;
    ;
    result.addListener(listener);
    suite.run(result);

    iterator = new ListIterator(listener.getMessages());
    while (iterator.more())
    {
        print iterator.value();
        iterator.next();
    }
    print '--------------------------------------------------------------';
    print result.getSummary();

    pause;
 }

You can also use any of the available listeners and just extend its behavior. This way you can easily change their formatting for example because most of them have protected write method.


You can download these samples: Part05: Execution.xpo


This is all for this post. Next week I'll try to cover code coverage.

Posted by dpokluda | 2 Comments
Filed under: ,

SysTest part IV.: Test/Suite initialization and cleaning; Execution isolation

We already know how to create test suites. Today I would like to cover some additional reasons for creating test suites: initialization, cleaning and test execution isolation.

Initialization and Clean up

As long as our test class contains just one test method everything is fine. In that method we include all preparation steps as well as all cleaning steps (resource cleaning and stuff). But honestly how often do you write test classes with just one test method? Not that often, right? Once we have more than jus one test method in the class we should start refactoring the code. That's pretty easy by extracting setup and tearDown methods. In these two methods we do whatever we need to do. All our test methods then call these methods:

 private void initialize()
 {
    // initialization code...
 }
 private void clean(
 {
    // cleaning code...
 } 
 
 public void testMethod1()
 {
    ;
    this.initialize();
 
    // your test method code...
 
    this.clean();
 }

The situation gets little bit more complicated when exceptions are expected.

 public void testException()
 {
    ;
    this.initialize();
 
    try
    {
        // do something here...
    }
    catch()
    {
        // make sure you catch every possible exception because if an exception gets uncaught
        // then it will be caught by the framework and your cleaning method will not be called!
    }
    this.clean();
 }

It would be much easier if the framework itself would force calling of these two methods. That's really happening. The framework itself is calling setup before every test method and tearDown after every test method (regardless of the test result).

You can rewrite the previous simple sample to:

 public class InitAndCleanTest extends SysTestCase
 {
    public static void main(Args _params)
    {
        SysTestSuite suite = new SysTestSuite(classstr(InitAndCleanTest));
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint());
        suite.run(result);
        print result.getSummary();
    
        pause;
    }

    public void setup()
    {
        ;
        print 'Test initialization called';
    }
    public void tearDown()
    {
        ;
        print 'Test cleaning called...';
    }

    public void testMethod1()
    {
        ;
        print 'testMethod1';
    }
    public void testMethod2()
    {
        ;
        print 'testMethod2';
    }
 }

But why do I mention it when discussing test suites? Well that might be one of the reasons for you to create a test suite. As well as test method setup and teardown methods, SysTest has setup and teardown methods for test suites. That means that the framework would call setup method before calling the first method in the suite and tearDown after the last method in the suite.

Let's create a simple suite with setup and tearDown methods and let's add our previous test class in it:

 public class InitAndCleanSuite extends SysTestSuite
 { 
    public static void main(Args _params)
    {
        InitAndCleanSuite suite = new InitAndCleanSuite();
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint());
        suite.run(result);
        print result.getSummary();
    
        pause;
    }

    public void new()
    {
        ;
        super();
        this.add(new InitAndCleanTest());
    }
    public void setup()
    {
        ;
        print 'Suite initialization called';
    }
    public void tearDown()
    {
        ;
        print 'Suite cleaning called...';
    }
 }

Run the suite and notice that the setup and tearDown are called just once. So whenever you need to call a special initialization (data setup for example) routine before all your tests then creating a suite would be a way to achieve that.

Execution isolation

Another reason for a suite would be an "execution isolation". Sometimes we want to run the test but we don't necessarily want to update data in the database. SysTest offers several execution isolations.

Transaction

We can ask SysTest to place our code in a transaction that will be aborted at the end of our test. You might ask why after every test. Well in unit testing all unit tests should run independently and thus it doesn't make sense to keep data from the previous test to the next ones. That's why SysTest aborts open transactions after every test method.

How do we do that? It's super easy. Whenever you create a test suite then extend the suite not from SysTestSuite but from SysTestSuiteTTS.

 public class InitAndCleanSuite extends SysTestSuiteTTS
 { 
    ...
 }

Company account

The problem with transaction isolation is in forms. Forms usually require data to be committed to the database. The solution that SysTest offers is in a separate company account. SysTest can create a company account for your test and at the end delete that company account. It has one drawback though. Creation, switching and deletion of the company account takes some time and the test execution is then dramatically affected. That's why SysTest offers here two isolation types: method and class isolations.

If you derive your test suite from SysTestSuiteCompanyIsolateMethod then a company account is created before each test. The test then is executed within that company account and the account is deleted when the test is finished.

This is probably exactly what you would expect. To speed it up a little bit you can derive the suite from SysTestSuiteCompanyIsolateClass and SysTest will not delete the company account after every test but only after the last test. This goes a little against the idea of independent tests and you should treat this as a compromise.

Runtime test isolation

In the previous section I have described how to create a test suite serving as execution isolation for our test classes that are included in this suite. Do we have something similar without actually creating the test suite manually at design time? Yes we do. Do you still remember how we executed our tests using SysTest API (programmatically)? If not then here is the code:

    SysTestSuite suite = new SysTestSuite(classstr(InitAndCleanTest));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
 
    pause;

To run a test we create a default suite and wrap our test class in this suite. Then we can run the suite (and it will run all or test methods). Well, we are not forced to stay with the base suite class. We can use whatever SysTestSuite class. The following job would run the same tests but this time that would be isolated in a transaction:

    SysTestSuite suite = new SysTestSuiteTts(classstr(InitAndCleanTest));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
 
    pause;

Final note about transaction isolation

Assume we have the following test class (testing that an exception was thrown).

 public class TtsExceptionTest extends SysTestCase
 { 
    public static void main(Args _params)
    {
        SysTestSuite suite = new SysTestSuite(classstr(TtsExceptionTest));
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint());
        suite.run(result);
        print result.getSummary();
    
        pause;
    }

    public void testExpectedException()
    {
        ;
        try
        {
            // exception is being expected
            throw Exception::Error;
        }
        catch
        {
            // exception was caught
        }
    }
 }

When you run the test you will see the expected result:

 2 run, 0 failed

Now let's change the execution isolation to transaction isolation.

    public static void main(Args _params)
    {
        SysTestSuite suite = new SysTestSuiteTTS(classstr(TtsExceptionTest));
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint());
        suite.run(result);
        print result.getSummary();
    
        pause;
    }

Notice that the test is now wrapped in SysTestSuiteTTS and executed. Suprisingly the test result is now:

 2 run, 1 failed

What!!! Why is that? Well, that's due to a "unique" way of handling exceptions inside transactions in DynamicsAX X++ language. When you are inside a transaction (and we are here) and an exception is thrown then the transaction is aborted and the exception is handled at the level where the transaction is defined (in our case inside SysTest framework). Therefore our catch doesn't work here and the framework reports our test as failure.
For more information about this see Developer Help in DynamicsAX 4.0 (Developer.chm) and navigate to Microsoft Dynamics AX SDK > X++ Language Reference Guide > Statements and Loops > Exception Handling > Exceptions Inside Transactions.
You can also read about this topic in the blog post Axapta error handling and database transactions posted on SysDictCoder.


You can download these samples: Part04: Suites and Execution isolation.xpo


In the next post I will try to describe listeners.

Posted by dpokluda | 2 Comments
Filed under: ,

Outbound web service (AIF)

In this post I'll try to demonstrate how to use AIF outbound web service adapter. I'll begin with building a sample web service implementing AIF SendMessage "interface". We will validate the web service by building a test.

For the end-to-end test we will need to deploy the web service to a real IIS server. When IIS is configured and web serviced deployed we will configure AIF in Dynamics AX 4.0.

At the end we will run our end-to-end test by sending purchase requisition to our web service.

Web Service

Let's begin with building our new web service. I assume you have Microsoft VisualStudio 2005 and NUnit (you can use Team System test framework or any other unit test framework but I picked NUnit because I believe that everybody has access to this one) ready on your box.

Generate project templates

  • Create a new blank solution named AifOutboundWebService in Visual Studio 2005.
  • Add new ASP.NET Web Service project named AifOutboundWebService by right-clicking on your solution and selecting Add > New Web Site.... Select ASP.NET Web Service template and set name to AifOutboundWebService. (I'm also selecting File System and C# as the target language.)
  • Delete generated web service Service.asmx and code file App_Code/Service.cs.
  • Create new Web Service named MyWebService by right-clicking on your web site project and selecting Add New Item.... Select Web Service and set name to MyWebService.
  • Rename web method HelloWorld to GetTestMessage.
  • Update web service namespace to:
 [WebService(Namespace = "http://localhost/AifWebServiceTest/MyWebService.asmx/")]
  • Create new class library project by right-clicking on your solution and selecting Add > New Project.... Select Class Library and set name to AifWebServiceTest.
  • Rename Class1.cs to MyWebServiceTest.cs in Solution Explorer.
  • Add reference to NUnit.Framework.dll.
  • Add NUnit.Framework namespace to your test class. using NUnit.Framework;
  • Build the solution.
  • Start web service by right-clicking on the web service project and selecting View in Browser... menu item.
  • Add web reference to our new web service by right-clicking your NUnit test project and selecting Add Web Reference.... Copy URL from IE running your web service (on my box that is http://localhost:1234/AifOutboundWebService/) and click Go button. Click on MyWebService.asmx, change Web reference name from localhost to MyWebService and click Add Reference.
  • Add simple test to your newly created class AifWebServiceTest:
 using System;
 using System.Collections.Generic;
 using System.Text;
 using NUnit.Framework;
 
 namespace AifWebServiceTest
 {
    [TestFixture]
    public class MyWebServiceTest
    {
        private MyWebService.MyWebService service;

        [Test]
        public void TestHelloMessage()
        {
            Assert.AreEqual("Hello World", service.GetTestMessage());
        }

        [SetUp]
        public void SetUp()
        {
            service = new MyWebService.MyWebService();
        }
        [TearDown]
        public void TearDown()
        {
            service = null;
        }
    }
 }
  • Start the web service (by right-clicking on the web service project and selecting View in Browser...)
  • Start NUnit and run the test.

Requirements for MyWebService

Let's update the unit test to capture requirements for our web service. Our web service will store all messages sent in a cache. Later you can ask the web service to retrieve all messages sent to it.

Add the following tests to your unit test (notice that Array2String is a helper method converting array of string to a string representation of that array):

ClearMessages

This method clears the cache:

  • when called then GetMessages always returns an empty array (until a new message is added)
        [Test]
        public void TestClearMessages()
        {
            string message = service.GetTestMessage();
            Assert.AreEqual(0, service.GetMessages().Length);
            service.SendMessage(message);
            Assert.AreEqual(1, service.GetMessages().Length);

            //ClearMessages: clears the cache
            service.ClearMessages();
            Assert.AreEqual(0, service.GetMessages().Length);
        }

GetMessages

This method returns all messages from the cache:

  • when called then it returns an array of strings
        [Test]
        public void TestGetMessages()
        {
            string message = service.GetTestMessage();
            List< string > emptyList = new List< string >();
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));

            service.SendMessage(message);
            emptyList.Add(message);
            service.SendMessage(message);
            emptyList.Add(message);

            //GetMessages: retrieves an array of messages from the cache
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));
        }

SendMessage

This method sends a message to the cache:

  • when called the message passed to it is added to the cache
  • when another instance of the same message is sent then both of them appear in the cache
        [Test]
        public void TestSendMessage()
        {
            string message = service.GetTestMessage();
            List< string > emptyList = new List< string >();
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));

            //SendMessage: add string ( = input ) to the cache
            service.SendMessage(message);
            emptyList.Add(message);
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));

            service.SendMessage(message);
            emptyList.Add(message);
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));
        }

readPurchaseRequisition

We will also need this method for our AIF end-to-end scenario:

  • this will be exactly same as our SendMessage
        [Test]
        public void TestReadPurchaseRequisition()
        {
            string message = service.GetTestMessage();
            List< string > emptyList = new List< string >();
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));

            //SendMessage: add string ( = input ) to the cache
            service.readPurchaseRequisition(message);
            emptyList.Add(message);
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));

            service.readPurchaseRequisition(message);
            emptyList.Add(message);
            Assert.AreEqual(Array2String(emptyList.ToArray()), Array2String(service.GetMessages()));
        }

Helper method

Let's code the helper method:

        private string Array2String(string[] array)
        {
            StringBuilder content = new StringBuilder();
            content.Append("<");
            foreach (string value in array)
            {
                if (content.Length > 1)
                    content.Append(", ");
                content.AppendFormat("\"{0}\"", value);
            }
            content.Append(">");

            return content.ToString();
        }

Coding our cache

So now we know exactly how our web service should behave. Let's begin with our cache for storing messages.

  • Add new class named AifMessageCache.cs to our web service project (confirm message asking to put the class into App_Code folder):
 using System;
 using System.Collections.Generic;

 internal class AifMessageCache
 {
    private static List< string > messages = new List< string >();

    public static void Clear()
    {
        messages = new List< string >();
    }
    public static List< string > Messages
    {
        get { return messages; }
    }
    public static void Add(string message)
    {
        messages.Add(message);
    }
 }

Coding MyWebService

Let's code our web methods ClearMessages, GetMessages, SendMessage and readPurchaseRequisition. It will be extremely simple because all we need to do is to delegate the call to our message cache.

    [WebMethod]
    public void ClearMessages()
    {
        AifMessageCache.Clear();
    }
    [WebMethod]
    public void SendMessage(string message)
    {
        AifMessageCache.Add(message);
    }
    [WebMethod]
    public void readPurchaseRequisition(string message)
    {
        AifMessageCache.Add(message);
    }
    [WebMethod]
    public List< string > GetMessages()
    {
        return AifMessageCache.Messages;
    }

For this code to compile we have to add support for generic collections:

 using System.Collections.Generic;

Build the solution

  • Build the web service project by right-clicking on it and selecting Build Web Site.... Now we need to update web service reference in our NUnit project. Right-click on the web reference to MyWebService and select Update Web Reference. Now let's rebuild the whole solution by right-clicking on the solution and selecting Rebuild Solution menu item.
  • Start the web service by right-clicking the web service project and selecting View in Browser.... Run all the unit tests to validate the basic functionality.

We get one failure in TestSendMessage. This failure is caused by a wrong assumption. Our tests assume that the cache is empty when the test begins. That's not true and thus we have to call ClearMessages before every test. To do that update SetUp method in your test class:

        [SetUp]
        public void SetUp()
        {
            service = new MyWebService.MyWebService();
            service.ClearMessages();
        }

Rebuild the solution and rerun the tests again. You should see all the tests passing.

IIS

I assume you have IIS configured on your box (localhost).
  • Create new folder AifWebServiceTest and copy there the content of your AifOutboundWebService folder (including App_Code and App_Data subfolders).
  • Open Internet Information Services (IIS) Manager from Administrative Tools. Go to your Default Web Site and display properties for AifWebServiceTest folder (right-click on the AifWebServiceTest folder and select Properties menu item).
  • Check Directory browsing, click Create button (to create web application). Go to Directory Security tab and check Integrated Windows authentication and uncheck Enable anonymous access. Close the dialog by clicking OK.
  • Check ASP.NET settings by going back to Properties, navigate to ASP.NET tab and verify that version 2.0.50727 is selected as ASP.NET version.
  • Validate the application by right-clicking on it and selecting Browse. You should see a listing of files. Click on MyWebService.asmx and you should see the web service methods listed.
  • Try the same using your browser -- open the browser and navigate to http://localhost/AifWebServiceTest/MyWebService.asmx.

Note:

  1. Make sure your default web site and your new web application are running ASP.NET 2.0.
  2. Your NUnit test project is not running against this new IIS web application. If you want that then you have to update the web reference to point to your new web application -- delete the current web reference and add new using the IIS URL: http://localhost/AifWebServiceTest/MyWebService.asmx.

Aif configuration

  • Open Microsoft Dynamics AX 4.0.
  • Navigate to Basic > Setup > Application Integration Framework.
  • Add new local endpoint with Local endpoint = MyLocalEndpoint.
  • Add transport adapter: Class = AifWebServiceAdapter and don't forget to check Active.
  • Add channel: ID = MyChannel, Name = MyChannel, Adapter = Web Service Adapter, Direction = Outbound, Address = http://localhost/AifWebServiceTest/MyWebService.asmx and don't forget to check Active.
  • Adding actions begins by clicking Scan and register button in Actions form. When the scan is done, navigate to readPurchaseRequisition and check Enabled.
  • Add endpoint: ID = MyEndpoint, Name = MyEndpoint, Local endpoint ID = MyLocalEndpoint. Go to General tab and select Outbound channel ID = MyChannel. Go to Constraints tab and check No constraints. Go to Users tab and add Admin user to Endpoint users section. Click Action policies to open endpoint policies form. Add readPurchaseRequisition action and set Status = Enabled, Loggins Mode = Log All. After saving the policies, close that form. Activate the endpoint by checking Active checkbox on the General tab page.

Final test

  • Create and post a purchase order (Accounts Payable > Purchase Order, create a purchase order, Posting > Purchase Order)
  • Send the purchase order electronically (Inquiries > Purchase, Send electronically > Original)
  • Execute job RunAifServices (you should configure batch process but since this is not the main focus of this post I will cheat a little bit here).
 static void RunAifServices(Args _args)
 {
    AifGatewaySendService gtwySend = new AifGatewaySendService();
    AifOutboundProcessingService outSvc = new AifOutboundProcessingService();
    ;
    print 'Running Outbound Processing Service...';
    outSvc.run();

    print 'Running Gateway Send Service...';
    gtwySend.run();

    print "AIF Services complete";
    pause;
 }
  • No error message should be displayed
  • Verify the send operation
    • Navigate to http://localhost/AifWebServiceTest/MyWebService.asmx.
    • Click on GetMessages and click on Invoke.
    • Verify the message is displayed

External identifier override

What if our web service doesn't have readPurchaseRequisition but only SendMessage. Can we still configure AIF so that it calls this method instead of readPurchaseRequisition? Yes we can. Go to Endpoint configuration form, select your endpoint and go to endpoint action policies form by clicking Action policies. Navigate to readPurchaseRequisition row and enter SendMessage to External Identifier Override. This will force AIF to call SendMessage whenever there is a message for readPurchaseRequisition action.

Rerun the test and verify that the message was sent to the web service.

FAQs, Tips and Tricks

Error messages

Problem: In case of a problem with Dynamics AX 4.0 you don't receive a valid error message but instead an error message saying:

 Could not find any resources appropriate for the specified culture or the neutral culture.

Solution: This is a known error that was already fixed. The fix will be part of 4.01 release. If you want you can download updated DLL from here.

  • Exit all Dynamics AX instances (clients)
  • Restart AOS
  • Copy the DLL to your Dynamics AX 4.0 Server\{CompanyName}\Bin folder

Now whenever there is an error with Outbound web service adapter in AIF you should receive a better error message containing the real cause of an error.

Can't call web method

Problem: You keep on getting a SOAP error that it's not possible to call your web method. You are sure that everything is fine but you keep on getting this error message.

Solution: Try to verify that the namespace defined in your web service corresponds to the web service URL. If your web service URL is http://localhost/AifWebServiceTest/MyWebService.asmx then your web service namespace should be set that way:

 [WebService(Namespace = "http://localhost/AifWebServiceTest/MyWebService.asmx/")]

Also, please note the trailing forward slash character. This all is very important!


You can download the complete solution at: AifOutboundWebService


Other resources

For more information about AIF I suggest:
Posted by dpokluda | 33 Comments
Filed under: , ,

SysTest part III.: Introduction to test suites

In the first part I mentioned the following code:

 static void RunMySimpleTests(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(MySimpleTests));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
 
    pause;
 } 

Simple suites

We didn't discuss this code in detail and we will not cover all the aspects of this sample even today. We will concentrate on the first line. Our test class usually contains multiple tests and to run them all it is easier for the framework to wrap them together (don't get me wrong, they are still independent UnitTests) and execute them at once. To do that SysTest contains SysTestSuite class.

What you see in the previous sample is creation of SysTestSuite class from MySimpleTests class. SysTestSuite constructor will take all test methods (see part 1 to understand what methods are considered unit tests) and add them to the newly created suite. To run all the tests now it is enough to just call run on the suite object.

Let's take a look at architecture diagram (this is extremely simplified diagram to show just the important parts):

You might recognize the Composite pattern there. Therefore it is possible not to add just tests into a suite but you can also add other suites. This way you can create pretty complex tree structure of suites (we might discuss later why that is a good idea).

We already know how to create suite from a test class but how do we add an individual test (or suite) to the suite? To answer this question we first have to understand how to represent (instantiate) an individual test.

 SysTest::newTestable(new MySimpleTests(), methodstr(MySimpleTests, testInfo))

I know what you think. This must be possible to do it simpler than this. Well, you're right but we don't have to worry about this too much now. Once we have the test we should be able to run just this individual test.

The following job runs just that one test:

    SysTestResult result = new SysTestResult();
    SysTest test = SysTest::newTestable(new MySimpleTests(), methodstr(MySimpleTests, testInfo));
    ;
    result.addListener(new SysTestListenerPrint());
    test.run(result);
    print result.getSummary();

Now let's get back to the original question. How do I add this individual test to my suite? Well, if it is just that one test then you can pass it to the suite constructor.

    SysTest test = SysTest::newTestable(new MySimpleTests(), methodstr(MySimpleTests, testInfo));
    SysTestSuite suite = new SysTestSuite(test);
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();

    pause;

What if now I want to create a suite containing two tests? First test will be testInfo from our previous MySimpleTests class and the other one will be testThrowErrorMessage from our previous MyExceptionTest class. We can't use suite constructor here but instead we will add individual tests using add method.

    SysTestSuite suite = new SysTestSuite();
    SysTestResult result = new SysTestResult();
    ;
    suite.add( SysTest::newTestable(new MySimpleTests(), methodstr(MySimpleTests, testInfo)) );
    suite.add( SysTest::newTestable(new myExceptionTest(), methodstr(myExceptionTest, testThrowErrorMessage)) );

    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();

Run the test to see our tests running (use print listener to see execution details and if you use job then don't forget to add pause at the end).

Suites of suites

Now let's create a suite containing other suites. Let's say that now we want to add a suite of all tests from MySimpleTests, a suite of individual tests testThrowErrorException and testThrowException2, and finally an individual test testThrowErrorMessage. The following code should be familiar to you now.

 static void RunSuiteOfSuites(Args _args)
 {
    SysTestSuite simpleTestsSuite = new SysTestSuite(classstr(MySimpleTests)); // <-- MySimpleTests suite
    SysTestSuite exceptionTestsSuite = new SysTestSuite();                     // <-- MyExceptionTest suite
    SysTestSuite suite = new SysTestSuite();                                   // <-- root suite
    SysTestResult result = new SysTestResult();
    ;
    // Populate exceptionTestsSuite:
    exceptionTestsSuite.add( SysTest::newTestable(new myExceptionTest(), 
                             methodstr(myExceptionTest, testThrowErrorException)) );
    exceptionTestsSuite.add( SysTest::newTestable(new myExceptionTest(), 
                             methodstr(myExceptionTest, testThrowErrorException2)) );
 
    // Populate the main suite
    suite.add(simpleTestsSuite);
    suite.add(exceptionTestsSuite);
    suite.add( SysTest::newTestable(new myExceptionTest(), 
               methodstr(myExceptionTest, testThrowErrorMessage)) );
    
    // Run the suite
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
 
    pause;
 }

Run the job and see that it really runs all your tests and that the tests are really executing from the right suite.

The previous code demonstrated how to create suite dynamically at runtime. Is it possible to do the same at design time? Let's create a suite class from the previous sample.

All we have to do is to create a class derived from SysTestSuite and override the constructor.

 public class MyFirstSuite extends SysTestSuite
 {
    public void new()
    {
        SysTestSuite simpleTestsSuite = new SysTestSuite(classstr(MySimpleTests)); // <-- MySimpleTests suite
        SysTestSuite exceptionTestsSuite = new SysTestSuite();                     // <-- MyExceptionTest suite
        ;
        super();
    
        // Populate exceptionTestsSuite:
        exceptionTestsSuite.add( SysTest::newTestable(new myExceptionTest(), 
                                 methodstr(myExceptionTest, testThrowErrorException)) );
        exceptionTestsSuite.add( SysTest::newTestable(new myExceptionTest(), 
                                 methodstr(myExceptionTest, testThrowErrorException2)) );
    
        // Populate the main suite
        this.add(simpleTestsSuite);
        this.add(exceptionTestsSuite);
        this.add( SysTest::newTestable(new myExceptionTest(), 
                  methodstr(myExceptionTest, testThrowErrorMessage)) );
    }

    public static void main(Args _params)
    {
        MySuite suite = new MySuite();
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint());
        suite.run(result);
        print result.getSummary();
    }
 }

When I load this test in my GUI runner I see the right structure.

The GUI runner used for the previous screenshot is not part of SysTest -- if there is interest we can discuss this runner in a later post. It allows you to run just a certain nodes and review execution messages per suite/test.


You can download these samples: Part03: Suites.xpo


It was a long post describing just how to create suites. In the next post I will describe why do we have suites and what are the different types of suites you can use.

 

Posted by dpokluda | 5 Comments
Filed under: ,

SysTest part II.: Exceptions

In this post I will try to describe exceptions handling using SysTest.

Very often applications throw exceptions and we have to be able to handle these situations correctly in our UnitTests. Let's say we have the following application class:

 public class ThrowingApp
 {
    public static ThrowingApp construct()
    {
        ;
        return new ThrowingApp();
    }
    public void throwingErrorException()
    {
        ;
        throw Exception::Error;
    }
 }

To write a test for this method verifying that it really throws the exception we can write the following UnitTest:

 void testThrowErrorException()
 {
    ThrowingApp theApp = ThrowingApp::construct();
    ;
    try
    {
        theApp.throwingErrorException();
        this.fail('Method throwErrorException didn\'t throw an exception.');
    }
    catch(Exception::Error)
    {
        this.info('An expected exception has been caught.');
    }
 }

This works but SysTest offers another way of handling exceptions. SysTestCase provides parmExceptionExpected property to specify that the test expects the calling code to throw an exception. To do so set parmExceptionExpected property to true.

 void testThrowErrorException2()
 {
    ThrowingApp theApp = ThrowingApp::construct();
    ;
    this.parmExceptionExpected(true);
    theApp.throwingErrorException();
 }

This test is doing the same thing as the previous one. The property can actually do even more. Let's add the following two methods to our ThrowingApp class.

    public void throwingErrorMessage()
    {
        ;
        throw error('Error message threw');
    }
    public void postingErrorMessage()
    {
        ;
        error('Error message posted');
    }

The paramater parmExceptionExpected has an additional optional parameter named _exceptionMessage where you can specify a message that is expected to be thrown:

 void testThrowErrorMessage()
 {
    ThrowingApp theApp = ThrowingApp::construct();
    ;
    this.parmExceptionExpected(true, 'Error message threw');
    theApp.throwingErrorMessage();
 }

The same is valid also for error messages that are just added to the Infolog (they don't have to be thrown).

 void testPostErrorMessage()
 {
    ThrowingApp theApp = ThrowingApp::construct();
    ;
    this.parmExceptionExpected(true, 'Error message posted');
    theApp.postingErrorMessage();
 }

If the class throws an exception but the exception/error message is different than expected then SysTest will fail the test and correctly report expected exception/error message:

 void testPostErrorMessage2()
 {
    ThrowingApp theApp = ThrowingApp::construct();
    ;
    this.parmExceptionExpected(true, 'Different error message');
    theApp.postingErrorMessage();
 }

When you execute this test (using SysTest toolbar for example: Tools > Development Tools > Unit Test > Show toolbar) you see the following error message:

 Error: [myExceptionTest.testPostErrorMessage] Failure: An exception was expected to be thrown! 
 (Expected: Different error message)

You can download these samples: Part02: Exceptions.xpo


In the next post I will try to describe test suites and their different "flavors".

Posted by dpokluda | 3 Comments
Filed under: ,

SysTest part I.: Assertions and test classes

In the next series of posts I will demonstrate and explain some of the basic as well as some more advanced features of SysTest.

Test classes

Let's begin with a very simple test. To create a test, derive a class from SysTestCase class. This is all that is needed for SysTest to be able to run tests from this class. This class might have many different methods. How does SysTest know what are the main methods that should be run as unit tests?

SysTest is looking for all the methods with the following signature:

 public void test*()

All these methods are recognized as unit tests and executed.

Assertions

SysTest offers a wide range of assert methods for you to use in your tests. You can see them all in SysTestAssert class.

Method Meaning
assertEquals asserts that the two parameters are equal; when the parameters are classes implementing "equal" method then this method is called to compare them, otherwise they are compared using == operator
assertNotEqual oposite to assertEquals
assertSame compares the parameters using == operator
assertNotSame oposite to assertSame
assertRealEquals compares two real values with a provided delta; therefore if you pass number1, number2 and delta, then the assert passes when | number1 - number2 | <= delta
assertNull assert fails when the parameter is not null
assertNotNull oposite to assertNull
assertTrue assert fails when the parameter is evaluated as false
assertFalse assert fails when the parameter is evaluated as true

The class has two additional public members.

Method Meaning
info logs an information message (used for debug trace for example)
fail logs an error message and fails the current test

Let's now write a simple set of tests exercising these assert methods. The following two tests call assertEquals to verify string conversion functions:

 public void testConversionFromString()
 {
    this.assertEquals(123, str2int('123'));
    this.assertEquals(1234.56, str2num('1234.56'));
    this.assertEquals(28\7\1973, str2date('28.7.1973', 123));
 }
 
 public void testConversionToString()
 {
    this.assertEquals('123', int2str(123));
    this.assertEquals('1,234.56', num2str(1234.56, -1, 2, 1, 2));
    this.assertEquals('28.7.1973', date2str(28\07\1973, 123, 1, 2, 1, 2, 4));
 }

See that assertEquals takes anytype as a parameter and thus you can compare strings as well as dates and other base types.

Let's now try to compare objects. Assume we have the following class:

public class ComparableObject
 { 
  str name;
   
  public str parmName(str _value = name)
  {
    ;
    name = _value;
    return name;
  }
   
  public static ComparableObject construct()
  {
    ;
    return new ComparableObject();
  }
  protected void new()
  {
    ;
    name = '';
  }
 
  public boolean equal(ComparableObject _compareWith)
  {
    ;
    return (strcmp(this.parmName(), _compareWith.parmName()) == 0);
  }
  public str toString()
  {
    ;
    return name;
  }
 }

Now we can compare instances of this class using our next test:

 void testObjectComparison()
 {
    #define.name('David Pokluda')
    
    ComparableObject object1 = ComparableObject::construct();
    ComparableObject object2 = ComparableObject::construct();
    ;
    object1.parmName(#name);
    object2.parmName(#name);
    
    this.assertEquals(#name, object1.parmName());
    this.assertEquals(#name, object2.parmName());

    this.assertNotSame(object1, object2);
    this.assertEquals(object1, object2);
 }

See that assertSame would fail because those are different instances but assertEqual passes just right. That's because assertEquals is calling object's equal method to determine whether the instances are equal or not.

Note: You might also notice how SysTest reports failures on objects. Try to change the call from assertNotSame to assertSame. The framework reports something like this:

[mySimpleTests.testObjectComparison] Failure: Assertion failed! 
(Expected: David Pokluda; Actual: David Pokluda)

How does the framework know that the expected value is "David Pokluda"? Notice that I have added toString method to my object. That's what SysTest calls (when available) to report the expected and actual value for objects.

Let's now try the last two methods, info and fail.

 public void testInfo()
 {
    ;
    this.info('This is a debug message.');
 }
 
 public void testFail()
 {
    ;
    this.fail('Failed the test by calling fail method.');
 }

When you run the tests you will see that testFail failed and the message would be the one provided by us when calling fail method. But where is our info message?

If you use toolbar to run tests (Tools > Development Tools > Unit Test > Show toolbar) then in case of errors/failures, the error messages are displayed in the Infolog. Info messages are ignored in toolbar. If you want to see them you have to either use other test runner (we will cover this topic hopefully sometime later -- if requested) or you write a simple runner yourself. The following job would run our tests:

 static void RunMySimpleTests(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(MySimpleTests));
    SysTestResult result = new SysTestResult();
    ;
    suite.run(result);
    print result.getSummary();
    pause;
 }

There are different listeners (we will cover this in detail sometime later). Toolbar runner is using a modification of SysTestListenerInfolog that displays only error messages. If you want to see all the messages use either SysTestListenerPrint or SysTestListenerInfolog. Or use both. Do you see how flexible this is?

 static void RunMySimpleTests(Args _args)
 {
    SysTestSuite suite = new SysTestSuite(classstr(MySimpleTests));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
    
    pause;
 } 

Don't worry for now what all those objects mean, just use this sample as a template for your own jobs. I prefer instead of jobs to add this to the test as main method:

 public static void main(Args _params)
 {
    SysTestSuite suite = new SysTestSuite(classstr(MySimpleTests));
    SysTestResult result = new SysTestResult();
    ;
    result.addListener(new SysTestListenerPrint());
    suite.run(result);
    print result.getSummary();
 }

Why do I do that? Well because during TestDrivenDevelopment I like to run the tests all the time and there is nothing easier than pressing F5 on my keyboard.


You can download these samples: Part01: Assertions.xpo


That's it for today. In the next post I will try to explain exception handling (expected exceptions) and hopefully much more.

Posted by dpokluda | 9 Comments
Filed under: ,

SysTest quick start

Overview

I am a big fan of TestDrivenDevelopment and I am trying to follow this approach for all my development. Unfortunately DynamicsAX didn't have any UnitTest framework and thus it was difficult to use TestDrivenDevelopment. It was clear that we needed something in DynamicsAX and that's why we now have SysTest.

SysTest

SysTest is a UnitTest framework. If you are familiar with any other UnitTest framework then you will feel comfortable with SysTest.

Main features/goals:

  • it is very fast
  • it offers rich set of "assert" methods
  • it supports tests throwing exceptions (expected exceptions)
  • it supports transactions (tests can be placed inside a database transaction that is aborted at the end)
  • it supports company accounts (tests can be placed inside a separate company account that is deleted at the end of the test)
  • it supports fixture setup and tear down that are called automatically by the framework
  • it supports suites of tests
  • it supports setup and tear down method for suite that is run before the first method and after the last test method
  • it supports suites of suites
  • it supports code coverage
  • it has a wide variety of listeners for DynamicsAX environment
  • toolbar for quick test suite execution

Examples

Let's demonstrate the framework on examples. Let's create a unit test to test int2str conversion method. First we have to create our test class:

public class MySimpleTest extends SysTestCase
{
}

Now if you use toolbar (Tools > Development Tools > Unit Test > Show toolbar), then enter name of your test MySimpleTest and click Run. You immediately see "0 run, 0 failed". It's that simple. Ok, it maybe simple because it doesn't do much. Let's add a test validating that the function can successfully convert positive and negative numbers. To do so just add new method named with test prefix that is public, returns void and doesn't take any parameter. In the method check all your asserts.

public class MySimpleTest extends SysTestCase
{
    public void testConversion()
    {
        this.assertEquals('123', int2str(123));
        this.assertEquals('-2', int2str(-2));
    }
}

When you run the test in a toolbar runner for example you will immediately see "1 run, 0 failed". Great! Let's add more tests to our test class. Let's see how the framework handles tests that throw exceptions but those exceptions are expected. Because of that we don't want to fail the test method. There are two ways to do that. First one is that you will code the exception handling yourself:

    public void testExpectedException()
    {
        try
        {
            throw Exception::Error;
        }
        catch(Exception::Error)
        {
            return;
        }
        this.fail('An expected exception wasn\'t thrown!');
    }

As you can see when we expect an exception and it is not thrown then it is considered a failure. SysTest offers a better way to do the same.

    public void testExpectedException()
    {
        this.parmExceptionExpected(true);
        throw Exception::Error;
    }

Exceptions are more complex in DynamicsAX. Usually we don't just throw an error exception but we throw an error message. How do we handle that in SysTest?

public void testExpectedError() { ; this.parmExceptionExpected(true, 'Oops -- error message'); throw error('Oops -- error message'); } }

When you run the test you will get "3 run, 0 failed". Let's now see what happens when a failure occurs. We will begin by adding a failing test.

    public void testFailure()
    {
        this.assertEquals('0', int2str(123));
    }

This is clearly a failing test. When we run the test class now we get "3 run, 1 failed". Toolbar displays all failures in Infolog window. For our test class we would see the following window:



On the other hand we don't have to rely on toolbar. Let's say that we want to see all messages (not just failures) and let's say that we want to write them into DynamicsAX Print window. First let's write our own runner. For a runner we have to specify the whole suite we want to run. That's easy and all you have to do is to create instance of SysTestSuite class and pass your test class name to SysTestSuite constructor. To run the suite call run method. Here is one little problem. Suite itself is not interested in results at all. The problem is that we are and if we want to know the result at the end we have to create special SysTestResult object and pass it to the suite when it runs our test.

    public static void main(Args _params)
    {
        SysTestSuite suite = new SysTestSuite(classstr(MySimpleTest));
        SysTestResult result = new SysTestResult();
        ;
        suite.run(result);
        print result.getSummary();
    }

The code above creates the suite for our test class and executes all the tests. At the end it prints the summary report to the Print window. If we want to print all messages (failures, information, but also when a test is started or completed) then we have to register the corresponding listener.

    public static void main(Args _params)
    {
        SysTestSuite suite = new SysTestSuite(classstr(MySimpleTest));
        SysTestResult result = new SysTestResult();
        ;
        result.addListener(new SysTestListenerPrint()); // <- LISTENER REGISTERED
        suite.run(result);
        print result.getSummary();
    }

In total our whole class looks like this:

public class MySimpleTest extends SysTestCase
{
public void testConversion()
{
;
this.assertEquals('123', int2str(123));
this.assertEquals('-2', int2str(-2));
}
public void testExpectedException()
{
;
this.parmExceptionExpected(true);
throw Exception::Error;
}
public void testExpectedError()
{
;
this.parmExceptionExpected(true, 'Oops -- error message');
throw error('Oops -- error message');
}
public void testFailure()
{
;
this.assertEquals('0', int2str(123));
}

public static void main(Args _params)
{
SysTestSuite suite = new SysTestSuite(classstr(MySimpleTest));
SysTestResult result = new SysTestResult();
;
result.addListener(new SysTestListenerPrint()); // <- LISTENER REGISTERED
suite.run(result);
print result.getSummary();
}
}

That's all. If you want more information then stay tuned. In the next post I will try to describe individual features of SysTest in more details.

Posted by dpokluda | 21 Comments
Filed under: ,

Unit testing in DynamicsAX

Microsoft DynamicsAX 4.0 introduced a new development feature to MorphX -- UnitTesting. The framework is named SysTest and is very similar to all traditional UnitTesting frameworks. In a series a posts I will try to explain SysTest and I will show samples demonstrating how to work with the framework.

Posted by dpokluda | 4 Comments
Filed under: ,
 
Page view tracker