Unit testing custom Microsoft Dynamics CRM code – Part 8 (exception raising)

This is the final post in my series on unit testing custom Microsoft Dynamics CRM code in which I've been showing how you can unit test custom C# code that interacts with Microsoft Dynamics CRM using Visual Studio's unit testing tools and Moq. We've looked at several different scenarios thus far, but none of them included the method under test throwing an exception. In this post I will show you how to test exception throwing using Visual Studio's unit testing tools.

If you think back to the regular expression validation custom workflow activity we were using earlier in this series, you'll recall that we had to pass three mock objects to the WorkflowInvoker object to represent the CRM tracing service, organization service factory and workflow context. In the custom workflow activity Execute method, there are a couple of checks that throw exceptions if the workflow context or tracing service extensions are null, but obviously we want to verify we throw the correct exceptions. There are two ways to do this.

The easy way

First, let's look at the easiest way to verify an exception is thrown in the method under test. If you decorate your test method with an ExpectedExceptionAttribute, Visual Studio will only mark the test as passed if an exception of the expected type gets thrown. Here's an example:

/// <summary>
/// This tests a null workflow context using the ExpectedException approach
/// </summary>
[TestMethod]
[ExpectedException(typeof(InvalidPluginExecutionException))]
public void ValidateRegex_Test_NullContext_ExpectedException()
{

	//ARRANGE

	//set matchpattern to nanp format of xxx-xxx-xxxx
	string matchPattern = @"^[2-9]\d{2}-\d{3}-\d{4}$";

	//set string to validate to a valid phone number
	string stringToValidate = "334-867-5309";

	//create our mocks
	var serviceMock = new Mock<IOrganizationService>();
	var factoryMock = new Mock<IOrganizationServiceFactory>();
	var tracingServiceMock = new Mock<ITracingService>();
	var workflowContextMock = new Mock<IWorkflowContext>();

	//set up a mock service to act like the CRM organization service
	IOrganizationService service = serviceMock.Object;

	//set up a mock workflowcontext
	var workflowUserId = Guid.NewGuid();
	var workflowCorrelationId = Guid.NewGuid();
	var workflowInitiatingUserId = Guid.NewGuid();

	workflowContextMock.Setup(t => t.InitiatingUserId).Returns(workflowInitiatingUserId);
	workflowContextMock.Setup(t => t.CorrelationId).Returns(workflowCorrelationId);
	workflowContextMock.Setup(t => t.UserId).Returns(workflowUserId);
	var workflowContext = workflowContextMock.Object;

	//set up a mock tracingservice - will write output to console for now
	tracingServiceMock.Setup(t => t.Trace(It.IsAny<string>(), It.IsAny<object[]>())).Callback<string, object[]>((t1, t2) => Console.WriteLine(t1, t2));
	var tracingService = tracingServiceMock.Object;

	//set up a mock servicefactory
	factoryMock.Setup(t => t.CreateOrganizationService(It.IsAny<Guid>())).Returns(service);
	var factory = factoryMock.Object;

	//get new validateregex object
	ValidateRegex valRegex = new ValidateRegex();

	var invoker = new WorkflowInvoker(valRegex);
	invoker.Extensions.Add<ITracingService>(() => tracingService);
	//below line commented out to generate exception
	//invoker.Extensions.Add<IWorkflowContext>(() => workflowContext);
	invoker.Extensions.Add<IOrganizationServiceFactory>(() => factory);

	var inputs = new Dictionary<string, object> 
	{
	{ "MatchPattern", matchPattern},
	{ "StringToValidate", stringToValidate }
	};

	//ACT (assertion is implied)
	invoker.Invoke(inputs);
}

You'll note this test method is structured almost exactly the same as the method I showed in an earlier post except for three points:

  1. There is an ExpectedExceptionAttribute decoration that tells Visual Studio this test should result in an InvalidPluginExecutionException being thrown.
  2. We deliberately don't include the mocked workflow context object in the WorkflowInvoker Extensions collection (so the Execute method will see it as "null").
  3. There is no separate ASSERT logic. When the invoker is invoked, the exception will be thrown and Visual Studio will handle the implied assertion.

As is frequently the case when we chose the easy approach, there's a significant limitation here. The ExpectedExceptionAttribute lets us verify an exception of the correct type was thrown, but that's it. We cannot verify that the exception's message is what we expect. Because our custom workflow activity throws an InvalidPluginExecutionException exception for a null workflow context or a null tracing service, we are unable to verify that the message, which is reported to the calling process, is correct. For that, let's look at an alternate approach.

The slightly harder way

When I say this way is slightly harder, it's only because it involves a few more lines of code. In fact, it's probably more intuitive than the previous test method. In this approach, we execute the method under test like usual, but we do it inside a try-catch block so we can examine the thrown exception using standard Visual Studio unit testing assertions.

Here's what that looks like:

/// <summary>
/// This tests a null workflow context using the try-catch approach
/// </summary>
[TestMethod]
public void ValidateRegex_Test_NullContext_TryCatch()
{

	//ARRANGE

	//set matchpattern to nanp format of xxx-xxx-xxxx
	string matchPattern = @"^[2-9]\d{2}-\d{3}-\d{4}$";

	//set string to validate to a valid phone number
	string stringToValidate = "334-867-5309";

	//create our mocks
	var serviceMock = new Mock<IOrganizationService>();
	var factoryMock = new Mock<IOrganizationServiceFactory>();
	var tracingServiceMock = new Mock<ITracingService>();
	var workflowContextMock = new Mock<IWorkflowContext>();

	//set up a mock service to act like the CRM organization service
	IOrganizationService service = serviceMock.Object;

	//set up a mock workflowcontext
	var workflowUserId = Guid.NewGuid();
	var workflowCorrelationId = Guid.NewGuid();
	var workflowInitiatingUserId = Guid.NewGuid();

	workflowContextMock.Setup(t => t.InitiatingUserId).Returns(workflowInitiatingUserId);
	workflowContextMock.Setup(t => t.CorrelationId).Returns(workflowCorrelationId);
	workflowContextMock.Setup(t => t.UserId).Returns(workflowUserId);
	var workflowContext = workflowContextMock.Object;

	//set up a mock tracingservice - will write output to console for now
	tracingServiceMock.Setup(t => t.Trace(It.IsAny<string>(), It.IsAny<object[]>())).Callback<string, object[]>((t1, t2) => Console.WriteLine(t1, t2));
	var tracingService = tracingServiceMock.Object;

	//set up a mock servicefactory
	factoryMock.Setup(t => t.CreateOrganizationService(It.IsAny<Guid>())).Returns(service);
	var factory = factoryMock.Object;

	//get new validateregex object
	ValidateRegex valRegex = new ValidateRegex();

	var invoker = new WorkflowInvoker(valRegex);
	invoker.Extensions.Add<ITracingService>(() => tracingService);
	//below line commented out to generate exception
	//invoker.Extensions.Add<IWorkflowContext>(() => workflowContext);
	invoker.Extensions.Add<IOrganizationServiceFactory>(() => factory);

	var inputs = new Dictionary<string, object> 
	{
	{ "MatchPattern", matchPattern},
	{ "StringToValidate", stringToValidate }
	};

	//instantiate an expection object we can use to see if we threw the right thing
	Exception expectedException = null;
	
	//ACT
	try
	{
		invoker.Invoke(inputs);
	}
	catch (Exception ex)
	{
		expectedException = ex;
	}
	
	//ASSERT
	Assert.IsInstanceOfType(expectedException, typeof(InvalidPluginExecutionException));
	Assert.AreEqual("Failed to retrieve workflow context.", expectedException.Message);
}

The code samples for this example can be downloaded here, and the testing code can be downloaded here.

I hope you’ve enjoyed this series on unit testing custom Microsoft Dynamics CRM code. If you have any questions or feedback, please let me know in the comments! I’d also love to hear your ideas on other blog series topics.

A version of this post was originally published on the HP Enterprise Services Application Services blog.

comments powered by Disqus