Unit Testing in K2

Introduction

I have been researching testing K2 workflows for a client over the last couple of days and I came across this white paper in my library and I thought I would share it with you all.

Development teams have been using automated tools, such as Visual Studio Test System (VSTS), as part of their Software Development Lifecycle to automate the unit testing of the application software artifacts. This document covers how to use VSTS to automate the unit testing of K2 processes. The same principles can be applied using other automated unit testing frameworks such as NUnit.

K2 processes are exposed directly through a client API as well as through a service layer. The service layer includes both WCF, REST, and legacy SOAP (WS) services. This document discusses how to use the client API, SourceCode.Workflow.Client, in VSTS unit tests to start and action K2 processes to test that the processes behave as expected.

This document shows how to set up a Visual Studio solution to test K2 processes. The solution includes a Framework class library project that provides some classes to help with unit testing to reduce the amount of code in each unit test and to help provide a common structure across tests. A set of best practices for unit testing K2 processes is then discussed. The best practices are ultimately followed by a set of How-To’s that describe how to perform different types of tests.

 

Sample K2 Workflow

The following depicts a sample K2 process that we will unit test as part of the examples in this document. It contains a standard approval, a rework step that sends a task back to the originator, a placeholder step that has a line rule, and then a couple of placeholders for the approve and deny branches.

sample

 

Setup the VSTS Solution

This section walks the reader through how to set up a solution in Visual Studio to use VSTS test projects to test K2 processes. This section describes how to set up a solution from scratch. It is perfectly acceptable to add the test project to an existing solution if that is preferred for a particular project.

Create Visual Studio Solution and Test Projects

This section outlines the steps necessary to get started. It includes high level instructions for creating the Visual Studio solution and projects that we will use in further exercises. It assumes the reader has familiarity with Visual Studio and does not include detailed instructions.

Create Visual Studio Solution

Create an empty solution found in the Visual Studio Other Project Types category. You can use an existing solution if you want to add the process test projects to a suite of other unit tests.

Create Visual Studio Test Framework Project

Create a standard class library project in your solution. This library will house common entities and a ProcessHelper class that includes much of the common code used in unit tests to start and move processes along their paths. Add the following references:

  • System.Configuration
  • SourceCode.Workflow.Client
  • SourceCode.Workflow.Management
  • SourceCode.HostClientAPI
  • SourceCode.Data.SmartObjectsClient
  • c:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

 

Create Visual Studio Test Project

Add a test project to the solution. The test project template can be found under the Test Projects\Test Documents node in the Installed Templates tree in the Add New Project dialog. Add the following references:

  • Add a reference to the test framework class library project
  • Add a reference to SourceCode.Workflow.Client

Your solution explorer should look similar to the following:

solutionexplorer

 

Configure the Framework Project

The framework project contains the following:

  • ProcessHelper – A helper class that contains utility functions for common tasks performed in unit tests. This pulls this code out of the unit tests to make them quicker to develop, easier to read, and focused on the scenario it is testing.
  • IProcessTest – This is an interface that test classes must implement to use the ProcessHelper
  • TestScenarioContext – A simple data class to keep the process context for a test scenario

Change Target Framework

Change the target framework to .NET Framework 4 by right clicking on the project, select Properties, and in the dropdown for Target framework select .NET Framework 4.

Add TestScenarioContext Class

Right click the project name and select Add -> New Folder. Make the folder name Entities. Right click the Entities folder and select Add Class. Name the class TestScenarioContext. This class is used to track the process instance data of a test run. Paste this class definition (changing namespaces to match your namespace).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SourceCode.Workflow.Client;
 
namespace SourceCode.Examples.UnitTests.Framework.Entities
{
 public class TestScenarioContext
 {
 public ProcessInstance ProcessInstance { get; set; }
 public Guid TestInstanceId { get; set; }
 
 public long ProcessId
 {
 get
 {
 long id = -1;
 if (this.ProcessInstance != null)
 {
 id = this.ProcessInstance.ID;
 }
 return id;
 }
 } 
 }
}

Add IProcessTest Interface

Add a folder to the framework project and name the folder Interfaces. Add the following interface to the folder. This interface is used by test projects to pass into the ProcessHelper classes as a reference back into the test class to be able work with the process instance and other process data.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using SourceCode.Examples.UnitTests.Framework.Entities;

using Microsoft.VisualStudio.TestTools.UnitTesting;




namespace SourceCode.Examples.UnitTests.Framework.Interfaces

{

       public interface IProcessTest

       {

              TestScenarioContext TestScenarioContext { get; set; }

              TestContext TestContext { get; set; }

              string ProcessName { get; }

       }

}

 

Add ProcessHelper Class

The ProcessHelper class provides helper methods for starting and working with processes and activities. Right click on the Framework project and click Add -> Class… Name the class ProcessHelper and copy the following code to the class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SourceCode.Examples.UnitTests.Framework.Interfaces;
using SourceCode.Workflow.Client;
using System.Configuration;
using SourceCode.Data.SmartObjectsClient;
using System.Data;
 
namespace SourceCode.Examples.UnitTests.Framework
{
 public static class ProcessHelper
 {
 public static string ConnectionString
 {
 get { return ConfigurationManager.AppSettings["K2WorkflowServerConnectionString"]; }
 }
 
 public static string ConnectionStringManagementServer
 {
 get { return ConfigurationManager.AppSettings["K2ManagementServerConnectionString"]; }
 }
 
 public static string ConnectionStringImpersonation
 {
 get { return ConfigurationManager.AppSettings["K2WorkflowServerConnectionStringImpersonate"]; }
 }
 
 public static void StartProcess(IProcessTest testClass)
 {
 StartProcess(testClass, null, true);
 }
 
 public static string SOConnectionString
 {
 get { return ConfigurationManager.AppSettings["K2SOConnectionString"]; }
 }
 
 public static string WorkflowServer
 {
 get { return ConfigurationManager.AppSettings["K2WorkflowServer"]; }
 }
 
 public static int WorkflowServerPort
 {
 get
 {
 int port = 0;
 bool parsed = Int32.TryParse(ConfigurationManager.AppSettings["K2WorkflowServerPort"], out port);
 if (!parsed)
 {
 port = 5555;
 }
 return port;
 }
 }
 
 public static void StartProcess(IProcessTest testClass, Dictionary<string, string> dataFields)
 {
 StartProcess(testClass, dataFields, true);
 }
 
 public static void StartProcess(IProcessTest testClass, Dictionary<string, string> dataFields, bool sleep)
 {
 ConnectionSetup connSetup = new ConnectionSetup();
 connSetup.ConnectionString = ConnectionString;
 using (Connection conn = new Connection())
 {
 conn.Open(connSetup);
 ProcessInstance instanceToStart = conn.CreateProcessInstance(testClass.ProcessName);
 
 if (dataFields != null)
 {
 foreach (KeyValuePair<string, string> field in dataFields)
 {
 instanceToStart.DataFields[field.Key].Value = field.Value;
 }
 }
 
 instanceToStart.Folio = GetFolio(testClass);
 
 conn.StartProcessInstance(instanceToStart, true);
 
 testClass.TestScenarioContext.ProcessInstance = instanceToStart;
 } 
 
 if (sleep)
 {
 System.Threading.Thread.Sleep(2000);
 }
 }
 
 public static void StopProcess(IProcessTest testClass)
 {
 try
 {
 SourceCode.Workflow.Management.WorkflowManagementServer server = new SourceCode.Workflow.Management.WorkflowManagementServer();
 server.Open(ConnectionStringManagementServer);
 server.StopProcessInstances(Convert.ToInt32(testClass.TestScenarioContext.ProcessId));
 }
 catch { } // ignore, don't want to fail the test if the process can't be stopped
 }
 
 /// <summary>
 /// Gets worklist items for the current running user
 /// </summary>
 /// <param name="testClass"></param>
 /// <returns></returns>
 public static Worklist GetWorklist(IProcessTest testClass)
 {
 return GetWorklist(testClass, null);
 }
 
 /// <summary>
 /// Gets worklist items for the username that is passed in. Pass in the username as domain\user
 /// </summary>
 /// <param name="testClass"></param>
 /// <param name="user"></param>
 /// <returns></returns>
 public static Worklist GetWorklist(IProcessTest testClass, string user)
 {
 ConnectionSetup connSetup = new ConnectionSetup();
 
 if (String.IsNullOrEmpty(user))
 {
 connSetup.ConnectionString = ConnectionString;
 }
 else
 {
 connSetup.ConnectionString = ConnectionStringImpersonation;
 }
 
 using (Connection conn = new Connection())
 {
 conn.Open(connSetup);
 if (!String.IsNullOrEmpty(user))
 {
 conn.ImpersonateUser(user); 
 }
 
 WorklistCriteria criteria = new WorklistCriteria();
 criteria.AddFilterField(WCField.ProcessFullName, WCCompare.Equal, testClass.ProcessName);
 
 // filter by the folio as well so we only get worklist items back for this process instance
 criteria.AddFilterField(WCField.ProcessFolio, WCCompare.Equal, GetFolio(testClass));
 
 Worklist list = conn.OpenWorklist(criteria);
 return list;
 }
 }
 
 public static void ActionWorklistItem(IProcessTest testClass, WorklistItem worklistItem, string action)
 {
 ConnectionSetup connSetup = new ConnectionSetup();
 connSetup.ConnectionString = ConnectionStringImpersonation;
 
 using (Connection conn = new Connection())
 {
 conn.Open(connSetup);
 
 // need to impersonate the allocated user in order to action the item
 conn.ImpersonateUser(worklistItem.AllocatedUser);
 
 // need to open the item so there is a current socket open
 WorklistItem openItem = conn.OpenWorklistItem(worklistItem.SerialNumber);
 
 if (openItem != null)
 {
 foreach (SourceCode.Workflow.Client.Action itemAction in openItem.Actions)
 {
 if (itemAction.Name == action)
 {
 itemAction.Execute(true);
 break;
 }
 }
 }
 }
 
 // sleep the thread to give time for the logs to be written
 System.Threading.Thread.Sleep(2000);
 }
 
 public static bool EnsureActivityExecuted(IProcessTest testClass, string activityName)
 {
 bool executed = false;
 string query = "select * from Activity_Instance.List where ProcessInstanceID = " + testClass.TestScenarioContext.ProcessId.ToString();
 using (SOConnection soConn = new SOConnection(SOConnectionString))
 using (SOCommand command = new SOCommand(query, soConn))
 using (SODataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
 {
 soConn.Open();
 while (reader.Read())
 {
 string executedActivityName = reader["ActivityName"].ToString(); 
 if (executedActivityName.Equals(activityName))
 {
 executed = true;
 break;
 }
 }
 }
 
 return executed;
 }
 
 public static string GetFolio(IProcessTest testClass)
 {
 string folio = "Unit Test - " + testClass.TestContext.TestName + " - Test Instance ID: " + testClass.TestScenarioContext.TestInstanceId.ToString();
 return folio;
 }
 }
}

 

Configure the Test Project

This section adds the necessary files and configures a build step to copy the application configuration file to the output directory.

Add Application Configuration File

You can specify a configuration file to use for a unit test project by creating a file that conforms to the naming convention of <ProjectName>.dll.config. For instance, if you have a project named MyCompany.UnitTests.Process you would add a configuration file named:

MyCompany.UnitTests.Process.dll.config

To add this file, right click on the test project and select Add -> New Item… Select Application Configuration File as the item type and name it according to the convention specified above. Add the following to the configuration file.

NOTE: change the server names and port numbers to match your environment and change the username/password for the K2WorkflowServerConnectionStringImpersonate to match your environment.

<appSettings>

              <add key="K2WorkflowServer" value="dlx"/>

              <add key="K2WorkflowServerPort" value="5555"/>

              <add key="K2SOConnectionString" value="Integrated=True;IsPrimaryLogin=True;Authenticate=True;EncryptedPassword=False;Host=dlx;Port=5555"/>

              <add key="K2WorkflowServerConnectionString" value="Integrated=True;IsPrimaryLogin=False;Authenticate=True;EncryptedPassword=False;Host=localhost;Port=5252"/>

              <add key="K2ManagementServerConnectionString" value="Integrated=True;IsPrimaryLogin=True;Authenticate=True;EncryptedPassword=False;Host=localhost;Port=5555"/>

              <add key="K2WorkflowServerConnectionStringImpersonate" value="Integrated=True;IsPrimaryLogin=True;Authenticate=True;EncryptedPassword=True;Host=localhost;Port=5252;WindowsDomain=denallix;UserID=K2Service;Password=K2pass!"/>

       </appSettings>

Add Build Script to Copy Configuration File to Output Directory

The configuration file created in the above section needs to be copied to the output directory so that it is in the same directory as the dll when the dll is executed. To automate this:

  • Right click on the test project and select Properties
  • Click the Build Events tab
  • In the Post-build event command line text box add the following:
    copy "$(ProjectDir)*.config" "$(TargetDir)"

Now every time the project is built it will copy the configuration file to the output directory.

Best Practices

Now that we have seen how to set up a solution in Visual Studio and how to set up a VSTS project to test K2 processes, let’s take a look at a number of best practices for testing K2 processes. This section will then be followed up with a series of How-To’s that show how to test each of the best practices in VSTS.

The following best practices are included in this section:

  • Scenario vs. Method Paradigm Shift – describes a slightly different way of designing the unit tests due to the fact that unit tests for processes test the entire process rather than testing a discreet unit of functionality
  • Use data fields to test line rules – discusses how to pass in different values to the process data fields to test line rules
  • Use the worklist to test task assignments – discusses how to use the worklist to test task assignments and to execute an action on a task to move the process to the next step
  • Use the K2 reporting SmartObjects to test activity execution – discusses how to use the K2 reporting SmartObjects to test and ensure that the correct activities executed
  • Use XML files for simulating submission of InfoPath forms – discusses how to use XML files stored in the VSTS project to simulate submission of InfoPath forms
  • Use SmartBox SmartObjects to simulate SmartObjects that connect to external systems – discusses how to use SmartBox SmartObjects to be able to manipulate data returned from the SmartObject to test how that data affects the process rules. This is similar to using mock objects in traditional unit testing to simulate connecting to a database.

Scenario vs. Method Paradigm Shift

In a typical unit test paradigm there is a one-to-one mapping between a class in the code library to a class in the unit test library. The class in the unit test library would then fully test the class in the code library. In addition to this, each method in a unit test class generally performs a discreet test against the code library; i.e. unit tests generally don’t test processes that involve multiple methods or properties in the code library.

As one might expect, it would be difficult to use the same paradigm when unit testing a K2 process. Take the simple fact that a process has to be started before a test can be run for something inside the process. This simple scenario breaks the traditional paradigm in that the test has to have code to both start the process and then to test something inside the process. The situation is exacerbated when you need to test an activity that is 10 steps into the process. The test code first has to start the process and then action the first nine steps before it can test the 10th step.

This challenge creates a slight paradigm shift in how unit tests are written for K2 processes. Most unit tests for K2 processes are scenario based rather than based on a single unit. A scenario generally tests one use case. For instance, you may have a scenario that tests an approval at the 10th step and a different scenario that tests a denial at the 10th step.

Scenarios are generally encapsulated within a class in the unit test project. Therefore, you have one class per scenario. This makes the code easier to read and easier to maintain because code to move the process along the various steps can be encapsulated in the class without worrying about name clashes for private methods and properties across scenarios.

Helper classes are typically used as well to move the process along the various steps. The helper class encapsulates common code activities such as starting a process, getting a worklist item, performing an action on a worklist item, updating a data field, etc. This is useful because many scenarios will be very similar to each other with perhaps only one activity having a different outcome than another scenario. In this solution a helper class is included in the Framework assembly. However, there may be cases where a test project may include an individual helper class for common code activities that are private to the process.

Use Data Fields to Test Line Rules

It is common to have line rules in a K2 process that test data fields and execute a branch in the process if the line rule evaluates to true. Line rules can be tested by passing in values for the data field to test both the execution path and the non-execution path. The data field can be set during the process start up or by updating the process definition. Setting the data field at the process start is the most common approach. However, if the data field needs to change mid-way through the process it is possible to change the value of the data field at any time.

Use the Worklist to Test Task Assignments

The worklist API in the WCF services can be leveraged to retrieve the worklist items for a process and then to interrogate those worklist items to ensure that the appropriate items have been created and that they have been assigned to the correct parties. In addition, the worklist API can be used to execute the worklist items and pass in an Action to test that the process correctly continues on to the next activity.

Use the K2 Reporting SmartObjects to Test Activity Execution

The client API does not expose methods to get a list of activities that have been executed for a process. This data is, however, exposed through the K2 Workflow Reports SmartObjects. The How-To section below describes how to retrieve the data using the SmartObject ADO query capabilities.

Use XML Files for Simulating Submission of InfoPath Forms

InfoPath forms are simply XML documents that have a client application and services that understand how to work with the InfoPath XML schema. Therefore, it is possible to simulate the submission of InfoPath forms in a K2 process by passing the XML to the K2 InfoPath service. The How To section describes how to save different versions of the InfoPath XML data as files in the test project and how to read the XML from the file and pass it to the K2 InfoPath service to test different scenarios.

Use SmartBox SmartObjects to Simulate SmartObjects that Connect to External Systems

SmartObjects provide a layer of abstraction between the data that flows from/to a process and the backend system where the data is physically stored. This abstraction provides an opportunity for unit testing, and for development testing, to swap out the physical layer in cases where it isn’t feasible to run automated unit tests against external systems.

For example, the process may integrate with the organization’s HRIS system. Your organization may only have a QA and Production HRIS environment and may not want unit tests run against the QA environment. In this scenario, you can configure the SmartObjects to use a SmartBox backend to simulate reading and writing from the HRIS system for the unit tests. This allows the process to be tested without making any changes to the process itself.

Give Process Rights to the Build Process

A user needs the appropriate rights to start a process. The user that the automated test run under will need these same process rights in order to start the processes. The rights only have to be given to the process once after the process is first deployed to the development server. This can be done manually or a step can be added to the build script to add the process rights.

How-To’s

This section includes a series of code examples that show how to implement the best practices described above.

Start a Process

All test scenarios will need to start a process. The ProcessHelper class has several overloads for starting a process. To start a process with data fields use the overload that accepts a string dictionary. You can set either one or all of the data fields in a process by setting the values in the string dictionary.

This example starts a process without data fields:

ProcessHelper.StartProcess(this);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");

 

This example starts a process and populates a data field named lineRuleField:

Dictionary<string, string> dataFields = new Dictionary<string, string>(1);

                     dataFields.Add("lineRuleField", "Go");




                     ProcessHelper.StartProcess(this, dataFields);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");

Start a Process and Sleep

Depending on the horsepower on the development machine that is running the process it may take varying lengths of time to start the process. If the process takes longer to start than it does to get to your next action in the test script then the test script will fail because the process instance will not be available to work with. Because of this, the StartProcess method in the ProcessHelper automatically sleeps the thread for 2 seconds. You can override this by passing in false for the overload that accepts the sleep parameter.

Get Worklist

The ProcessHelper class in the Framework assembly contains a method GetWorklist that retrieves all the active worklist items for the process. There are two overloads for this method. The overload that does not accept a user will load the worklist for the current user context of the thread. The overload that accepts a user will load the worklist for that user.

Action a Worklist Item

The ProcessHelper class contains an ActionWorklistItem method that will action a worklist item based on the action string passed in. A test script will typically first call the GetWorklist to validate that the worklist contains a task for a user(s). Then it will action the worklist item to move it on to the next step and ensure that the appropriate activity or branch is then executed in the process.

Worklist activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\chad");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     // the user for this task should be chad

                     Assert.AreEqual<string>(@"k2:denallix\chad", activeWorkListItems[0].AllocatedUser.ToLower());




                     ProcessHelper.ActionWorklistItem(this, activeWorkListItems[0], "Approve");




                     bool approveBranchExecuted = ProcessHelper.EnsureActivityExecuted(this, "Approve Branch");

                     Assert.IsTrue(approveBranchExecuted, "Approve Branch did not execute");

 

Ensure an Activity is Executed

When testing line rules or executing an action on a worklist item it is common to subsequently test that the correct activity in the next branch of the workflow executed. The ProcessHelper contains an EnsureActivityExecuted method that accepts the name of the activity. The method returns true if the activity executed or false if it did not.

In some cases the unit test may need to wait for a period of time for the system to write the execution of the activity to the logs. To help with this the ActionWorklistItem in the ProcessHelper automatically sleeps the thread for 2 seconds. If you are not using the ActionWorklistItem method and are seeing that the unit test is failing and indicating that the activity is not executing then you will need to sleep the thread before calling EnsureActivityExecuted in order to give the system some time to write to the logs.

Scenarios for Testing the Sample Process

Basic Process Approve Scenario

The following class tests an approval scenario for the sample process. The class uses the process helper to start a process instance. It then uses the process helper to ensure that only one task is created and then to action the task. Finally, it ensures that the approve branch executed and that the deny branch did not execute.

using System;

using System.Text;

using System.Collections.Generic;

using System.Linq;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using SourceCode.Examples.UnitTests.Framework.Entities;

using SourceCode.Examples.UnitTests.Framework.Interfaces;

using SourceCode.Examples.UnitTests.Framework;

using SourceCode.Workflow.Client;




namespace SourceCode.Examples.UnitTests.Process

{

       [TestClass]

       public class BasicProcessApproveScenario : IProcessTest

       {

              private const string PROCESS_NAME = @"UnitTestingProcesses\BasicProcess";




              private TestScenarioContext _testScenarioContext;

              private TestContext _testContext;




              public TestContext TestContext

              {

                     get { return _testContext; }

                     set { _testContext = value; }

              }




              public TestScenarioContext TestScenarioContext

              {

                     get { return _testScenarioContext; }

                     set { _testScenarioContext = value; }

              }




              public string ProcessName

              {

                     get { return PROCESS_NAME; }

              }




              [TestInitialize]

              public void InitializeTest()

              {

                     _testScenarioContext = new TestScenarioContext();

                     _testScenarioContext.TestInstanceId = Guid.NewGuid();

              }




              [TestCleanup]

              public void CleanupTest()

              {                   

                     _testScenarioContext = null;

              }




              [TestMethod]

              public void TestApproveScenario()

              {

                     ProcessHelper.StartProcess(this);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");




                     Worklist activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\chad");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     // the user for this task should be chad

                     Assert.AreEqual<string>(@"k2:denallix\chad", activeWorkListItems[0].AllocatedUser.ToLower());




                     ProcessHelper.ActionWorklistItem(this, activeWorkListItems[0], "Approve");




                     bool approveBranchExecuted = ProcessHelper.EnsureActivityExecuted(this, "Approve Branch");

                     Assert.IsTrue(approveBranchExecuted, "Approve Branch did not execute");




                     bool denyBranchExecuted = ProcessHelper.EnsureActivityExecuted(this, "Deny Branch");

                     Assert.IsFalse(denyBranchExecuted, "Deny Branch executed when it should not have");

              }

       }

}

 

Basic Process Deny Scenario

The following class tests a deny scenario for the sample process. The class uses the process helper to start a process instance. It then uses the process helper to ensure that only one task is created and then to action the task. Finally, it ensures that the deny branch executed and that the approve branch did not execute.

using System;

using System.Text;

using System.Collections.Generic;

using System.Linq;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using SourceCode.Examples.UnitTests.Framework.Entities;

using SourceCode.Examples.UnitTests.Framework.Interfaces;

using SourceCode.Examples.UnitTests.Framework;

using SourceCode.Workflow.Client;




namespace SourceCode.Examples.UnitTests.Process

{

       [TestClass]

       public class BasicProcessDenyScenario : IProcessTest

       {

              private const string PROCESS_NAME = @"UnitTestingProcesses\BasicProcess";




              private TestScenarioContext _testScenarioContext;

              private TestContext _testContext;




              public TestContext TestContext

              {

                     get { return _testContext; }

                     set { _testContext = value; }

              }




              public TestScenarioContext TestScenarioContext

              {

                     get { return _testScenarioContext; }

                     set { _testScenarioContext = value; }

              }




              public string ProcessName

              {

                     get { return PROCESS_NAME; }

              }




              [TestInitialize]

              public void InitializeTest()

              {

                     _testScenarioContext = new TestScenarioContext();

                     _testScenarioContext.TestInstanceId = Guid.NewGuid();

              }




              [TestCleanup]

              public void CleanupTest()

              {

                     _testScenarioContext = null;

              }




              [TestMethod]

              public void TestDenyScenario()

              {

                     ProcessHelper.StartProcess(this);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");




                     Worklist activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\chad");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     ProcessHelper.ActionWorklistItem(this, activeWorkListItems[0], "Deny");




                     bool denyBranchExecuted = ProcessHelper.EnsureActivityExecuted(this, "Deny Branch");

                     Assert.IsTrue(denyBranchExecuted, "Deny Branch did not execute");




                     bool approveBranchExecuted = ProcessHelper.EnsureActivityExecuted(this, "Approve Branch");

                     Assert.IsFalse(approveBranchExecuted, "Approve Branch executed when it should not have");




              }

       }

}

 

Basic Process Rework Scenario

The following class tests a rework scenario for the sample process. The class uses the process helper to start a process instance. It then uses the process helper to ensure that only one task is created and then to action the manager approval task as a Rework action. It then assures that the administrator has a worklist item for the rework and then executes the Resubmit task for the administrator. Finally, it ensures that Chad had been assigned a worklist item after the Resubmit task was submitted. Note that this scenario does not test the approve or deny branch because it is intended to only test the rework scenario.

using System;

using System.Text;

using System.Collections.Generic;

using System.Linq;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using SourceCode.Examples.UnitTests.Framework.Entities;

using SourceCode.Examples.UnitTests.Framework.Interfaces;

using SourceCode.Examples.UnitTests.Framework;

using SourceCode.Workflow.Client;




namespace SourceCode.Examples.UnitTests.Process

{

       [TestClass]

       public class BasicProcessReworkScenario : IProcessTest

       {

              private const string PROCESS_NAME = @"UnitTestingProcesses\BasicProcess";




              private TestScenarioContext _testScenarioContext;

              private TestContext _testContext;




              public TestContext TestContext

              {

                     get { return _testContext; }

                     set { _testContext = value; }

              }




              public TestScenarioContext TestScenarioContext

              {

                     get { return _testScenarioContext; }

                     set { _testScenarioContext = value; }

              }




              public string ProcessName

              {

                     get { return PROCESS_NAME; }

              }




              [TestInitialize]

              public void InitializeTest()

              {

                     _testScenarioContext = new TestScenarioContext();

                     _testScenarioContext.TestInstanceId = Guid.NewGuid();

              }




              [TestCleanup]

              public void CleanupTest()

              {

                     _testScenarioContext = null;

              }




              [TestMethod]

              public void TestReworkScenario()

              {

                     ProcessHelper.StartProcess(this);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");




                     Worklist activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\chad");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     // the user for this task should be chad

                     Assert.AreEqual<string>(@"k2:denallix\chad", activeWorkListItems[0].AllocatedUser.ToLower());




                     ProcessHelper.ActionWorklistItem(this, activeWorkListItems[0], "Rework");




                     activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\administrator");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     // the user for this task should be chad

                     Assert.AreEqual<string>(@"k2:denallix\administrator", activeWorkListItems[0].AllocatedUser.ToLower());




                     ProcessHelper.ActionWorklistItem(this, activeWorkListItems[0], "Resubmit");




                     activeWorkListItems = ProcessHelper.GetWorklist(this, @"denallix\chad");




                     // in this process there should only be one active worklist item

                     Assert.AreEqual<int>(1, activeWorkListItems.Count);




                     // the user for this task should be chad

                     Assert.AreEqual<string>(@"k2:denallix\chad", activeWorkListItems[0].AllocatedUser.ToLower());




                     ProcessHelper.StopProcess(this);

              }

       }

}

 

 

Basic Process Line Rule Equals Go Scenario

This scenario tests that the Line Rule Activity executes when “Go” is passed in for the lineRuleField data field. This scenario is meant to only test this branch of the process so it does not bother with the manager approval branch.

using System;

using System.Text;

using System.Collections.Generic;

using System.Linq;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using SourceCode.Examples.UnitTests.Framework.Entities;

using SourceCode.Examples.UnitTests.Framework.Interfaces;

using SourceCode.Examples.UnitTests.Framework;

using SourceCode.Workflow.Client;




namespace SourceCode.Examples.UnitTests.Process

{

       [TestClass]

       public class BasicProcessLineRuleEqualGoScenario : IProcessTest

       {

              private const string PROCESS_NAME = @"UnitTestingProcesses\BasicProcess";




              private TestScenarioContext _testScenarioContext;

              private TestContext _testContext;




              public TestContext TestContext

              {

                     get { return _testContext; }

                     set { _testContext = value; }

              }




              public TestScenarioContext TestScenarioContext

              {

                     get { return _testScenarioContext; }

                     set { _testScenarioContext = value; }

              }




              public string ProcessName

              {

                     get { return PROCESS_NAME; }

              }




              [TestInitialize]

              public void InitializeTest()

              {

                     _testScenarioContext = new TestScenarioContext();

                     _testScenarioContext.TestInstanceId = Guid.NewGuid();

              }




              [TestCleanup]

              public void CleanupTest()

              {

                     _testScenarioContext = null;

              }




              [TestMethod]

              public void TestLineRuleBranchEqualsGoScenario()

              {

                     Dictionary<string, string> dataFields = new Dictionary<string, string>(1);

                     dataFields.Add("lineRuleField", "Go");




                     ProcessHelper.StartProcess(this, dataFields);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");




                     bool lineRuleActivityExecuted = ProcessHelper.EnsureActivityExecuted(this, "Line Rule Activity");

                     Assert.IsTrue(lineRuleActivityExecuted, "Line Rule Activity did not execute");




                     ProcessHelper.StopProcess(this);

              }

       }

}

 

 

 

Basic Process Line Rule Equals Stop Scenario

This scenario tests that the Line Rule Activity does NOT execute when “Stop” is passed in for the lineRuleField data field. This scenario is meant to only test this branch of the process so it does not bother with the manager approval branch.

using System;

using System.Text;

using System.Collections.Generic;

using System.Linq;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using SourceCode.Examples.UnitTests.Framework.Entities;

using SourceCode.Examples.UnitTests.Framework.Interfaces;

using SourceCode.Examples.UnitTests.Framework;

using SourceCode.Workflow.Client;




namespace SourceCode.Examples.UnitTests.Process

{

       [TestClass]

       public class BasicProcessLineRuleEqualStopScenario : IProcessTest

       {

              private const string PROCESS_NAME = @"UnitTestingProcesses\BasicProcess";




              private TestScenarioContext _testScenarioContext;

              private TestContext _testContext;




              public TestContext TestContext

              {

                     get { return _testContext; }

                     set { _testContext = value; }

              }




              public TestScenarioContext TestScenarioContext

              {

                     get { return _testScenarioContext; }

                     set { _testScenarioContext = value; }

              }




              public string ProcessName

              {

                     get { return PROCESS_NAME; }

              }




              [TestInitialize]

              public void InitializeTest()

              {

                     _testScenarioContext = new TestScenarioContext();

                     _testScenarioContext.TestInstanceId = Guid.NewGuid();

              }




              [TestCleanup]

              public void CleanupTest()

              {

                     _testScenarioContext = null;

              }




              [TestMethod]

              public void TestLineRuleBranchEqualsStopScenario()

              {

                     Dictionary<string, string> dataFields = new Dictionary<string, string>(1);

                     dataFields.Add("lineRuleField", "Stop");




                     ProcessHelper.StartProcess(this, dataFields);

                     Assert.IsNotNull(this.TestScenarioContext.ProcessInstance, "Unable to start process.");




                     bool lineRuleActivityExecuted = ProcessHelper.EnsureActivityExecuted(this, "Line Rule Activity");

                     Assert.IsFalse(lineRuleActivityExecuted, "Line Rule Activity executed when it should not have");




                     ProcessHelper.StopProcess(this);

              }

       }

}

 

The test project can be downloaded from here

In my next ‘How To’ I will be looking at how to use Specflow, to make testing a workflow more readable to the business.

 

 

2 thoughts on “Unit Testing in K2

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s