Testing a workflow inside a Smartform

So I have posted a few articles on testing in K2, we have looked at Unit Testing,  Testing Smartforms and then just recently more examples of testing a workflow.  After writing that article I realised that there was something missing. What about building a workflow testing tool inside of a Smartform. As it all well and good that we can write unit tests for testing a workflow, but that relies on the workflow developer being able to write code and being that the whole idea of K2 is to build low code , no code solutions. Then there should be away of doing it without the developer having to write code.

There are a number of tools out in the market and they do absolutely fantastic job of testing workflows without having to write code. But I wanted to see if it could be done inside a Smartform.

So my challenge is to build a Smartform App that would test a workflow, with out the tester having to write any code.

The app had to do the following

  1. Start any workflow and check that it has started
  2. Check that tasks have been created and the correct number
  3. Action a task
  4. Checking the task has been completed
  5. Check that the workflow has finished
  6. Check the workflow has gone down the direct path.

The app should all be in Smartforms and should be easy to use.

So lets see how I got on.  So i am going to start with the finished tool and then i will take us through it.

Testing Dashboard

The dashboard shows all the current tests that have been performed and shows the overall percentage of pass and failures.

dashboard

From here you can also click on ‘Create Test’ button to create a new test. Selecting an existing test and clicking on ‘Run Test’ will  run an existing test. Finally double clicking on a test will open up the details about the test.

Test Details

So the test details shows the following information

test details

  1. Workflow being tested
  2. If has already been tested
  3. When it was tested
  4. Overall Pass or Fail
  5. What percentage passed and failed
  6. What was tested and whether each test passed or failed.
  7. It also shows any other tests relating to the workflow being tested.

From the test details you also have the ability to retest the test as well.

Creating a new test

From the dashboard click on ‘Create Test’ button.

test builder.PNG

  1. In the new test form, give the test a name
  2. Select a workflow that you are going to test. Once selected you will then be able to see all the activities of that workflow beneath.test-builder1
  3. Select the activities that the workflow will go throughtest-builder2
  4. The last section is where we build up the actual the tests and what results we expect from those tests.test-builder3

Building a simple test script

The workflow we have selected is a very simple one. It has the following activities

  1. Setup
  2. Task
  3. Approve
  4. Reject
  5. End

test-workflow

Task is where there is a client event which has one slot and goes to the originator.

Now that we have got the basic test details and what route we expect the test to go. We can now build are test script.

  1. Select ‘Start Process’ from the test type drop down list
  2. You will notice that the other text boxes in the row, will be pre populated with information and some will be disabled depending on what information the particular test type requires.
  3. Now we choose comparison sign we are going to use it can be either
    1.  =
    2. >
    3. >=
    4. <
    5. <=
  4. We are going to choose >
  5. In the expected result / comparison box lets enter 0, this is because if workflow starts successfully we expect a value greater than 0.
  6. So ‘Start Process’, needs to know the ‘Folio’ to give the workflow, you can either enter in something. Or if you leave it blank it will take the name we have given earlier to the test.
  7. The next parameter we need to give the test type is the process name. You can either copy the full workflow name and paste into this box or leave it and it will automatically get the workflow name.
  8. The next box is called milliseconds, you can either leave it at 0. Or you can enter in a value. This is useful as it may be your workflow is taking a while to perform an event. So you can enter in a delay to the tests to compensate for this.
  9.  Click on ‘Add’ again , we now what to check the status of the workflow, to make sure that once we started it. It is now active.
  10. So select ‘Get Workflow Status’ from the drop down list
  11. Choose ‘=’ from Sign
  12. Enter ‘Active’ in expected result
  13. We don’t need to enter in anything else
  14. Click on ‘Add’
  15. We now want to check to see if has generated some tasks
  16. Select ‘Get Task Count’
  17. Select ‘=’ in Sign
  18. Expected Result should be 1, as we know it’s only one slot and going to the originator.
  19. ProcessInstanceId, leave that how it is, as the testing framework will replace that with the actual process instance id as run time of the test
  20. Process name either enter in the full name or leave it and it will get automatically populated
  21. Next we can action the task
  22. Select ‘=’ for the sign
  23. In the expected result box enter ‘True’
  24. Where it’s says ‘Action’ we need to enter in the action we expect the test to use. So lets enter ‘Approve’
  25. Where it says ‘Serial Number’ we leave that and like the process instance id, it will be populated by the testing framework at run time.
  26. For Destination user, enter in the destination user that the task is supposed to be for.
  27. When this test type runs it will try and action the task based on the information we have given it (Task Action and Destination User) if the test is successful the actual result will be true.
  28. The last test we are going to do is to check that workflow has gone down the correct path and the correct activities were used. The test will compare the path we expect it to be. which was defined in the ‘Workflow Route’ section to the actual path taken.
  29. Select ‘Compare Activities’ from ‘Test Type’
  30. Select ‘=’ from Sign
  31. Enter in ‘True’ in expected result
  32. Leave ProcessInstanceId and Activities how it is
  33. In the Millisecond box enter ‘4000’ this will make the ‘Compare Activities’ test to wait 4000 milliseconds before starting the test. This will give the workflow time to perform it’s other activities before completing. If this test fails then increase the time. It will then pass.
  34. The test script should now look like thistest-builder4
  35. Now click on ‘Save’
  36. You will now see a read only version of the test
  37. With two buttons ‘Edit Test’ and ‘Run Test’

Running a Test

To run a test, either select a test from the test list and click on ‘Run Test’ or from the test details click on the button ‘Run Test’

Once the test has been completed, you will be displayed the results for each individual test and also the overall test.

resuts

Each individual test will show whether it has passed or failed. If it has passed the status will be true and in green and if had failed it will be false and in red.

For each test that we create we also get the overall test result.

A failed test would be when any of the individual tests has failed

fail

It will show a pie chart showing the % of pass and fail with big black x cross above it.

pass

For an overall Pass we get a tick and of course a 100%

These test results will remain until, you click on ‘Run Test’ again and this will overwrite the previous test results with the new ones.

Continue reading “Testing a workflow inside a Smartform”

Workflow Testing

In a previous article i went through how to unit test K2 workflow, now i have some more examples of testing a workflow from start to finish.

These unit tests just use the out of the box K2 API and Visual Studio’s testing framework. It uses an XML file to store test results and data such as task SN and process instance id.

The code can be downloaded from here 

Starting the workflow

To start a workflow we can use the following API

// <summary>
 /// Starts a workflow
 /// </summary>
 /// <param name="Folio"></param>
 /// <param name="ProcessName"></param>
 /// <returns></returns>
 public int StartProcess(string Folio, string ProcessName)
 {
 var con = ConfigurationManager.AppSettings;
 int ProcessInstanceId = 0;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 try
 {
 SourceCode.Workflow.Client.ProcessInstance K2Proc = K2Conn.CreateProcessInstance(ProcessName);
 
 K2Proc.Folio = Folio;
 K2Conn.ImpersonateUser(ServiceAccount);
 K2Conn.StartProcessInstance(K2Proc);
 ProcessInstanceId = K2Proc.ID;
 }
 catch (Exception EX)
 {
 ProcessInstanceId = 0;
 }
finally
 {
 K2Conn.Close();
 }
 return ProcessInstanceId;
 }

This method returns the process instance id. We can use this in a unit test and check to see if the value returned from the above method is greater than 0

Test: Unit Test for starting the workflow and check to see if it has started (Common)

/// <summary>
 /// Tests the starting of a workflow, should return a number greater than 0
 /// </summary>
 /// <param name="folio"></param>
 /// <param name="ProceesName"></param>
 /// <returns></returns>
 [TestMethod]
 public void StartWorkflow_Success()
 {
int actual = workflow.StartProcess(this.folio, this.ProcessName);
 this.proceessInstance = actual;
 Results.SaveResult(TestType.ProcessInstance,this.proceessInstance.ToString()); 
 Assert.AreNotEqual(0, actual);
 ProcInstId = this.proceessInstance;
 
 }

Check the workflow is running

Now that the workflow has started, we can run a method to check it’s current status

/// <summary>
 /// Gets the current status of the workflow
 /// </summary>
 /// <param name="processinstanceId"></param>
 /// <returns></returns>
 public string GetWorkflowStatus(int processinstanceId)
 {
 string Result = string.Empty;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 try
 {
 SourceCode.Workflow.Client.ProcessInstance K2Proc = K2Conn.OpenProcessInstance(processinstanceId);
 switch (K2Proc.Status1)
 {
 case SourceCode.Workflow.Client.ProcessInstance.Status.Active:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Active.ToString();
 break;
 }case SourceCode.Workflow.Client.ProcessInstance.Status.Completed:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Completed.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Deleted:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Deleted.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Error:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Error.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.New:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.New.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Running:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Running.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Stopped:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Stopped.ToString();
 break;
 }
 }  
 }
 catch (Exception ex)
 { Result = ex.Message; }
 finally
 {
 K2Conn.Close();
 }return Result;
 }

This method returns the current status, we can use this to see if the current workflow is running or if has completed

Test: Check workflow has started (Common)

/// <summary>
 /// Test to check that the workflow has started, by checking it's status
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 [TestMethod]
 public void StartWorkflow_Running_Success()
 {
int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 string actual = workflow.GetWorkflowStatus(this.proceessInstance);
 Results.SaveResult(TestType.StartStatus, actual);
 StringAssert.Equals(SourceCode.Workflow.Client.ProcessInstance.Status.Active.ToString(), actual);
 }

We could also take this same unit test and change the SourceCode.Workflow.Client.ProcessInstance.Status.Active to ‘Error’ or ‘Completed’

so we can create tests to see if the workflow is completed or it has gone into error state

 

Checking to see if  a task has been created

This method check to see if a task has been created for the particular process and instance.

public Boolean IsTaskFound(int ProcessInstanceId, string ProcessName)
 {
 Boolean Result = false;
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername, 5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
 foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 Result = true;
 }
 }
 }
 catch (Exception ex)
 {
Result = false;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return Result;
 }

 

Test: Work List – task found (Common)

Returns true if task has been found

/// <summary>
 /// Tests to see if tasks have been generated for this instance
 /// </summary>
 [TestMethod]
 public void WorkList_TasksFound_Success()
 {
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 Boolean actual = false;
 actual = workflow.IsTaskFound(this.proceessInstance, this.ProcessName);
 Assert.AreEqual(true, actual);
 }

 

 

Checking the correct number of tasks has been created

This method gets the number of tasks that has been generated. So we can check if the correct number of tasks.

public int GetTaskCount(int ProcessInstanceId,string ProcessName)
 {
 int Result = 0;
 int count = 0;
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername ,5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 count++;
 }
 }
 Result = count;
 }
catch (Exception ex)
 {
Result = 0;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return Result;
 }

 

Test: Correct number of tasks created (Common)

Returns the number of tasks, which we can then compare against the amount expected

/// <summary>
 /// Checks that the correct number of tasks is generated
 /// </summary>
 [TestMethod]
 public void WorkList_CorrectNumberOfTasksCreated_Success()
 {
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 int actual = 0;
 actual = workflow.GetTaskCount(this.proceessInstance, this.ProcessName);
 Results.SaveResult(TestType.TaskCount, actual.ToString());
 Assert.AreEqual(this.TaskCount, actual);
 }

 

Get task details

This methods takes the task, and gets the details about the task

/// <summary>
 /// Gets details about the task
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 /// <returns></returns>
 public List<tasklist> GetTask(int ProcessInstanceId,string ProcessName)
 {
List<tasklist> list = new List<tasklist>();
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername, 5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 var x = worklistItem.ActivityName;
list.Add(new tasklist
 {
 Status = worklistItem.Status.ToString(),
 Destination = worklistItem.Destination,
 EventName = worklistItem.EventName,
 ActInstDestID = worklistItem.ActInstDestID.ToString(),
 ActivityName = worklistItem.ActivityName,
 SerialNumber = (ProcessInstanceId + "_" + worklistItem.ActInstDestID)
 });

}
 }
 }
 catch (Exception ex)
 {
// Result = false;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return list;
 }

 

Test:  Check that the correct task has been created (Common)

Returns the name of the activity which compares against the expected activity

/// <summary>
 /// Gets the activity name of the task
 /// </summary>
 [TestMethod]
 public void WorkList_RetrieveTaskList_Success()
 {
 string Actual = string.Empty;
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
var task = workflow.GetTask(this.proceessInstance, this.ProcessName);
 
 this.SerialNumber = task[0].SerialNumber;
 Results.SaveResult(TestType.SerialNumber, this.SerialNumber);
 this.DestinationUser = task[0].Destination;
 Results.SaveResult(TestType.Destination, this.DestinationUser);
 Actual = task[0].ActivityName;
 Results.SaveResult(TestType.TaskActivity, Actual);
 Assert.AreEqual(this.TaskActivity, Actual);
}

 

Action a task

Actions the task and moves the workflow along

/// <summary>
 /// Actions a task
 /// </summary>
 /// <param name="action"></param>
 /// <param name="serialnumber"></param>
 /// <param name="destinationuser"></param>
 /// <returns></returns>
 public Boolean ActionTask(string action, string serialnumber, string destinationuser)
 {
 Boolean result = false;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 K2Conn.ImpersonateUser(destinationuser);
 SourceCode.Workflow.Client.WorklistItem K2WListItem = K2Conn.OpenWorklistItem(serialnumber);
 try
 {
K2WListItem.Actions[action].Execute();
 result = true;
 }
 catch (Exception ex)
 {
 result = false;
 }
 finally {
 K2Conn.Close();
 }
 return result;
 }

Test : Action’s the task

Returns true if the action of the task has been successful which we compare against the expected.

/// <summary>
 /// Actions a task
 /// </summary>
 [TestMethod]
 public void Task_GetActions_Sucess()
 {
 Boolean Actual = false;
 this.SerialNumber = Results.GetResult(TestType.SerialNumber);
 this.DestinationUser = Results.GetResult(TestType.Destination);
K2WorkflowMap.TaskDetails task = workflow.OpenTask(this.SerialNumber, this.DestinationUser)[0];
 Actual = workflow.ActionTask(this.TaskAction, this.SerialNumber, this.DestinationUser);
 Assert.AreEqual(true, Actual);
}

This test only accounts for one slot, it can be modified to loop through the task list for the particular instance.

 

Task Count

This method, counts the number of tasks for the current instance

 /// <summary>
 /// Gets the number of active tasks for instance of a workflow
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 /// <returns></returns>
 public int GetTaskCount(int ProcessInstanceId,string ProcessName)
 {
 int Result = 0;
 int count = 0;
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername ,5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 count++;
 }
 }
 Result = count;
 }
catch (Exception ex)
 {
Result = 0;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return Result;
 }

 

Test: Check Task Complete (Common)

Test checks to see if the number of the tasks for instance is 0

/// <summary>
 /// checks to see if task is complete by checking that task has gone
 /// </summary>
 [TestMethod]
 public void Task_CheckTaskComplete_Success()
 {
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 int actual = 0;
 actual = workflow.GetTaskCount(this.proceessInstance, this.ProcessName);
 Results.SaveResult(TestType.TaskCount, actual.ToString());
 Assert.AreEqual(0, actual);
 }

 

 

Once the main lot of testing is complete we can check that that the correct number of activities have been executed and we can check that the correct activities have ran.

Also using the status method we can check if its completed.

 

Activities correct number

This method gets all the activities that ran during the process instance.

/// <summary>
 /// Gets the list of activities from process instance
 /// </summary>
 /// <param name="processinstanceId"></param>
 /// <returns></returns>
 public List<Activities> GetActivities(string processinstanceId)
{
List<Activities> list = new List<Activities>();
 SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder hostServerConnectionString = new SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder();
 hostServerConnectionString.Host = servername;
 hostServerConnectionString.Port = 5555;
 hostServerConnectionString.IsPrimaryLogin = true;
 hostServerConnectionString.Integrated = true;
SourceCode.SmartObjects.Client.SmartObjectClientServer serverName = new SourceCode.SmartObjects.Client.SmartObjectClientServer();
 serverName.CreateConnection();
 serverName.Connection.Open(hostServerConnectionString.ToString());
try
 {
SourceCode.SmartObjects.Client.SmartObject smartObject = serverName.GetSmartObject("Activity_Instance");
 smartObject.MethodToExecute = "List";
 smartObject.Properties["ProcessInstanceID"].Value = processinstanceId;
 SourceCode.SmartObjects.Client.SmartObjectList smoList = serverName.ExecuteList(smartObject);
 foreach (SourceCode.SmartObjects.Client.SmartObject item in smoList.SmartObjectsList)
 {
int ProcInstId = 0;
 int ActInstId = 0;
 int.TryParse(item.Properties["ProcessInstanceID"].Value, out ProcInstId);
 int.TryParse(item.Properties["ActivityInstanceID"].Value, out ActInstId);
DateTime startDate = DateTime.Today;
 DateTime finishDate = DateTime.Today;
 DateTime.TryParse(item.Properties["StartDate"].Value, out startDate);
 DateTime.TryParse(item.Properties["FinishDate"].Value, out finishDate);
list.Add(new Activities
 {
 ProcessInstanceId = ProcInstId,
 ActivityInstanceId = ActInstId,
 ActivityName = item.Properties["ActivityName"].Value,
 Status = item.Properties["Status"].Value,
 StartDate = startDate,
 FinishDate = finishDate
});
 }
}
 catch (Exception ex)
 {
 list.Add(new Activities
 {
 ProcessInstanceId = 0,
 ActivityInstanceId = 0,
 ActivityName = ex.Message,
 Status = "Error",
 StartDate = DateTime.Today,
 FinishDate = DateTime.Today
});
}
 finally {
serverName.Connection.Close();
 }
return list;
 }

 

Task: Compare Number Of Activities (Common)

This test compares the number of activities that actually ran for the instance against the number expected

/// <summary>
 /// Checks the correct number of activities ran
 /// </summary>
 [TestMethod]
 public void Activities_CompareNumberOfActivities_Success()
 {
 
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
int NumberOfActivitiesRan = workflow.GetActivities(this.proceessInstance.ToString()).Count;
 String[] activities = this.Activities.Split(';');
 int count = 0;
 foreach (var activity in activities)
 {
 count++;
}
Assert.AreEqual(count, NumberOfActivitiesRan);
 }

 

Activities than ran

This method loops through the activities that have ran against the an array of activities

public Boolean CompareActivities(int ProcessInstanceId, string Activities)
 {
Boolean result = false;
List<Activities> instAct = new List<Activities>();
 try
 {
 instAct = GetActivities(ProcessInstanceId.ToString());
String[] activities = Activities.Split(';');
 int count = 0;
 int match = 0;
 foreach (var activity in activities)
 {
 var instanceactivity = instAct.Where(p => p.ActivityName == activity);
foreach (var act in instanceactivity)
 {
 match++;
 }
count++;
 }
if (count == match)
 { result = true; }
 }
 catch (Exception ex)
 { }
 finally { }
return result;
}

Test: Correct Activities Ran (Common)

This test returns true if the activities ran is the same to the activities in the string array.

 /// <summary>
 /// Checks the correct activities were executed in correct order
 /// </summary>
 /// 
 [TestMethod]
 public void Activities_CorrectActivitiesRan_Success()
 {
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
Boolean actual = workflow.CompareActivities(this.proceessInstance, this.Activities);
 Assert.IsTrue(actual);
 }

 

Code Appendix

Below is the complete code

Interface Iworkflowframework.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace K2WorkflowMap
{
 interface Iworkflowframeworkcs
 {
int StartProcess(string Folio, string ProcessName);
 Boolean IsTaskFound(int ProcessInstanceId, string ProcessName);
 int GetTaskCount(int ProcessInstanceId, string ProcessName);
List<tasklist> GetTask(int ProcessInstanceId, string ProcessName);
 List<TaskDetails> OpenTask(string serialnumber, string destination);
List<ActionList> WhatCanIdo(SourceCode.Workflow.Client.Actions actions);
Boolean ActionTask(string action, string serialnumber, string destinationuser);
Boolean IsTaskComplete(int ProcInst, int Count, string processname);
Boolean IsTaskComplete(string action, string serialnumber, string destinationuser);
string GetWorkflowStatus(int processinstanceId);
List<dataField> GetProcessDataFields(int processinstanceId);
List<Activities> GetActivities(string processinstanceId);
List<Events> GetEvents(string activityid);
List<ActivityDesign> GetWorkflowActivities(int ProcId);
List<Workflow> GetProcesses();
}
}

 

Class Model.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace K2WorkflowMap
{
public enum TestToRun
 {
 Start = 1,
 CheckStatus = 2,
 TaskExists = 3,
 TaskCount = 4,
 CheckTask = 5,
 OpenTaskDetails = 6,
 TaskActions = 7,
 CompleteTask = 8,
 GetActivities = 9
}
 public class tasklist
 {
 public string ActInstDestID { get; set; }
 public string ActivityName { get; set; }
 public string EventName { get; set; }
 public string Destination { get; set; }
 public string Status { get; set; }
 public string SerialNumber { get; set; }
}
public class ActionList
 {
 public string ActionValue { get; set; }
 public string ActionText { get; set; }
 }
public class TaskDetails
 {
public string Status { get; set; }
 public string SerialNumber { get; set; }
 public SourceCode.Workflow.Client.Actions Actions { get; set; }
 public string Data { get; set; }
 public string DataFields { get; set; }
 }
 public class dataField
 {
 public string Name { get; set; }
 public string Value { get; set; }
 public string valueType { get; set; }
 }
public class Activities
 {
 public int ActivityInstanceId { get; set; }
 public int ProcessInstanceId { get; set; }
 public string ActivityName { get; set; }
 public DateTime StartDate { get; set; }
public DateTime FinishDate { get; set; }
 public String Status { get; set; }
 }
public class Events
 {
public int ActivityInstanceId { get; set; }
 public int ProcessInstanceId { get; set; }
 public string EventName { get; set; }
 public string Destination { get; set; }
 public DateTime StartDate { get; set; }
public DateTime FinishDate { get; set; }
 public String Status { get; set; }
 }
public class ActivityDesign
 {
 public int ID { get; set; }
 public string Name { get; set; }
 public string Description { get; set; }
 }
public class Workflow
 {
 public int ID { get; set; }
 public string Name { get; set; }
}
}

Class Results.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.IO;
namespace K2WorkflowUnitTest
{
 public enum TestType
 {
ProcessName,
 Folio,
 ProcessInstance,
 StartStatus,
 TaskCount,
 SerialNumber,
 Destination,
 TaskActivity,
 Actions,
 Activities,
 
 }
 public enum TestSubTypes
 {
Action,
 Activity,
 Event,
 }
public class Results
 {
 private string assemblyFolder;
 private string xmlFileName;
 public Results()
 {
 this.assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
 this.xmlFileName = Path.Combine(assemblyFolder, "DataStoreResults.xml");
 }
 public void SetUpXML(string ProcessName, string Folio)
 {
 //Gets the settings file
 if (!File.Exists(xmlFileName))
 {
 XmlTextWriter writer = new XmlTextWriter(xmlFileName, System.Text.Encoding.UTF8);
 writer.WriteStartDocument(true);
 writer.Formatting = Formatting.Indented;
 writer.WriteStartElement("Settings");
 writer.WriteStartElement("ProcessName");
 writer.WriteString(ProcessName);
 writer.WriteEndElement();
 writer.WriteStartElement("Folio");
 writer.WriteString(Folio);
 writer.WriteEndElement();
 writer.WriteStartElement("ProcessInstanceId");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("StartStatus");
 writer.WriteString("Stop");
 writer.WriteEndElement();
 writer.WriteStartElement("TaskCount");
 writer.WriteAttributeString("Actual","0");
 writer.WriteEndAttribute();
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("SerialNumber");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("Destination");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("TaskActivity");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("Actions");
 writer.WriteStartElement("Action");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteEndElement();
 writer.WriteStartElement("ActionResult");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteStartElement("Activities");
 
 writer.WriteStartElement("Activity");
 
 writer.WriteString("0");
 writer.WriteStartElement("Event");
 writer.WriteString("0");
 writer.WriteEndElement();
 writer.WriteEndElement();
 writer.WriteEndElement();
 writer.WriteEndElement();
 writer.WriteEndDocument();
 writer.Close();
 }
 else
 {
XmlDocument xmlDoc = new XmlDocument();
 xmlDoc.Load(xmlFileName);
 xmlDoc.ChildNodes[1].ChildNodes[0].InnerXml = ProcessName;
 xmlDoc.ChildNodes[1].ChildNodes[1].InnerXml = Folio;
 xmlDoc.Save(xmlFileName);
 }

}


 /// <summary>
 /// Saves the result into XML Document
 /// </summary>
 /// <param name="testType"></param>
 /// <param name="Result"></param>
 public void SaveResult(TestType testType, string Result)
 {
 int childNodeIndex = 0;
 childNodeIndex = (int)testType;
 XmlDocument xmlDoc = new XmlDocument();
 try
 {
 xmlDoc.Load(xmlFileName);
 xmlDoc.ChildNodes[1].ChildNodes[childNodeIndex].InnerXml = Result;
 xmlDoc.Save(xmlFileName);
 }
 catch (Exception ex)
 {
}
 finally
 {
}
}
 /// <summary>
 /// Saves the result of sub node in XML Document
 /// </summary>
 /// <param name="testType"></param>
 /// <param name="subTestType"></param>
 /// <param name="Result"></param>
public void SaveResult(TestType testType, TestSubTypes subTestType ,string Result)
 {
 int childNode = 0;
 childNode = (int)testType;
int childNodeIndex = 0;
 childNodeIndex = (int)subTestType;
XmlDocument xmlDoc = new XmlDocument();
 try
 {
 xmlDoc.Load(xmlFileName);
 xmlDoc.ChildNodes[1].ChildNodes[childNode].ChildNodes[childNodeIndex].InnerXml = Result;
 xmlDoc.ChildNodes[1].ChildNodes[childNode].ChildNodes[childNodeIndex].Attributes["Name"].Value = Result;
 xmlDoc.Save(xmlFileName);
 }
 catch (Exception ex)
 {
}
 finally
 {
}
}
 
 /// <summary>
 /// Gets a saved result
 /// </summary>
 /// <param name="testType"></param>
 /// <returns></returns>
 public string GetResult(TestType testType)
 {
int childNodeIndex = 0;
 childNodeIndex = (int)testType;
string results = string.Empty;
 XmlDocument xmlDoc = new XmlDocument();
 xmlDoc.Load(xmlFileName);
 results = xmlDoc.ChildNodes[1].ChildNodes[childNodeIndex].InnerXml;
 return results;
}

}
}

Class Workflowframework.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Configuration;
namespace K2WorkflowMap
{

public class WorkflowInstanceFramework : Iworkflowframeworkcs
 {
 public string servername = "cft-v-k2int02";
 public string ServiceAccount = @"CHANNEL4\SVCK2MINTMULEUSER";


/// <summary>
 /// Starts a workflow
 /// </summary>
 /// <param name="Folio"></param>
 /// <param name="ProcessName"></param>
 /// <returns></returns>
 public int StartProcess(string Folio, string ProcessName)
 {
 var con = ConfigurationManager.AppSettings;
 int ProcessInstanceId = 0;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 try
 {
 SourceCode.Workflow.Client.ProcessInstance K2Proc = K2Conn.CreateProcessInstance(ProcessName);
 
 K2Proc.Folio = Folio;
 K2Conn.ImpersonateUser(ServiceAccount);
 K2Conn.StartProcessInstance(K2Proc);
 ProcessInstanceId = K2Proc.ID;
 }
 catch (Exception EX)
 {
 ProcessInstanceId = 0;
 }
finally
 {
 K2Conn.Close();
 }
 return ProcessInstanceId;
 }
/// <summary>
 /// Checks to see if there is an task
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 /// <param name="ProcessName"></param>
 /// <returns></returns>
 public Boolean IsTaskFound(int ProcessInstanceId, string ProcessName)
 {
 Boolean Result = false;
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername, 5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
 foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 Result = true;
 }
 }
 }
 catch (Exception ex)
 {
Result = false;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return Result;
 }

/// <summary>
 /// Gets the number of active tasks for instance of a workflow
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 /// <returns></returns>
 public int GetTaskCount(int ProcessInstanceId,string ProcessName)
 {
 int Result = 0;
 int count = 0;
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername ,5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 count++;
 }
 }
 Result = count;
 }
catch (Exception ex)
 {
Result = 0;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return Result;
 }
/// <summary>
 /// Gets details about the task
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 /// <returns></returns>
 public List<tasklist> GetTask(int ProcessInstanceId,string ProcessName)
 {
List<tasklist> list = new List<tasklist>();
 SourceCode.Workflow.Management.WorkflowManagementServer wrkmgt = new SourceCode.Workflow.Management.WorkflowManagementServer(servername, 5555);
 SourceCode.Workflow.Management.WorklistItems worklistItems = null;
 wrkmgt.Open();
 try
 {
 worklistItems = wrkmgt.GetWorklistItems("", "", "", "", "", "", "");
foreach (SourceCode.Workflow.Management.WorklistItem worklistItem in worklistItems)
 {
 if (worklistItem.ProcInstID == ProcessInstanceId)
 {
 var x = worklistItem.ActivityName;
list.Add(new tasklist
 {
 Status = worklistItem.Status.ToString(),
 Destination = worklistItem.Destination,
 EventName = worklistItem.EventName,
 ActInstDestID = worklistItem.ActInstDestID.ToString(),
 ActivityName = worklistItem.ActivityName,
 SerialNumber = (ProcessInstanceId + "_" + worklistItem.ActInstDestID)
 });

}
 }
 }
 catch (Exception ex)
 {
// Result = false;
}
 finally
 {
 wrkmgt.Connection.Close();
 }
return list;
 }
 /// <summary>
 /// Task Details
 /// </summary>
 /// <param name="serialnumber"></param>
 /// <param name="destination"></param>
 /// <returns></returns>
 public List<TaskDetails> OpenTask(string serialnumber, string destination)
 {
List<TaskDetails> list = new List<TaskDetails>();
SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 K2Conn.ImpersonateUser(destination);
 SourceCode.Workflow.Client.WorklistItem K2WListItem = K2Conn.OpenWorklistItem(serialnumber);
try
 {
list.Add(new TaskDetails
 {
 Status = K2WListItem.Status.ToString(),
 SerialNumber = K2WListItem.SerialNumber,
 Actions = K2WListItem.Actions,
 Data = K2WListItem.Data,
 DataFields = K2WListItem.ProcessInstance.DataFields.ToString()
 });
 K2WListItem.Release();
 }
 catch (Exception ex)
 {
 list.Add(new TaskDetails
 {
 Status = "No Task"
 });
}
 finally {
 K2Conn.Close();
 }
 return list;
 }
/// <summary>
 /// List of Actions
 /// </summary>
 /// <param name="actions"></param>
 /// <returns></returns>
 public List<ActionList> WhatCanIdo(SourceCode.Workflow.Client.Actions actions)
 {
 List<ActionList> list = new List<ActionList>();
 if (actions != null)
 {
 foreach (SourceCode.Workflow.Client.Action action in actions)
 {
 list.Add(new ActionList
 {
 ActionText = action.Name,
 ActionValue = action.Name
 });
 }
 }
 else {
list.Add(new ActionList
 {
 ActionText = "No Action",
 ActionValue = "No Action"
 });
 }
return list;
 }

 /// <summary>
 /// Actions a task
 /// </summary>
 /// <param name="action"></param>
 /// <param name="serialnumber"></param>
 /// <param name="destinationuser"></param>
 /// <returns></returns>
 public Boolean ActionTask(string action, string serialnumber, string destinationuser)
 {
 Boolean result = false;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 K2Conn.ImpersonateUser(destinationuser);
 SourceCode.Workflow.Client.WorklistItem K2WListItem = K2Conn.OpenWorklistItem(serialnumber);
 try
 {
K2WListItem.Actions[action].Execute();
 result = true;
 }
 catch (Exception ex)
 {
 result = false;
 }
 finally {
 K2Conn.Close();
 }
 return result;
 }
/// <summary>
 /// Checks to see if the task count has decreased
 /// </summary>
 /// <param name="ProcInst"></param>
 /// <param name="Count"></param>
 /// <returns></returns>
 public Boolean IsTaskComplete(int ProcInst, int Count,string processname)
 {
 Boolean result = false;
 int LastCount = Count;
 int NewCount = GetTaskCount(ProcInst,processname);
 if (LastCount == Count)
 {
 result = false;
 }
 else
 {
 result = true;
 }
return result;
 }
/// <summary>
 /// Checks to see if the actual task that was actioned has been successful
 /// </summary>
 /// <param name="action"></param>
 /// <param name="serialnumber"></param>
 /// <param name="destinationuser"></param>
 /// <returns></returns>
 public Boolean IsTaskComplete(string action, string serialnumber, string destinationuser)
 {
 Boolean result = true;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 K2Conn.ImpersonateUser(destinationuser);
 SourceCode.Workflow.Client.WorklistItem K2WListItem = K2Conn.OpenWorklistItem(serialnumber);
 try
 {
K2WListItem.Actions[action].Execute();
 result = false;
 }
 catch (Exception ex)
 {
 result = true;
 }
 finally
 {
 K2Conn.Close();
 }
 return result;
 }
/// <summary>
 /// Gets the current status of the workflow
 /// </summary>
 /// <param name="processinstanceId"></param>
 /// <returns></returns>
 public string GetWorkflowStatus(int processinstanceId)
 {
 string Result = string.Empty;
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 try
 {
 SourceCode.Workflow.Client.ProcessInstance K2Proc = K2Conn.OpenProcessInstance(processinstanceId);
 switch (K2Proc.Status1)
 {
 case SourceCode.Workflow.Client.ProcessInstance.Status.Active:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Active.ToString();
 break;
 }
case SourceCode.Workflow.Client.ProcessInstance.Status.Completed:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Completed.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Deleted:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Deleted.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Error:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Error.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.New:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.New.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Running:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Running.ToString();
 break;
 }
 case SourceCode.Workflow.Client.ProcessInstance.Status.Stopped:
 {
 Result = SourceCode.Workflow.Client.ProcessInstance.Status.Stopped.ToString();
 break;
 }
 }


 }
 catch (Exception ex)
 { Result = ex.Message; }
 finally
 {
 K2Conn.Close();
 }
return Result;
 }
/// <summary>
 /// Gets the process data fields
 /// </summary>
 /// <param name="processinstanceId"></param>
 /// <returns></returns>
 public List<dataField> GetProcessDataFields(int processinstanceId)
 {
 List<dataField> list = new List<dataField>();
 SourceCode.Workflow.Client.Connection K2Conn = new SourceCode.Workflow.Client.Connection();
 K2Conn.Open(servername);
 try
 {
 SourceCode.Workflow.Client.ProcessInstance K2Proc = K2Conn.OpenProcessInstance(processinstanceId);
 foreach (SourceCode.Workflow.Client.DataField datafield in K2Proc.DataFields)
 {
 list.Add(new dataField {
Name = datafield.Name,
 Value = datafield.Value.ToString(),
 valueType = datafield.ValueType.ToString()
});
 }
 }
 catch (Exception ex)
 {
 list.Add(new dataField
 {
Name = "Error",
 Value = ex.Message.ToString()
});
}
 finally {
 K2Conn.Close();
 }
return list;
 }
 public Boolean CompareActivities(int ProcessInstanceId, string Activities)
 {
Boolean result = false;
List<Activities> instAct = new List<Activities>();
 try
 {
 instAct = GetActivities(ProcessInstanceId.ToString());

String[] activities = Activities.Split(';');
 int count = 0;
 int match = 0;
 foreach (var activity in activities)
 {
 var instanceactivity = instAct.Where(p => p.ActivityName == activity);
foreach (var act in instanceactivity)
 {
 match++;
 }
count++;
 }
if (count == match)
 { result = true; }
 }
 catch (Exception ex)
 { }
 finally { }
return result;
}


/// <summary>
 /// Gets the list of activities from process instance
 /// </summary>
 /// <param name="processinstanceId"></param>
 /// <returns></returns>
 public List<Activities> GetActivities(string processinstanceId)
{
List<Activities> list = new List<Activities>();
 SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder hostServerConnectionString = new SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder();
 hostServerConnectionString.Host = servername;
 hostServerConnectionString.Port = 5555;
 hostServerConnectionString.IsPrimaryLogin = true;
 hostServerConnectionString.Integrated = true;
SourceCode.SmartObjects.Client.SmartObjectClientServer serverName = new SourceCode.SmartObjects.Client.SmartObjectClientServer();
 serverName.CreateConnection();
 serverName.Connection.Open(hostServerConnectionString.ToString());
try
 {
SourceCode.SmartObjects.Client.SmartObject smartObject = serverName.GetSmartObject("Activity_Instance");
 smartObject.MethodToExecute = "List";
 smartObject.Properties["ProcessInstanceID"].Value = processinstanceId;
 SourceCode.SmartObjects.Client.SmartObjectList smoList = serverName.ExecuteList(smartObject);
 foreach (SourceCode.SmartObjects.Client.SmartObject item in smoList.SmartObjectsList)
 {
int ProcInstId = 0;
 int ActInstId = 0;
 int.TryParse(item.Properties["ProcessInstanceID"].Value, out ProcInstId);
 int.TryParse(item.Properties["ActivityInstanceID"].Value, out ActInstId);
DateTime startDate = DateTime.Today;
 DateTime finishDate = DateTime.Today;
 DateTime.TryParse(item.Properties["StartDate"].Value, out startDate);
 DateTime.TryParse(item.Properties["FinishDate"].Value, out finishDate);
list.Add(new Activities
 {
 ProcessInstanceId = ProcInstId,
 ActivityInstanceId = ActInstId,
 ActivityName = item.Properties["ActivityName"].Value,
 Status = item.Properties["Status"].Value,
 StartDate = startDate,
 FinishDate = finishDate
});
 }
}
 catch (Exception ex)
 {
 list.Add(new Activities
 {
 ProcessInstanceId = 0,
 ActivityInstanceId = 0,
 ActivityName = ex.Message,
 Status = "Error",
 StartDate = DateTime.Today,
 FinishDate = DateTime.Today
});
}
 finally {
serverName.Connection.Close();
 }
return list;
 }
/// <summary>
 /// Gets a list of Events for an activity
 /// </summary>
 /// <param name="activityid"></param>
 /// <returns></returns>
 public List<Events> GetEvents(string activityid)
 {
 List<Events> list = new List<Events>();
 SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder hostServerConnectionString = new SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder();
 hostServerConnectionString.Host = servername;
 hostServerConnectionString.Port = 5555;
 hostServerConnectionString.IsPrimaryLogin = true;
 hostServerConnectionString.Integrated = true;
SourceCode.SmartObjects.Client.SmartObjectClientServer serverName = new SourceCode.SmartObjects.Client.SmartObjectClientServer();
 serverName.CreateConnection();
 serverName.Connection.Open(hostServerConnectionString.ToString());
try
 {
SourceCode.SmartObjects.Client.SmartObject smartObject = serverName.GetSmartObject("Event_Instance");
 smartObject.MethodToExecute = "List";
 smartObject.Properties["ActivityInstanceID"].Value = activityid;
 SourceCode.SmartObjects.Client.SmartObjectList smoList = serverName.ExecuteList(smartObject);
 foreach (SourceCode.SmartObjects.Client.SmartObject item in smoList.SmartObjectsList)
 {
int ProcInstId = 0;
 int ActInstId = 0;
 int.TryParse(item.Properties["ProcessInstanceID"].Value, out ProcInstId);
 int.TryParse(item.Properties["ActivityInstanceID"].Value, out ActInstId);
DateTime startDate = DateTime.Today;
 DateTime finishDate = DateTime.Today;
 DateTime.TryParse(item.Properties["StartDate"].Value, out startDate);
 DateTime.TryParse(item.Properties["FinishDate"].Value, out finishDate);
list.Add(new Events
 {
 ProcessInstanceId = ProcInstId,
 ActivityInstanceId = ActInstId,
 EventName = item.Properties["EventName"].Value,
 Status = item.Properties["Status"].Value,
 StartDate = startDate,
 FinishDate = finishDate,
 Destination = item.Properties["Destination"].Value
});
 }
}
 catch (Exception ex)
 {
 list.Add(new Events
 {
 ProcessInstanceId = 0,
 ActivityInstanceId = 0,
 EventName = ex.Message,
 Status = "Error",
 StartDate = DateTime.Today,
 FinishDate = DateTime.Today
});
}
 finally
 {
serverName.Connection.Close();
 }
return list;
 }
public void IsErrors(int processinstanceId, string workflowname)
 {



}


/// <summary>
 /// Gets all the activities that are in a workflow
 /// </summary>
 /// <param name="ProcId"></param>
 /// <returns></returns>
 public List<ActivityDesign> GetWorkflowActivities(int ProcId)
 {
 List<ActivityDesign> list = new List<ActivityDesign>();
 SourceCode.Workflow.Management.WorkflowManagementServer workflowServer = new SourceCode.Workflow.Management.WorkflowManagementServer(servername, 5555);
 workflowServer.Open();
 try
 {
 
 foreach (SourceCode.Workflow.Management.Activity activity in workflowServer.GetProcActivities( ProcId))
 {
 list.Add(new ActivityDesign
 {
ID = activity.ID,
 Name = activity.Name,
 Description = activity.Description
});

 }
 }
 catch (Exception ex)
 {
 list.Add(new ActivityDesign
 {
ID = 0,
 Name = "Error",
 Description = ex.Message
 });
 }
 finally
 {
 workflowServer.Connection.Close();
 }
 return list;
}
/// <summary>
 /// Gets a list of all the workflows
 /// </summary>
 /// <returns></returns>
 public List<Workflow> GetProcesses()
 {
 List<Workflow> list = new List<Workflow>();
SourceCode.Workflow.Management.WorkflowManagementServer workflowServer = new SourceCode.Workflow.Management.WorkflowManagementServer(servername,5555);
 workflowServer.Open();
 try
 {
 SourceCode.Workflow.Management.Criteria.ProcessCriteriaFilter filter = new SourceCode.Workflow.Management.Criteria.ProcessCriteriaFilter();
 SourceCode.Workflow.Management.Processes processes = workflowServer.GetProcesses(filter);
 foreach (SourceCode.Workflow.Management.Process process in processes)
 {
 if (process.DefaultVersion == true)
 {
 list.Add(
 new Workflow
 {
 ID = process.ProcID,
 Name = process.FullName
}
);
 }
 }
 }
 catch (Exception ex)
 {
 list.Add(
 new Workflow
 {
 ID = 0,
 Name = ex.Message
}
);
 }
 finally
 {
 workflowServer.Connection.Close();
 }
 return list;
 }
 }
}

 

XML DataStoreResult.xml

Used to hold test data

<?xml version="1.0" encoding="utf-8" ?>
<Settings>
<ProcessName>k2-server-int02</ProcessName>
<Folio></Folio>
<ProcessInstanceId>5555</ProcessInstanceId>
<StartStatus>End</StartStatus>
<TaskCount Actual="1">0</TaskCount>
 <SerialNumber>00_00</SerialNumber>
 <Destination>Bob</Destination>
 <Actions>
 <Action>Approve</Action>
 </Actions>
 <ActionResult>Approve</ActionResult>
 <Activities>
 <Activity>
 <Event name="Status Check">Pass</Event>
 </Activity>
 </Activities>
</Settings>

Unit Test Unittest.cs

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;
namespace K2WorkflowUnitTest
{
 [TestClass]
 public class UnitTest1
 {
private string folio;
 private string ProcessName;
 private int proceessInstance;
 private string SerialNumber;
 private string TaskAction;
 private int TaskCount = 0;
 private string Activities;
 private string DestinationUser;
 private string TaskActivity;
 private int milliseconds;
 private SourceCode.Workflow.Client.Actions Actions;
 private K2WorkflowMap.WorkflowInstanceFramework workflow;
 private Results Results;
public int ProcInstId { get; set; }
[TestInitialize]
 public void TestInitialize()
 {
 this.folio = "I am a folio 3";
 this.ProcessName = @"K2Project2\TestProcess";
 this.TaskActivity = "Task";
 this.TaskAction = "Approve";
 this.Activities = "Setup;Task;Approve;End";
 this.TaskCount = 1;
 this.milliseconds = 3000;
 workflow = new K2WorkflowMap.WorkflowInstanceFramework();
 Results = new Results();
 Results.SetUpXML(this.ProcessName, this.folio);
 }
 /// <summary>
 /// Tests the starting of a workflow, should return a number greater than 0
 /// </summary>
 /// <param name="folio"></param>
 /// <param name="ProceesName"></param>
 /// <returns></returns>
 [TestMethod]
 public void StartWorkflow_Success()
 {
int actual = workflow.StartProcess(this.folio, this.ProcessName);
 this.proceessInstance = actual;
 Results.SaveResult(TestType.ProcessInstance, this.proceessInstance.ToString());
 Assert.AreNotEqual(0, actual);
 ProcInstId = this.proceessInstance;
}
/// <summary>
 /// Test to check that the workflow has started, by checking it's status
 /// </summary>
 /// <param name="ProcessInstanceId"></param>
 [TestMethod]
 public void StartWorkflow_Running_Success()
 {
int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 string actual = workflow.GetWorkflowStatus(this.proceessInstance);
 Results.SaveResult(TestType.StartStatus, actual);
 StringAssert.Equals(SourceCode.Workflow.Client.ProcessInstance.Status.Active.ToString(), actual);
 }

/// <summary>
 /// Tests to see if tasks have been generated for this instance
 /// </summary>
 [TestMethod]
 public void WorkList_TasksFound_Success()
 {
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 Boolean actual = false;
 actual = workflow.IsTaskFound(this.proceessInstance, this.ProcessName);
 Assert.AreEqual(true, actual);
 }
 /// <summary>
 /// Checks that the correct number of tasks is generated
 /// </summary>
 [TestMethod]
 public void WorkList_CorrectNumberOfTasksCreated_Success()
 {
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 int actual = 0;
 actual = workflow.GetTaskCount(this.proceessInstance, this.ProcessName);
 Results.SaveResult(TestType.TaskCount, actual.ToString());
 Assert.AreEqual(this.TaskCount, actual);
 }
 /// <summary>
 /// Gets the activity name of the task
 /// </summary>
 [TestMethod]
 public void WorkList_RetrieveTaskList_Success()
 {
 string Actual = string.Empty;
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
var task = workflow.GetTask(this.proceessInstance, this.ProcessName);
 
 this.SerialNumber = task[0].SerialNumber;
 Results.SaveResult(TestType.SerialNumber, this.SerialNumber);
 this.DestinationUser = task[0].Destination;
 Results.SaveResult(TestType.Destination, this.DestinationUser);
 Actual = task[0].ActivityName;
 Results.SaveResult(TestType.TaskActivity, Actual);
 Assert.AreEqual(this.TaskActivity, Actual);
}
 /// <summary>
 /// Actions a task
 /// </summary>
 [TestMethod]
 public void Task_GetActions_Sucess()
 {
 Boolean Actual = false;
 this.SerialNumber = Results.GetResult(TestType.SerialNumber);
 this.DestinationUser = Results.GetResult(TestType.Destination);
K2WorkflowMap.TaskDetails task = workflow.OpenTask(this.SerialNumber, this.DestinationUser)[0];
 Actual = workflow.ActionTask(this.TaskAction, this.SerialNumber, this.DestinationUser);
 Assert.AreEqual(true, Actual);
}
/// <summary>
 /// checks to see if task is complete by checking that task has gone
 /// </summary>
 [TestMethod]
 public void Task_CheckTaskComplete_Success()
 {
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
 int actual = 0;
 actual = workflow.GetTaskCount(this.proceessInstance, this.ProcessName);
 Results.SaveResult(TestType.TaskCount, actual.ToString());
 Assert.AreEqual(0, actual);
 }
 /// <summary>
 /// Checks the correct number of activities ran
 /// </summary>
 [TestMethod]
 public void Activities_CompareNumberOfActivities_Success()
 {
 
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
int NumberOfActivitiesRan = workflow.GetActivities(this.proceessInstance.ToString()).Count;
 String[] activities = this.Activities.Split(';');
 int count = 0;
 foreach (var activity in activities)
 {
 count++;
}
Assert.AreEqual(count, NumberOfActivitiesRan);
 }
/// <summary>
 /// Checks the correct activities were executed in correct order
 /// </summary>
 /// 
 [TestMethod]
 public void Activities_CorrectActivitiesRan_Success()
 {
 Thread.Sleep(milliseconds);
 int.TryParse(Results.GetResult(TestType.ProcessInstance), out this.proceessInstance);
Boolean actual = workflow.CompareActivities(this.proceessInstance, this.Activities);
 Assert.IsTrue(actual);
 }


}
}

Testing Smartforms with the Smartform tester in K2 4.7

There are a number of Smartform testing tools currently on the market to allow developers to test their Smartforms. But in 4.7 there is something hidden away that is just dying to be played with. Like the developer’s best friend SmartObject Service Tester, there is now a tool that will allow you to automate testing of your Smartforms.

How to access it

To access this hidden gem of a tool, go to the K2 Designer and click on ‘show’ link at the bottom of the page and in the pop up click on System.

show-all

How you will notice in the context browser for the designer there is now some additional categories.

This slideshow requires JavaScript.

  1. Expand System
  2. Expand Forms
  3. Expand Smartform tester
  4. Click on ‘SmartForms Tester – Landing Page’
  5. Click on the ‘Runtime URL’ in the properties page
  6. The Smartform Tester Portal will now load up and we can start to build a test case

 

SmartForms Tester Portal

This is where we can manage the automating of the forms from creating new tests to running existing ones.

smartform-tester-form

K2 do give us some examples of how to use the tester portal in the ‘Examples’ folder or what they like to call ‘Suites’

So lets create a ‘Suite’

 

Creating a Test Suite

  1. Click on ‘Test Suites’
  2. A list of options will appear at the top
  3. Click on ‘New Suite’
  4. A pop up window will appear enter in the name of  what your suite is going to be called. I am going to call mine ‘Annual Leave’
  5. Click on ‘Accept’
  6. Click on ‘Refresh List’ button
  7. You will now see the suite you just created in the list

This slideshow requires JavaScript.

 

Creating  a new test

  1. Click on the suite you just created
  2. The list options above will change
  3. Click on ‘New Test’
  4. This will open a new window, from where we can build a new test case
  5. Click on smartform-tester-form25 in the Smartform section and choose the View or Smartform you want to test. In this case i am going to create a simple test to test a view
  6. When you select a view/form it will then appear in the in  top section of the testing  form
  7. Below the name of the select artifact
  8. Is the actions this where will build the test script that the portal will execute for us

 

This slideshow requires JavaScript.

Creating the automated test script

  1. In the actions section, you will see there part of a action already selected
  2. Because i have chosen a view to test. The first drop down list has ‘View’ selected. The 2nd drop down list has ‘Control’ selected.
  3. From the 3rd drop down list we can now choose a control on the view
  4. I am going to choose  the picker control ‘pkrUsername’.
  5. A 4th drop down list appears and should have ‘Change Value’ selected
  6. Text box is also available, where a value can be entered. I am going to enter in ‘sallport’smartform-tester-form8
  7. When we execute this action  the tester, will put ‘sallport’ into the picker control for you.
  8. Click on ‘Add’ to add another action smartform-tester-form26
  9. I am going to select the ‘calStartDate’ control and enter in a date and i am going to the same with for ‘calEndDate’
  10. I am going to add another action so it can ‘select’ a leave type. I am going to enter in ‘Paid leave’
  11. Last of all i am going to select the ‘Button’ control and tell it to click the button.
  12. Now that my simple test script of filling out the view and clicking on the button to submit the data. I am ready to get the tester to run it for me.
  13. But before I do that just click on the smartform-tester-form27 in the top left hand corner and select ‘Save’ from the context menu
  14. ‘Save Test’ pop window will appear, enter in a name for test and make sure the correct suite is selected and click on ‘Ok’

This slideshow requires JavaScript.

Running the test

From the test designer, we can either run the whole script or by clicking one of the actions we can test a particular line.

  1. I am going to run the whole script, so click on ‘Run All’ button. The designer will then carry out the actions line by line.
  2. A report will then be displayed telling you if it was successful or not.
  3. The actions line will also all be green and of course the data inserted into the view will be displayed and of course you can check the SmartObject that was used to save the data.
  4. You can also run the test from main testing portal , by selecting the test from the suite and then clicking on ‘Run Test’ option

This slideshow requires JavaScript.

 

So this is a basic how to use the Smartform tester, and there are other options that I have not covered such as delayed actions and modifying variables and assertions such total count of items in a list etc..

Demo forms can be downloaded from here

Conclusion

What is good about this tool it’s all built using Smartforms, so not only are we seeing some new controls that are not made available yet to in the Smartform designer, such as the context menu, the action list with is probably a more advanced list view. I hope these will made available soon.

The 2nd good thing about this is that the test data is available as SmartObjects which mean we can then use these to build rich testing reports that also take into account business data.

It’s great start to something that will hopefully be fleshed out with further updates in the future. So it would be possible to fill out the form, get a task and action it, all in one test case.

 

 

 

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.