Workflow Driven Development: Asserting a Workflow using an Audit Trail

Share Button

There are lots of different ways that you can track an end user’s interaction with your application. In the beginning, it may be good enough to infer current user state or interaction by analyzing data artifacts related to a given user, but this fast becomes an unmaintainable mess. The next logical step of a user tracking requirement is to maintain a log. This kind of log is typically referred to as an audit trail or audit log. Audit trails are used in a variety of scenario’s, but as its name implies the most common is to audit a user’s action from a security perspective. For example, an accounting application will keep a log of every single debit and credit made on your accounts and then relate these transactions to other accounts, the current time, the active user, and perhaps many other attributes. Versioning is another common implementation that allows users to move forward and back through the state of their data at ease (i.e. adobe photoshop history, source control management systems, etc).

For my purposes, I only want to be able to track certain business events a user raises. Originally this was only intended for reporting purposes. For example.

  • How many users choose path A vs B?
  • What’s the most commonly used function in a client session?
  • How long is the duration of a typical user session?

But with an audit trail and arbitrary audit events logged in my application the sky’s the limit for what type of information I can infer. It’s even possible to use this data to enforce business rules in my app!

I’ve been attempting to follow the practices of BDD & TDD so I began my implementation with specifying an acceptance test. It soon became apparent that not only can we assert that certain domain events have occurred, but also what order they were recorded in. This gave us the capability to assert a workflow for the application behaviour under test. As a team (not only developers, but business analysts, testers, and internal clients too), we came up domain events that were defined using the ubiquitous language of the business. This ensured that developers to clients alike could all understand an audit trail.

Example

For the sake of this example I won’t require much information associated with every event, just the fact that events occur in a particular order is good enough. The information I need includes a definition of my audit events, a facebook user, a transient web session, and the audit trail itself. A WebSession record is created for every new user session that is created (think of the ASP.NET Session). Each domain event I want to capture is statically defined in a lookup table known as AuditEvents. When a new Facebook user signs in they’re added to FacebookUser. And finally, the AuditTrail ties a user’s web session to each domain event raised in our application. Below is a simple SQL Server diagram to illustrates the schema.

In the application I created an enumeration with all of the possible domain events. Then a fairly routine use case of the app was identified and I wrote a new acceptance test for it. I declared an expected list of domain events that should be recorded as part of the audit trail. It’s important to assert not only that the domain events occurred, but also in what sequence they happened in, therefore order was important in the declaration of our expected events. Below is an example of the kind of events we chose to assert.

var expectedAuditEvents = new List
					{
						AuditEvent.UserCreatedASession,
						AuditEvent.UserEnteredTodaysDraw,
						AuditEvent.UserDidNotWinTodaysDraw,
						AuditEvent.ContinuedToRecommendationPage,
						AuditEvent.UserIntendsToRecommendOnFacebook,
						AuditEvent.FacebookAuthorizationWasSuccessful,
						AuditEvent.UserSharedRecommendation,
						AuditEvent.UserSuccessfullySharedOnFacebook
					}

After the use case was executed we retrieved the list of actual audit events that were associated with the transient user session and asserted them against the expected list.

A full example is defined below.

[Test]
public void Share_A_Recommendation_On_Facebook_And_Assert_Correct_Audit_Trail()
{
	// GIVEN
	I_Create_A_New_User_Session();
	I_Enter_Todays_Draw();

	// WHEN
	I_Choose_To_Share_A_Recommendation_On_Facebook();

	// AND WHEN
	I_Login_To_Facebook();

	// AND WHEN
	I_Confirm_My_Recommendation_Is_Correct();

	// THEN
	I_Assert_I_Am_On_The_Callback_Page();

	// AND THEN
	I_Assert_My_Session_Audit_Trail();
}

protected void I_Assert_My_Session_Audit_Trail()
{
	var actualAuditEvents = GetTransientSessionOrderedAuditEvents();
	AssertAuditTrail(new List
					{
						AuditEvent.UserCreatedASession,
						AuditEvent.UserEnteredTodaysDraw,
						AuditEvent.UserDidNotWinTodaysDraw,
						AuditEvent.ContinuedToRecommendationPage,
						AuditEvent.UserIntendsToRecommendOnFacebook,
						AuditEvent.FacebookAuthorizationWasSuccessful,
						AuditEvent.UserSharedRecommendation,
						AuditEvent.UserSuccessfullySharedOnFacebook
					}, actualAuditEvents);
}

protected void AssertAuditTrail(IList expectedAuditEvents, IList actualAuditEvents)
{
	Assert.AreEqual(expectedAuditEvents.Count, actualAuditEvents.Count, "The number of audit events is different.");

	for (var i = 0; i < expectedAuditEvents.Count; i++)
	{
		Assert.AreEqual(expectedAuditEvents[i], actualAuditTrail[i], "The actual audit event was noted expected.");
	}
}

Conclusion

After creating a few of these tests it dawned on me how powerful this process was. Creating acceptance tests that assert based on page state can become very brittle. In my experience a page design changes a lot more frequently than a workflow. I believe tests that assert workflow are not only a lot more resilient, bult also more maintainable and readable. Defining workflow first (Workflow Driven Development) as with any test first development method, forces you to come up with a [workflow] design before you actually trigger any events. When thinking of your use cases this way you can quickly identify the steps of your application that may make it more difficult to use. I’m excited to write more testing using this process in the future and I welcome any feedback from the community.

Share Button