Programming Microsoft Dynamics CRM 4.0: Plug-ins
- 12/15/2008
Unit Testing
Automated unit testing, used to validate the individual units of functionality in a program, continues to gain momentum and popularity in the software development community. Unit testing can improve the quality of an application and reduce the risk of breaking functionality when changes are made to the code.
Taken a step further, you can use unit tests as a design tool. Test-driven design (TDD) is a methodology that dictates that unit tests should be written before implementing the feature. The developer then implements the functionality in the simplest way possible to satisfy the unit test.
Mock Objects
Writing unit tests that depend on an external data source, such as the CRM Web Services, introduces additional challenges. Every time a test runs, the state of the server impacts the test results, causing tests that previously passed to unexpectedly fail. Because tests should only start to fail when the code changes, this server dependency needs to be removed.
Fortunately, nothing in the plug-in definition dictates that it must communicate with a live server. A plug-in only references a single type in its definition, IPluginExecutionContext. Because IPluginExecutionContext is an interface, we can provide our own implementation during testing and remove the dependency on the server. This concept of providing a “fake” implementation of an abstract type is commonly called mocking in automated unit testing.
Test Frameworks
In our sample test, we will take advantage of two testing frameworks. The Microsoft Unit Testing Framework, commonly referred to as MSTest, is now included in all editions of Visual Studio 2008, with the exception of the Express edition. This framework provides custom attributes used to decorate test classes and a library of assertions that you can use within your tests to validate the actual results against the expected results. In addition, MSTest integrates with the Visual Studio 2008 user interface and allows you to execute your tests without leaving the development environment.
A framework called Rhino Mocks provides our mock IPluginExecutionContext implementation. Rhino Mocks works by generating classes on the fly that can implement a specific interface or extend a base class. As the test authors, we define which methods the tested functionality will call and what should be returned when those calls are made.
Sample Test
Now we will walk through the implementation of an automated unit test that verifies that our AccountNumberValidator plug-in is implemented correctly. Before we can write our first test, we need to include a test project in our solution.
Adding the test project
On the File Menu, select Add and then click New Project.
In the New Project dialog box, within the Visual C# > Test project types, select the Test Project template targeting the .Net Framework 3.0.
Type the name ProgrammingWithDynamicsCrm4.PluginTests in to the Name boxand click OK.
Delete the default UnitTest1.cs file.
Right-click the ProgrammingWithDynamicsCrm4.PluginTests project in Solution Explorer and then click Add Reference.
On the Browse tab, navigate to the CRM SDK’s bin folder and select microsoft.crm.sdk.dll and microsoft.crm.sdktypeproxy.dll. Click OK.
Right-click the ProgrammingWithDynamicsCrm4.PluginTests project in Solution Explorer and then click Add Reference.
On the Project tab, select the ProgrammingWithDynamicsCrm4.Plugins project and click OK.
Now we can add our test class. Typically you would add a unit test to your project, which already contains sample code, but to introduce things one at a time, we will build the class from scratch. Create a class named AccountNumberValidatorTests and enter the code from Example 5-18.
Example 5-18. The empty AccountNumberValidatorTests class
using System; using Microsoft.Crm.Sdk; using Microsoft.VisualStudio.TestTools.UnitTesting; using ProgrammingWithDynamicsCrm4.Plugins; namespace ProgrammingWithDynamicsCrm4.PluginTests { [TestClass] public class AccountNumberValidatorTests { } }
Note the inclusion of the TestClass attribute on the AccountNumberValidatorTests class. This attribute, defined by the MSTest framework, indicates that the AccountNumberValidatorTests class contains tests and should be included when tests are run.
To define our first test, add the following code to the AccountNumberValidatorTests class:
[TestMethod] public void TestInvalidFormat() { AccountNumberValidator validator = new AccountNumberValidator(); validator.Execute(null); }
Similar to the TestClass attribute previously discussed, the TestMethod attribute identifies a method that represents a test within the test class. When all tests are run, MSTest will iterate through all the classes marked with a TestClass attribute and execute the methods marked with a TestMethod attribute individually.
You can run this test by selecting Test > Run > Tests in Current Context from the menu, but at this point it will always fail with the message “Test method ProgrammingWithDynamicsCrm4.PluginTests. AccountNumberValidatorTests.TestInvalidFormat threw exception: System.NullReferenceException: Object reference not set to an instance of an object.” This makes sense because the AccountNumberValidator plug-in expects a valid (non-null) IPluginExecutionContext to be passed in to the Execute method.
To provide an implementation of the IPluginExecutionContext interface to the AccountNumberValidator class, we need to add a reference to the Rhino Mocks framework.
Adding the Rhino Mocks Reference
Download and extract the latest stable build of the Rhino Mocks framework that targets .NET 2.0 from http://www.ayende.com/projects/rhino-mocks/downloads.aspx.
Right-click the ProgrammingWithDynamicsCrm4.PluginTests project in Solution Explorer and then click Add Reference.
On the Browse tab, navigate to the Rhino Mocks framework folder and select Rhino. Mocks.dll. Click OK.
Before we define our mock implementation, we should add a using statement to the top of the AccountNumberValidatorTests.cs file to make references to the framework easier:
using Rhino.Mocks;
With the Rhino Mocks framework properly referenced, we can modify the TestInvalidFormat method to match Example 5-19.
Example 5-19. The TestInvalidFormat method updated with a mock IPluginExecutionContext
[TestMethod] public void TestInvalidFormat() { MockRepository mocks = new MockRepository(); IPluginExecutionContext context = mocks.CreateMock<IPluginExecutionContext>(); PropertyBag inputParameters = new PropertyBag(); DynamicEntity account = new DynamicEntity(); account["accountnumber"] = "123456"; inputParameters[ParameterName.Target] = account; using (mocks.Record()) { Expect.On(context).Call(context.InputParameters).Return(inputParameters); } using (mocks.Playback()) { AccountNumberValidator validator = new AccountNumberValidator(); validator.Execute(context); } }
The first difference we notice is the inclusion of the mocks variable. This instance of the MockRepository class is responsible for creating instances of our mock classes, as well as switching between record and playback modes, which we will discuss shortly. Creating an instance of a mock object is as simple as calling the CreateMock method and passing in the type you want to mock in the generics argument.
The next steps revolve around defining the expected use of the mock object. The AccountNumberValidator plug-in only accesses the InputParameters property on the IPluginExecutionContext. To avoid an error during test execution, we need to let Rhino Mocksk now how it should respond when the InputParameters property is accessed. We begin by creating an instance of a PropertyBag and setting up the target property in it to contain a simple instance of a DynamicEntity with a short name.
With the local version of inputParameters set up and ready to go, we tell our MockRepository to switch to record mode. Record mode allows us to define our expectations on any mock objects. The next line might look a little odd if you are not used to dealing with mock frameworks. It reads more like English than typical C# code and tells the MockRepository to expect a call for the InputParameters property on the mock IPluginExecutionContext. It goes on to say that when this call is made, return our local inputParameters variable.
With the expectations defined on our mock object, we switch the MockRepository to playback mode, in which all the expectations must be met as defined during the record mode.
Finally, we pass our mock IPluginExecutionContext in to the AccountNumberValidator’s Execute method. If we run our test at this point, however, we still get a failure with the message: “Test method ProgrammingWithDynamicsCrm4.PluginTests.AccountNumberValidatorTests.TestShortName threw exception: Microsoft.Crm.Sdk.InvalidPluginExecutionException: Account number does not follow the required format (AA-######).” This, of course, is the expected behavior for our plug-in and means that it is validating as expected.
Tests that require an exception to be thrown in order for the test to pass have an additional attribute at their disposal. The ExpectedException attribute is applied at the method level and notifies MSTest that for this test to pass, the specific exception must be thrown. An example of the ExpectedException attribute applied to our TestInvalidFormat method can be seen here:
[TestMethod] [ExpectedException(typeof(InvalidPluginExecutionException), "Account number does not follow the required format (AA-######).")] public void TestInvalidFormat() { ... }
With this addition our test will run and pass every time, unless the AccountNumberValidator code is modified to change the behavior. If the test fails, it is up to the developer to modify the test accordingly—to include the new functionality or determine whether the new code has inadvertently broken something that was previously working.
For this test class to be complete, it should minimally test account numbers that are in a valid format as well. It could additionally test for a null IPluginExecutionContext or an InputParameters property that does not include a value for the “target” key. All thes escenarios would be simple to include using the techniques already demonstrated.