In the past year I learned a lot about unit testing and TDD in general. After meeting some very passionate TDD type guys like Peter Provost (see this ARCast.TV interview for more) and Roy Osherove I became convinced that this style of development offers significant benefits. Of course most things you read about TDD don't really discuss unit testing in an environment like Windows Workflow Foundation (WF). Recently I've been asked how you can unit test workflows so today lets consider a simple case of testing a workflow.
To make this very simple I'm going to use a workflow that does nothing more than start and complete. I simply pass a value into it and get a value out of it.
For illustration purposes let's assume that
If you want to following along at home try this...
As Peter Provost said in this interview, the first thing we should do is ask ourselves a question. How would I know if this bit of code I'm about to write does the right thing? As we think about it, a bunch of assertions pop out.
To test these assertions I'm going to create a number of scenarios using the Behavioral Driven Development template for scenarios
Given that the Input value of 2 Ensure that the workflow returns an Output value of 4
Given that the Input value is 0 Ensure that the workflow terminates with an ArgumentOutOfRangeException
Given that the Input value is 11 Ensure that the workflow terminates with an ArgumentOutOfRangeException
Now I'm going to create my first test. At this point I do have a workflow that does nothing so the test will fail as it should. There are some interesting elements to this unit test that are commented in the code so pay careful attention
[TestMethod()] public void ShouldReturn4WhenInputIs2() { int expected = 4; int input = 2; int output = 0; // Create a runtime WorkflowRuntime runtime = new WorkflowRuntime(); // Use the manual workflow scheduler service to make the workflow execute // on the test thread synchronously ManualWorkflowSchedulerService manualScheduler = new ManualWorkflowSchedulerService(); // Add the scheduler to the runtime before it is started runtime.AddService(manualScheduler); // Handle the workflow completed event so you can capture the output runtime.WorkflowCompleted += (o, e) => { // Note you cannot Assert the value here // because failures are thrown exceptions which will // be caught by the caller - the workflowRuntime and // will not propagate to the test framework output = (int)e.OutputParameters["Output"]; }; // Setup the input parameters Dictionary args = new Dictionary(); // The name of the argument here must match the name of the property // on the workflow class args.Add("Input", input); // Create the workflow passing the arguments WorkflowInstance targetWorkflow = runtime.CreateWorkflow(typeof(Workflow1), args); // Start the workflow (note: it won't run until the scheduler runs it) targetWorkflow.Start(); // Run the workflow manualScheduler.RunWorkflow(targetWorkflow.InstanceId); // Assert the results Assert.AreEqual(expected, output); }
Now you can run the unit test (Shortcut Ctrl+R,A) and it will fail with an exception
Failed ShouldReturn4WhenInputIs2 TestProject1 Test method TestProject1.Workflow1Test.ShouldReturn4WhenInputIs2 threw exception: System.ArgumentException: The activity 'Workflow1' has no public writable property named 'Input';
This is good - it should fail because we haven't implemented the code yet. However we did make 2 decisions about the interface to this workflow. We decided that there are two public properties named Input and Output that will be used to get data in and out of the workflow
Now we want our test to turn green - to pass. I don't want to write more code than is necessary so here is the plan
Note: I am not validating arguments yet because this test does not require this.
public sealed partial class Workflow1 : SequentialWorkflowActivity { public Workflow1() { InitializeComponent(); } public int Input { get; set; } public int Output { get; set; } private void codeActivity1_ExecuteCode(object sender, EventArgs e) { Output = Input * 2; } }
Now I can run my unit test again (Ctrl+R,A) and I see that my test is passing.
The old saying goes... "Red, Green, Refactor". At this point I should take a hard look at the code and ask if it makes sense to refactor either the test code or the application code. After a quick glance I would say that there isn't much refactoring to be done here so I'll continue on.
I said that my code show through ArgumentOutOfRangeException for Input values outside of the 1-10 range. Handling exceptions from workflows is different than handling standard exceptions. For example, you can't use the [ExpectedException(...)] attribute to handle these exceptions without doing some fancy footwork as shown below, because when the workflow throws an exception the WorkflowRuntime will catch it, invoke any attached FaultHandlerActivity(s) and then it will terminate the WorkflowInstance. The test code will have to handle the WorkflowTerminated event to know about this happening as shown in this test code.
[TestMethod()] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void ShouldThrowArgumentOutOfRangeWhenInputIs0() { ArgumentOutOfRangeException workflowException = null; // A value of 0 should result in an ArgumentOutOfRangeException int input = 0; // Create a runtime WorkflowRuntime runtime = new WorkflowRuntime(); // Use the manual workflow scheduler service to make the workflow execute // on the test thread synchronously ManualWorkflowSchedulerService manualScheduler = new ManualWorkflowSchedulerService(); // Add the scheduler to the runtime before it is started runtime.AddService(manualScheduler); // Handle the WorkflowTerminated event so you can see what the exception and message were runtime.WorkflowTerminated += (o, e) => { // Note: You cannot Assert anything here // because failures are thrown as exceptions // save the value and assert later // We are expecting this kind of exception - if it is // anything else it will result in a null workflowException = e.Exception as ArgumentOutOfRangeException; }; // Setup the input parameters Dictionary args = new Dictionary(); // The name of the argument here must match the name of the property // on the workflow class args.Add("Input", input); // Create the workflow passing the arguments WorkflowInstance targetWorkflow = runtime.CreateWorkflow(typeof(Workflow1), args); // Start the workflow (note: it won't run until the scheduler runs it) targetWorkflow.Start(); // Run the workflow manualScheduler.RunWorkflow(targetWorkflow.InstanceId); // Make sure we got the right exception Assert.IsNotNull(workflowException, "No ArgumentOutOfRangeException received from workflow"); // Re throw the exception throw workflowException; }
Run the unit tests again (Ctrl+R,A) and notice that now one test is passing and the other is failing with the following exception
Failed ShouldThrowArgumentOutOfRangeWhenInputIs0 TestProject1 Assert.IsFalse failed. Workflow completed when it should have terminated because of an Input of 0
Now I need to add validation of the Input argument. To do that I've modified the code to throw an exception for an invalid value on the low end of the range
private void codeActivity1_ExecuteCode(object sender, EventArgs e) { if (Input < 1) throw new ArgumentOutOfRangeException("Input"); Output = Input * 2; }
Now we have some duplication of code between tests. Both of them create a runtime and manual scheduler. I could refactor the code to create a helper method that would create the runtime and add the manual scheduler but it won't save many lines of code and would probably just make the test code a little more complex. On the other hand I need to create another validation test that will be exactly like the first one but with a different value. This is a great opportunity to refactor by extracting the body of that test into a method that will test to see that an invalid input arg (of whatever value) results in an ArgumentOutOfRangeException
Since the refactoring took place, this is now a simple 1 line of code to invoke the extracted method with a different parameter. My two test methods now look like this.
[TestMethod()] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void ShouldThrowArgumentOutOfRangeWhenInputIs0() { ShouldThrowExceptionOnInvalidValue(0); } [TestMethod()] [ExpectedException(typeof(ArgumentOutOfRangeException))] public void ShouldThrowArgumentOutOfRangeWhenInputIs11() { ShouldThrowExceptionOnInvalidValue(11); }
Now I run my test and see it fail for an input of 11. All that remains now is to validate the upper bound of the argument and throw an argument exception
You might ask - why didn't I do this earlier when I modified the code to check the lower bound? I suppose it is just a habit, a discipline if you will to not do anything until a test requires it. This habit insures that nothing in my code is untested. So once again I modify my code to handle the upper bound validation
private void codeActivity1_ExecuteCode(object sender, EventArgs e) { if (Input < 1) throw new ArgumentOutOfRangeException("Input"); if (Input > 10) throw new ArgumentOutOfRangeException("Input"); Output = Input * 2; }
Run the unit tests again (Ctrl+R,A) - now all tests are passing.
If this seems like a lot of work... well, let's just admit that it is. Could you have written a simple workflow like this in less time? Why bother with all this testing?
I bother with it because I, like you, have experienced too many applications that just didn't work. Most people verify that their application worked at some point in the past. But as things change around it, platform, server, surrounding code, config etc. things can break.
Having these three tests for my simple workflow will give me a way to know for certain that the code is verifiably correct in the future. And that is worth the effort.
When I started on this blog post this morning I was surprised by several things along the way. Next time I'll share with you the lessons learned so these little issues don't surprise you.
Sample code for this test is here
PingBack from http://informationsfunnywallpaper.cn/?p=2361
Useful example. Well presented.
A while a go I write some posts about unit testing Windows Workflow foundation Activities, you can find
Most of the presentations you have probably seen on Windows Workflow Foundation (WF), including the one