Mini test suite for async CQS backend

Our backend consists of a WebApi backend sporting an async CQS API (Not involving Event sourcing). I wanted a fluent syntax setup for our scenario tests, here is an actual example from the resulting code. (Code removed that are customer specific)

[TestClass]
public class When_doing_a_complete_booking : BusinessTest
{
	private Booking _result;
	private DateTime _date;

	[TestInitialize]
	public void Context()
	{
		Guid bookingKey = Guid.Empty;
		_date = DateTime.Now.AddDays(5);

		_result = Given(db => /* Setup here */)
			.When(() => new SearchQuery{ Date = _date, ...})
			.And(result =>
			{
				bookingKey = result.First().BookingKey;
				return new ReserveCommand { BookingKey = bookingKey, ... };
			})
			.And(() => new ConfirmCommand
			{
				BookingKey = bookingKey, 
				...
			})
			.Then(db => db.Set<Booking>().FirstOrDefaultAsync(b => b.BookingKey == bookingKey));
	}

	[TestMethod]
	public void It_should_book_correctly ()
	{
		Assert.IsNotNull(_result);
		Assert.IsTrue(...);
	}
}

For this we need some async helper magic, first the Test base class, nothing fancy here

public abstract class BusinessTest
{
	static BusinessTest()
	{
		Core.AutoMapper.Config.Initialize();
	}

	private readonly IContainer _container;
		
	protected BusinessTest()
	{
		_container = Bootstrapper.Create();
		_container.Configure(config => config.For(typeof(IBusinessWorker<>)).Use(typeof(DummyWorker<>)));
	}

	protected GivenResult Given(Func<DbContext, Task> given)
	{
		return new GivenResult(given, _container);
	}

	protected GivenResult Given(Action<DbContext> given)
	{
		return Given(async db => given(db));
	}
}

Here you can choose to setup the DB either async or none async using the Given method. Next we need to create a GivenResult class that can handle next step which is specifying When scenario.

public class GivenResult : TestResultBase
{
	private Func<DbContext, Task> _given;

	public GivenResult(Func<DbContext, Task> given, IContainer container) : base(container)
	{
		_given = given;
	}

	public GivenResult And(Func<DbContext, Task> given)
	{
		var orgGiven = _given;
		_given = async db =>
		{
			await orgGiven(db);
			await given(db);
		};

		return this;
	}

	public GivenResult And(Action<DbContext> given)
	{
		return And(async db => given(db));
	}

	public WhenResult<TResult> When<TResult>(Func<Query<TResult>> when)
	{
		return new WhenResult<TResult>(_given, async () => await Invoke(when()), _container);
	}

	public WhenResult When(Func<Command> when)
	{
		return new WhenResult(_given, async () => await Invoke(when()), _container);
	}
}

Here we can chose to either chain the Given setup with another Given setup using the And method. This is useful when defining common and reusable Given scenarios somthing like

GivenThatAnOrderIsPayed()
.And(db => ...)

You can also choose to either specify a Command or Query using the When overloads. Since our Queries inherit Query of TResult the fluent synstax is more readable since you can omit the generic result argument. Next we need to specify a WhenResult class, here we have to create one generic class for Queries and a none generic one for Commands.

public class WhenResult<TResult> : WhenBase
{
	private readonly Func<Task<TResult>> _when;

	public WhenResult(Func<DbContext, Task> given, Func<Task<TResult>> when, IContainer container) : base(given, container)
	{
		_when = when;
	}

	public TAssert Then<TAssert>(Func<TResult, DbContext, Task<TAssert>> then)
	{
		return ThenInternal(_when, then);
	}

	public TAssert Then<TAssert>(Func<TResult, TAssert> then)
	{
		return Then((r, db) => Task.FromResult(then(r)));
	}

	public WhenResult And(Func<TResult, Command> when)
	{
		workDelegated = true;

		return new WhenResult(_given, async () =>
		{
			var result = await _when();
			await Invoke(when(result));
		}, _container);
	}

	public WhenResult<TResultOuter> And<TResultOuter>(Func<TResult, Query<TResultOuter>> when)
	{
		workDelegated = true;

		return new WhenResult<TResultOuter>(_given, async () =>
		{
			var input = await _when();
			return await Invoke(when(input));
		}, _container);
	}
}

Here you can choose to chain the WhenResult, this is useful when you want to execute a scenario that consist of several Commands or Queries, like my example above. We also have a none generic version for the Commands

public class WhenResult : WhenBase
{
	private readonly Func<Task> _when;

	public WhenResult(Func<DbContext, Task> given, Func<Task> when, IContainer container) : base(given, container)
	{
		_when = when;
	}

	public TAssert Then<TAssert>(Func<DbContext, Task<TAssert>> then)
	{
		return ThenInternal(async () =>
		{
			await _when();
			return true;
		}, (r, ctx) => then(ctx));
	}

	public WhenResult And(Func<Command> when)
	{
		workDelegated = true;

		return new WhenResult(_given, async () =>
		{
			await _when();
			await Invoke(when());
		}, _container);
	}

	public WhenResult<TResult> And<TResult>(Func<Query<TResult>> when)
	{
		workDelegated = true;

		return new WhenResult<TResult>(_given, async () =>
		{
			await _when();
			return await Invoke(when());
		}, _container);
	}
}

Last but not least we need to execute the Test when one of the Then methods is called. Its done using the ThenInternal method in the WhenBase class.

public class WhenBase : TestResultBase
{
	protected readonly Func<DbContext, Task> _given;
	protected bool workDelegated = false;

	public WhenBase(Func<DbContext, Task> given, IContainer container) : base(container)
	{
		_given = given;
	}

	protected TResult Get<TResult>()
	{
		return _container.GetInstance<TResult>();
	}

	protected TAssert ThenInternal<TResult, TAssert>(Func<Task<TResult>>  when, Func<TResult, DbContext, Task<TAssert>> then)
	{
		if(workDelegated) throw new ApplicationException("The work from this WHEN result has been delegated to a new result, calling THEN, using this instance is invalid!");

		var reset = new AutoResetEvent(false);
		TAssert assert = default(TAssert);
		Task.Run(async () =>
		{
			try
			{
				using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
				{
					var db = Get<DbContext>();
					await _given(db);
					await db.SaveChangesAsync();

					var result = await when();
					assert = await then(result, Get<DbContext>());
				}
			}
			finally
			{
				reset.Set();
			}
		});

		reset.WaitOne();
		return assert;
	}
}

Its here the magic happens, we execute all the steps defined earlier using Task.Run, we also make sure test runner thread waits until the scenario is done or fails.

This is specific to our infrastructure, but I also have a base class that takes care of invoking the Commands and Queries

public class TestResultBase
{
	protected readonly IContainer _container;

	protected TestResultBase(IContainer container)
	{
		_container = container;
	}

	protected async Task Invoke(Command command)
	{
		using(var nested = _container.GetNestedContainer())
		{
			await nested.GetInstance<IInvoker>().Invoke(command);
		}
	}

	protected async Task<TResult> Invoke<TResult>(Query<TResult> query)
	{
		using(var nested = _container.GetNestedContainer())
		{
			return (TResult)await nested.GetInstance<IInvoker>().Invoke(query);
		}
	}
}

Update: And its easy to extend
Needed a way to be able to repeat commands. It was fairly easy to extend added this method to the non generic When command class (Probably no need to repeat Queries).

public RepeatWhenResult Repeat(Func<Command> when)
{
	return new RepeatWhenResult(this, when);
}

Small class to handle the repeat

public class RepeatWhenResult
{
	private readonly WhenResult _parent;
	private readonly Func<Command> _when;

	public RepeatWhenResult(WhenResult parent, Func<Command> when)
	{
		_parent = parent;
		_when = when;
	}

	public WhenResult Twice()
	{
		return Count(2);
	}

	public WhenResult Count(int count)
	{
		var current = _parent;
		Enumerable.Range(0, count).ForEach(i =>
		{
			current = current.And(_when);
		});
		return current;
	}
}

Usage

_result = Given(db => /* Setup here */)
.When(() => new SearchQuery{ Date = _date, ...})
.And(result =>
{
	bookingKey = result.First().BookingKey;
	return new ReserveCommand { BookingKey = bookingKey, ... };
})
.And(() => new ConfirmCommand
{
	BookingKey = bookingKey, 
	...
})
.Repeat(() => new CancelCommand { BookingKey = bookingKey  })
.Twice()
.Then(db => db.Set<Booking>().FirstOrDefaultAsync(b => b.BookingKey == bookingKey));

Leave a Reply

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

WordPress.com Logo

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

Facebook photo

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

Connecting to %s