Category Archives: 8964

Running Fitnesse.NET tests using MSTest – Revisited

In 2008 I wrote a post Running Fitnesse.NET tests using MSTest.  Recently I had need to us this technique on a VS2010 project, and as is so often the issue the code that worked then does not seem to work now. All I can assume is that the Fitnesse API had altered, but I thought I was using the same assemblies!

So I pulled down the code from http://sourceforge.net/projects/fitnessedotnet/ and had a poke about. Basically I seemed to be using the command line switches wrong. The listing below shows what I found is the working usage:

[TestMethod]
    public void WorkFlowSwitchOnName_DataViaFitnesse_Success()
    {
        fit.Runner.FolderRunner runner = new fit.Runner.FolderRunner(new fit.Runner.ConsoleReporter());
        var errorCount = runner.Run(new string[] {
                "-i",@"WF Workflow", // the directory that holds the HTM test files
                "-a",@"TestProject.dll",  //we have the fit fascard in this assembly
                "-o",@"results"}); // the directory the results are dumped into as HTML
        // fit can fail silently giving no failures as no test are run, so check for exceptions
        Assert.AreEqual(false, Regex.IsMatch(runner.Results, "^0.+?0.+?0.+?0.+?$"), "No tests appear to have been run");
        // look for expected errors
        Assert.AreEqual(0, errorCount, runner.Results);
        
    }


The –i option is the one that was wrong. I had been specifying a single HTM file. What I should have done was specify a directory that contains one or more HTM files. I handled this as a sub directory in my project with its contents set to copy to the output directory.



Once I made these edits I had my tests running again as expect.

Lessons learnt building a custom activity to run Typemock Isolator in VS2010 Team Build

I have previously posted on how you can run Typemock Isolator based tests within a VS2010 using the InvokeMethod activity. After this post Gian Maria Ricci, a fellow Team System MVP suggested it would be better to put this functionality in a custom code activity, and provided the basis of the solution. I have taken this base sample and worked it up to be a functional activity, and boy have I learnt a few things doing it.

Getting the custom activity into a team build

Coding up a custom Team Build activity is not easy, there are a good few posts on the subject (Jim Lamb’s is a good place to start). The problem is not writing the code but getting the activity into the VS toolbox. All documentation gives basically the same complex manual process, there is no way of avoiding it. Hopefully this will be addressed in a future release of Visual Studio. But for now the basic process is this:

  1. Create a Class Library project in your language of choice
  2. Code up your activity inheriting it from the CodeActivity<T> class
  3. Branch the build workflow, that you wish to use for testing, into the folder of the class library project
  4. Add the build workflow’s .XAML file to the class library project then set it’s properties: “build action” to none and “copy to output directory” to do not copy
  5. Open the .XAML file (in VS2010), the new activity should appear in the toolbox, it can be dropped onto the workflow. Set the properties required.
  6. Check in the file .XAML file
  7. Merge the .XAML file to the original location, if you get conflicts simply tell merge to use the new version discarding the original version, so effectively overwriting the original version with the version edited in the project.
  8. Check in the merged original .XAML file that now contains the modifications.
  9. Take the .DLL containing the new activity and place it in a folder under source control (usually under the BuildProcessTemplates folder)
  10. Set the Build Controller’s custom assemblies path to point to this folder (so your custom activity can be loaded) 

    image
  11. Run the build and all should be fine

But of course is wasn’t. I kept getting the error when I ran a build

TF215097: An error occurred while initializing a build for build definition \Typemock Test\BuildTest Branch: Cannot create unknown type '{clr-namespace:TypemockBuildActivity}ExternalTestRunner'.

This was because I had not followed the procedure correctly. I had tried to be clever. Instead of step 6 and onwards I had had an idea. I created a new build that referenced the branched copy of the .XAML file in the class library project directly. I thought this would save me a good deal of tedious merging while I was debugged my process. It did do this but introduced other issues

The problem was when I inspected the .XAML in my trusty copy of Notepad, I saw that there was no namespace declared for my assembly (as the TF21509 error suggested). If I looked at the actual activity call in the file it was declared as <local:ExternalTestRunner  …… />, the local: replacing the namespace reference I would expect. This is obviously down to the way I was editing the .XAML file in the VS2010.

The fix is easy, using Notepad I added a namespace declaration to the Activity block

<Activity ……    xmlns:t="clr-namespace:TypemockBuildActivity;assembly=TypemockBuildActivity" >

and then edited the references from local: to t: (the alias for my namespace) for any classes called from the custom assembly e.g.

<t:ExternalTestRunner ResultsFileRoot="{x:Null}" BuildNumber="[BuildDetail.Uri.ToString()]" Flavor="[platformConfiguration.Configuration]" sap:VirtualizedContainerService.HintSize="200,22" MsTestExecutable="C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe" Platform="[platformConfiguration.Platform]" ProjectCollection="http://typhoon:8080/tfs/DefaultCollection" Result="[ExternalTestRunnerResult]" ResultsFile="ExternalTestRunner.Trx" SearchPathRoot="[outputDirectory]" TeamProjectName="[BuildDetail.TeamProject]" TestAssemblyNames="[testAssemblies.ToArray()]" TestRunnerExecutable="C:\Program Files (x86)\Typemock\Isolator\6.0\TMockRunner.exe" TestSettings="[localTestSettings]" />

Once this was done I could use my custom activity in a Team Build, though I had to make this manual edit every time I edited the branched .XAML file in VS2010 IDE. So I had swapped repeated merges with repeated editing, you take your view as to which is worst.

So what is in my Typemock external test runner custom activity?

The activity is basically the same as the one suggest by Gian Maria, it takes all the same parameters as the MSTest team build activity and then executes the TMockRunner to wrapper MSTest. What I have done is add a couple of parameters that were missing in the original sample and also added some more error traps and logging.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.IO;
using System.Diagnostics;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.Build.Client;
using System.Text.RegularExpressions;
 
namespace TypemockBuildActivity
{
    public enum ExternalTestRunnerReturnCode { Unknown =0 , NotRun, Passed, Failed };
 
    [BuildExtension(HostEnvironmentOption.Agent)]
    [BuildActivity(HostEnvironmentOption.All)]
    public sealed class ExternalTestRunner : CodeActivity<ExternalTestRunnerReturnCode>
    {
        // Define an activity input argument of type string  
 
        /// <summary>
        /// The name of the wrapper application, usually tmockrunner.exe
        /// </summary>
        public InArgument<string> TestRunnerExecutable { get; set; }
        
        /// <summary>
        /// The name of the application that actually runs the test, defaults to MSTest.exe if not set
        /// </summary>
        public InArgument<string> MsTestExecutable { get; set; }
 
        /// <summary>
        /// The project collection to publish to e.g. http://tfs2010:8080/tfs/DefaultCollection
        /// </summary>
        public InArgument<string> ProjectCollection { get; set; }
 
        /// <summary>
        /// The build ID to to publish to e.g. vstfs:///Build/Build/91
        /// </summary>
        public InArgument<string> BuildNumber { get; set; }
 
        /// <summary>
        /// The project name to publish to e.g: "Typemock Test"
        /// </summary>
        public InArgument<string> TeamProjectName { get; set; }
 
        /// <summary>
        /// The platform name to publish to e.g. Any CPU
        /// </summary>
        public InArgument<string> Platform { get; set; }
 
        /// <summary>
        /// The flavour (configuration) to publish to e.g. "Debug"
        /// </summary>
        public InArgument<string> Flavor { get; set; }
 
 
        /// <summary>
        /// Array of assembly names to test
        /// </summary>
        public InArgument<string[]> TestAssemblyNames { get; set; }
        
        /// <summary>
        /// Where to search for assemblies under test
        /// </summary>
        public InArgument<string> SearchPathRoot { get; set; }
        
        /// <summary>
        /// A single name result file
        /// </summary>
        public InArgument<string> ResultsFile { get; set; }
 
        /// <summary>
        /// A directory to store results in (tends not be used if the ResultFile is set)
        /// </summary>
        public InArgument<string> ResultsFileRoot { get; set; }
 
        /// <summary>
        /// The file that list as to how test should be run
        /// </summary>
        public InArgument<string> TestSettings { get; set; }
 
 
        // If your activity returns a value, derive from CodeActivity<TResult> 
        // and return the value from the Execute method. 
        protected override ExternalTestRunnerReturnCode Execute(CodeActivityContext context)
        {
            String msTestOutput = string.Empty;
            ExternalTestRunnerReturnCode exitMessage = ExternalTestRunnerReturnCode.NotRun;
 
            if (CheckFileExists(TestRunnerExecutable.Get(context)) == false)
            {
                LogError(context, string.Format("TestRunner not found {0}", TestRunnerExecutable.Get(context)));
            }
            else
            {
                String mstest = MsTestExecutable.Get(context);
                if (CheckFileExists(mstest) == false)
                {
                    mstest = GetDefaultMsTestPath();
                }
 
                String testrunner = TestRunnerExecutable.Get(context);
 
                var arguments = new StringBuilder();
                arguments.Append(string.Format("\"{0}\"", mstest));
                arguments.Append(" /nologo ");
 
                // the files to test
                foreach (string name in TestAssemblyNames.Get(context))
                {
                    arguments.Append(AddParameterIfNotNull("testcontainer", name));
                }
 
                // settings about what to test
                arguments.Append(AddParameterIfNotNull("searchpathroot", SearchPathRoot.Get(context)));
                arguments.Append(AddParameterIfNotNull("testSettings", TestSettings.Get(context)));
                
                // now the publish bits
                if (string.IsNullOrEmpty(ProjectCollection.Get(context)) == false)
                {
                    arguments.Append(AddParameterIfNotNull("publish", ProjectCollection.Get(context)));
                    arguments.Append(AddParameterIfNotNull("publishbuild", BuildNumber.Get(context)));
                    arguments.Append(AddParameterIfNotNull("teamproject", TeamProjectName.Get(context)));
                    arguments.Append(AddParameterIfNotNull("platform", Platform.Get(context)));
                    arguments.Append(AddParameterIfNotNull("flavor", Flavor.Get(context)));
                }
 
                // where do the results go, tend to use one of these not both
                arguments.Append(AddParameterIfNotNull("resultsfile", ResultsFile.Get(context)));
                arguments.Append(AddParameterIfNotNull("resultsfileroot", ResultsFileRoot.Get(context)));
 
                LogMessage(context, string.Format("Call Mstest With Wrapper [{0}] and arguments [{1}]", testrunner, arguments.ToString()), BuildMessageImportance.Normal);
 
                using (System.Diagnostics.Process process = new System.Diagnostics.Process())
                {
                    process.StartInfo.FileName = testrunner;
                    process.StartInfo.WorkingDirectory = SearchPathRoot.Get(context);
                    process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
                    process.StartInfo.UseShellExecute = false;
                    process.StartInfo.ErrorDialog = false;
                    process.StartInfo.CreateNoWindow = true;
                    process.StartInfo.RedirectStandardOutput = true;
                    process.StartInfo.Arguments = arguments.ToString();
                    try
                    {
                        process.Start();
                        msTestOutput = process.StandardOutput.ReadToEnd();
                        process.WaitForExit();
                        // for TypemockRunner and MSTest this is alway seems to be 1 so does not help tell if test passed or not
                        //  In general you can detect test failures by simply checking whether mstest.exe returned 0 or not.  
                        // I say in general because there is a known bug where on certain OSes mstest.exe sometimes returns 128 whether 
                        // successful or not, so mstest.exe 10.0 added a new command-line option /usestderr which causes it to write 
                        // something to standard error on failure.
 
                        // If (error data received)
                        //    FAIL
                        // Else If (exit code != 0 AND exit code != 128)
                        //    FAIL
                        // Else If (exit code == 128)
                        //    Write Warning about weird error code, but SUCCEED
                        // Else
                        //   SUCCEED
 
                        ///int exitCode = process.ExitCode;
                        LogMessage(context, string.Format("Output of ExternalTestRunner: {0}", msTestOutput), BuildMessageImportance.High);
                    }
                    catch (InvalidOperationException ex)
                    {
                        LogError(context, "ExternalTestRunner InvalidOperationException :" + ex.Message);
                    }
 
                    exitMessage = ParseResultsForSummary(msTestOutput);
                }
            }
            LogMessage(context, string.Format("ExternaTestRunner exiting with message [{0}]", exitMessage), BuildMessageImportance.High);
            return exitMessage;
        }
 
        /// <summary>
        /// Adds a parameter to the MSTest line, it has been extracted to allow us to do a isEmpty chekc in one place
        /// </summary>
        /// <param name="parameterName">The name of the parameter</param>
        /// <param name="value">The string value</param>
        /// <returns>If the value is present a formated block is return</returns>
        private static string AddParameterIfNotNull(string parameterName, string value)
        {
            var returnValue = string.Empty;
            if (string.IsNullOrEmpty(value) == false)
            {
                returnValue = string.Format(" /{0}:\"{1}\"", parameterName, value);
            }
            return returnValue;
       }
 
        /// <summary>
        /// A handler to check the results for the success or failure message
        /// This is a rough way to do it, but is more reliable than the MSTest exit codes
        /// It returns a string as opposed to an  exit code so that it 
        /// Note this will not work of the /usestderr flag is used
        /// </summary>
        /// <param name="output">The output from the test run</param>
        /// <returns>A single line summary</returns>
        private static ExternalTestRunnerReturnCode ParseResultsForSummary(String output)
        {
            ExternalTestRunnerReturnCode exitMessage = ExternalTestRunnerReturnCode.NotRun;
            if (Regex.IsMatch(output, "Test Run Failed"))
            {
                exitMessage = ExternalTestRunnerReturnCode.Failed;
            }
            else if (Regex.IsMatch(output, "Test Run Completed"))
            {
                exitMessage = ExternalTestRunnerReturnCode.Passed;
            }
            else
            {
                exitMessage = ExternalTestRunnerReturnCode.Unknown;
            }
 
            return exitMessage;
        }
 
        /// <summary>
        /// Handles finding MSTest, checking both the 32 and 64 bit paths
        /// </summary>
        /// <returns></returns>
        private static string GetDefaultMsTestPath()
        {
            String mstest = @"C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\mstest.exe";
            if (CheckFileExists(mstest) == false)
            {
                mstest = @"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe";
                if (CheckFileExists(mstest) == false)
                {
                    throw new System.IO.FileNotFoundException("MsTest file cannot be found");
                }
            }
            return mstest;
        }
 
        /// <summary>
        /// Helper method so we log in both the VS Build and Debugger modes
        /// </summary>
        /// <param name="context">The workflow context</param>
        /// <param name="message">Our message</param>
        /// <param name="logLevel">Team build importance level</param>
        private static void LogMessage(CodeActivityContext context, string message, BuildMessageImportance logLevel)
        {
            TrackingExtensions.TrackBuildMessage(context, message, logLevel);
            Debug.WriteLine(message);
        }
 
        /// <summary>
        /// Helper method so we log in both the VS Build and Debugger modes
        /// </summary>
        /// <param name="context">The workflow context</param>
        /// <param name="message">Our message</param>
        private static void LogError(CodeActivityContext context, string message)
        {
            TrackingExtensions.TrackBuildError(context, message);
            Debug.WriteLine(message);
        }
 
        /// <summary>
        /// Helper to check a file name to make sure it not null and that the file it name is present
        /// </summary>
        /// <param name="fileName"></param>
        /// <returns></returns>
        private static bool CheckFileExists(string fileName)
        {
            return !((string.IsNullOrEmpty(fileName) == true) || (File.Exists(fileName) == false));
        }
    }
}




This activity does need a good bit of configuring to use it in a real build. However, as said previously, the options it takes are basically those needed for the MSTest activity, so you just replace the existing calls to the MSTest activities as shown in the graph below should be enough.



image



Note: The version of the ExternalTestRunner activity in this post does not handle tests based on Metadata parameters (blue box above), but should be OK for all other usages (it is just that these parameters have not been wired through yet). The red box show the new activity in place (this is the path taken if the tests are controlled by a test setting file) and the green box contains an MSTest activity waiting to be swapped out (this is the path taken if no test setting or metadata files are provided).



The parameters on the activity in the red box are as follows, as said before they are basically the same as parameters for the standard MSTest activity.



image



The Result parameter (the Execute() method return value) does need to be associated with a variable declared in the workflow, in my case ExternalTestRunnerResult. This is defined at the sequence scope, the scope it is defined at must be such that it can be read by any other steps in the workflow that require the value. It is declared as being of the enum type ExternalTestRunnerReturnCode defined in the custom activity.



image



Further on in the workflow you need to edit the if statement that branches on whether the tests passed or not to use this ExtenalTestRunnerResult value



image



Once all this is done you should have all your MSTests running inside a Typemock’ed wrapper and all the results should be shown correctly in the build summary



image



And the log of the build should show you all the parameters that got passed through to the MSTest program.



image



Is there a better way to test a custom activity project?



Whilst sorting out the logic for the custom activity I did not want to have to go through the whole process of running the team build to test the activity, it just took too long. To speed this process I did the following



  1. In my solution I created a new Console Workflow project
  2. I referenced my custom activity project from this new workflow project
  3. I added my custom activity as the only item in my workflow
  4. For each parameter of the custom activity I created a matching argument for the workflow and wired the two together.

    image
  5. I then created a Test Project that referenced the workflow project and custom activity project.
  6. In this I could write unit tests (well more integration tests really) that exercise many of the options in the custom activity. To help in this process I created some simple Test Projects assemblies that contained just passing tests, just failing test and a mixture of both.
  7. A sample test is shown below
    [TestMethod]
    public void RunTestWithTwoNamedAssembly_OnePassingOneFailingTestsNoPublishNoMstestSpecified_FailMessage()
    {
     
        // make sure we have no results file, MSTest fails if the file is present
        File.Delete(Directory.GetCurrentDirectory() + @"\TestResult.trx");
     
        var wf = new Workflow1();
     
        Dictionary<string, object> wfParams = new Dictionary<string, object>
        {
            { "BuildNumber", string.Empty },
            { "Flavour", "Debug" },
            { "MsTestExecutable", string.Empty },
            { "Platform", "Any CPU" },
            { "ProjectCollection",string.Empty },
            { "TeamProjectName", string.Empty },
            { "TestAssemblyNames", new string[] { 
                Directory.GetCurrentDirectory() + @"\TestProjectWithPassingTest.dll",
                Directory.GetCurrentDirectory() + @"\TestProjectWithfailingTest.dll"
            }},
            { "TestRunnerExecutable", @"C:\Program Files (x86)\Typemock\Isolator\6.0\TMockRunner.exe" },
            { "ResultsFile", "TestResult.trx" }
        };
     
     
        var results = WorkflowInvoker.Invoke(wf, wfParams);
     
        Assert.AreEqual(TypemockBuildActivity.ExternalTestRunnerReturnCode.Failed, results["ResultSummary"]);
    }
  8. The only real limit here is that some of the options (the publishing ones) need a TFS server to be tested. You have to make a choice as to whether this type of publishing test is worth the effort of filling your local TFS server with test runs  from the test project or whether you want to test these features manually in a real build environment, especially give the issues I mention in my past post












Summary



So I have a working implementation of a custom activity that makes it easy to run Typemock based tests without losing any of the other features of a Team Build. Butt as I learnt getting around the deployment issues can be a real pain.

Testing SharePoint Workflows using TypeMock Isolator

I have for a while been trying to test SharePoint workflows using TypeMock Isolator to mock out the SharePoint fixtures, I want to remove the dependency of having SharePoint on any test boxes where possible. I have at last got this working after getting a new version of TypeMock Isolator 5.3.0 + a fix from the very helpful team at TypeMock


My idea was to be able to build a workflow that could changed list item properties for a document e.g. the workflow could set a field called approved to true if certain criteria were met. Now as a MOSS2007 workflow is based on .NET WF I knew I could try to build upon the work I document in my previous post on TDD for WF.


My test system was as follows:


  1. I created a new SharePoint workflow, all it contained was a decision box that went down the true path if the document associated with the workflow has a title starting with the letter A
  2. In a coded action for the true path, I then set an approved property to true.

All very simple, but good enough for this test, the key methods are shown below

    private void IfTest(object sender, ConditionalEventArgs e)
    {
       var currentItem = workflowProperties.Item; 
e.Result = currentItem.Title.StartsWith("A"); } private void TrueTask(object sender, EventArgs e) { var currentItem = workflowProperties.Item; currentItem["Approved"] = true.ToString(); currentItem.Update(); }

I then created a test using the same form I did for WF based testing, I think the comments cover the key points

  [TestMethod]
        public void WorkFlowSwitchOnTitle_TitleStartsWithA_SetApprovelField()
        {
            using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
            {

                // Create our fake workflow and items
                var fakeProperties = Isolate.Fake.Instance<SPWorkflowActivationProperties>(Members.ReturnRecursiveFakes);
                var fakeItem = Isolate.Fake.Instance<SPListItem>(Members.ReturnRecursiveFakes);

                var fakeField = Isolate.Fake.Instance<SPField>(Members.ReturnRecursiveFakes);
                fakeField.DefaultValue = false.ToString();
                Isolate.WhenCalled(() => fakeProperties.Item).WillReturn(fakeItem);
// setup the if test Isolate.WhenCalled(() => fakeItem.Title).WillReturn("ABC"); Isolate.WhenCalled(() => fakeItem["Approved"]).WillReturn(fakeField); // setup the workflow handling
AutoResetEvent waitHandle = new AutoResetEvent(false); workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) { // don't put asserts here as will be in the wrong thread waitHandle.Set(); }; workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { // don't put asserts here as will be in the wrong thread waitHandle.Set(); }; // when this is called the constructor is called twice // the first time is for validation for the workflow in this appdomain see http://odetocode.com/Blogs/scott/archive/2006/03/30/3192.aspx // then the real construction is run, the problem is this double run means that the Isolate.Swap.NextInstance // fails as it attaches to the first validation create, not the second real one WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(SharePointWorkflow.Workflow1)); // SO for this reason we only get the swap after the first create has been done Isolate.Swap.NextInstance<SPWorkflowActivationProperties>().With(fakeProperties); // we then recreate the workflow again, this time it has already been validated so // the swap works instance = workflowRuntime.CreateWorkflow(typeof(SharePointWorkflow.Workflow1)); instance.Start(); waitHandle.WaitOne();
// wait for the workflow to complete and then check the method expected were called
Isolate.Verify.WasCalledWithExactArguments(() => fakeItem.Update()); Isolate.Verify.WasCalledWithExactArguments(() => fakeItem["Approved"] = "True"); } }

If you try this without the fix TypeMock provided me with, the two verifies will fail, I am told this is due to a threading issue. Interesting that this is SharePoint specific as the same basic method works OK for standard WF workflows.


I also understand this fix will be in the next TypeMock release, I will update this post when I know for sure

Testing Driven Development for Workflow Foundation

As we move into the SOA world workflows will become more common and so the need to test them will increase. If we take the most simplistic view these are a sets if statements and loops so should be amenable to automated testing and TDD.

However you have to careful how you try to do this, it is too easy to forget that your workflow will run the the WF thread. Why is this problem?

Consider this scenario, if you create a simple sequential WF console application you get a block of code like this in the Main(args) method

using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
   AutoResetEvent waitHandle = new AutoResetEvent(false);
   workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
   {
      // could put asserts here (A)
      waitHandle.Set();
   };

   workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
   {
          // could put asserts here (B)
   
   waitHandle.Set();
   };

  WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowConsoleApplication1.Workflow1));
   instance.Start();

   waitHandle.WaitOne();
}

// could put asserts here (C)

This block of code will be the basis of each of your unit tests, but as indicated you could put your test asserts at one of three places

  • In the success delegate
  • In the fail/terminate delegate
  • After the workflow has completed

The best place is the third option, when the workflow has finished, even though you know in a test whether it should completed OK or failed. The problem with the previous two locations is that the anonymous delegate will be run in the WF worker thread. Interestingly, if the assert passes in this location all will be OK, but if it fails the thrown assert exception (the mechanism used in nUnit to trap failure and in other test frameworks to my knowledge) will be in the wrong thread and so will not be picked up by the test harness, so in effect the test runner stalls waiting for completion or an exception, neither of which arrive. This issue is not the the case if the asserts are done when the workflow completes. This does mean that you have to pass results out of the workflow, but this should not be a major issue as the results should be simple objects (workflow parameters) or checks on external (mock?) systems, such as file systems, DBs or Smtp servers.

Though not always standard practice in TDD, I think it is a good idea here to move the code in the above sample’s using statement into a static method in your test class; it is boiler plate that will probably be use in many tests. As tests are meant to readable I think a single line call to the RunWorkFlow() method is better than a block of repeated loop and delegate code. Remember you do have to be careful here, as you may need to pass in parameters and get a return value to get the end state of the workflow; the requirement will depend what your workflow does and how it interacts with other systems. It maybe you need more than one version of the RunWorkFlow() static method depending on what is under test e.g one to harvest output parameters from completed workflow runs and another to harvest inner exceptions that are inside the workflow terminated delegate.

So now there is not reason to not test your WF workflows – assuming you can mock out any external connections, and if you are using web services or IoC to talk to data stores this should be fairly easy. 

Next I am off to try to do the same with SharePoint workflows.