Skip to content

Instantly share code, notes, and snippets.

@abdullin
Created September 14, 2012 14:01
Show Gist options
  • Save abdullin/3722071 to your computer and use it in GitHub Desktop.
Save abdullin/3722071 to your computer and use it in GitHub Desktop.
Rough cuts of improved Simple Testing
// sample unit test for a command "LockUser"
public class lock_user : user_syntax
{
static readonly UserId id = new UserId(1);
static readonly SecurityId sec = new SecurityId(1);
static readonly TimeSpan fiveMins = TimeSpan.FromMinutes(5);
[Test]
public void given_new_user()
{
Given(new UserCreated(id, sec, fiveMins));
When(new LockUser(id, "reason"));
Then(new UserLocked(id, "reason", sec, Current.MaxValue));
}
[Test]
public void given_locked_user()
{
Given(new UserCreated(id, sec, fiveMins),
new UserLocked(id, "locked", sec, Current.MaxValue));
When(new LockUser(id, "lock again"));
Then();
}
[Test]
public void given_temporarily_locked_user()
{
Given(new UserCreated(id, sec, fiveMins),
new UserLocked(id, "locked", sec, Time(1, 20)));
When(new LockUser(id, "lock again"));
Then(new UserLocked(id, "lock again", sec, Current.MaxValue));
}
[Test]
public void given_no_user()
{
When(new LockUser(id, "Reason"));
Then("premature");
}
[Test]
public void given_deleted_user()
{
Given(new UserCreated(id, sec, TimeSpan.FromMinutes(5)),
new UserDeleted(id, sec));
When(new LockUser(id, "sec"));
Then("zombie");
}
}
Sample printout in unit tests
Test: lock user
Specification: given temporarily locked user
GIVEN:
1. Created user User-1 (security Security-1) with threshold 00:05:00
2. User User-1 locked with reason 'locked'.
WHEN:
Lock user User-1 with reason 'lock again'
THEN:
1. User User-1 locked with reason 'lock again'.
Results: [Passed]
--------------------------------------------------------------------
Test: lock user
Specification: given deleted user
GIVEN:
1. Created user User-1 (security Security-1) with threshold 00:05:00
2. Deleted user User-1 from security Security-1
WHEN:
Lock user User-1 with reason 'sec'
THEN:
1. Domain error 'zombie'
Results: [Passed]
--------------------------------------------------------------------
Test: lock user
Specification: given locked user
GIVEN:
1. Created user User-1 (security Security-1) with threshold 00:05:00
2. User User-1 locked with reason 'locked'.
WHEN:
Lock user User-1 with reason 'lock again'
THEN nothing.
Results: [Passed]
--------------------------------------------------------------------
Test: lock user
Specification: given new user
GIVEN:
1. Created user User-1 (security Security-1) with threshold 00:05:00
WHEN:
Lock user User-1 with reason 'reason'
THEN:
1. User User-1 locked with reason 'reason'.
Results: [Passed]
--------------------------------------------------------------------
Test: lock user
Specification: given no user
GIVEN no events
WHEN:
Lock user User-1 with reason 'Reason'
THEN:
1. Domain error 'premature'
Results: [Passed]
--------------------------------------------------------------------
// ReSharper disable InconsistentNaming
// this is core class, that defines testing and printing functionality
// replace Describe.Object(obj) with obj.ToString() if you use DSL
public abstract class spec_syntax<T> where T : IIdentity
{
readonly List<IEvent<T>> _given = new List<IEvent<T>>();
ICommand<T> _when;
readonly List<IEvent<T>> _then = new List<IEvent<T>>();
readonly List<IEvent<T>> _expectedEvents = new List<IEvent<T>>();
protected static DateTime Date(int year, int month = 1, int day = 1, int hour = 0)
{
return new DateTime(year, month, day, hour, 0, 0, DateTimeKind.Unspecified);
}
protected static DateTime Time(int hour, int minute = 0, int second = 0)
{
return new DateTime(2011, 1, 1, hour, minute, second, DateTimeKind.Unspecified);
}
protected class ExceptionThrown : IEvent<T>
{
public T Id { get; private set; }
public string Name { get; set; }
public ExceptionThrown(string name)
{
Name = name;
}
public override string ToString()
{
return string.Format("Domain error '{0}'", Name);
}
}
protected IEvent<T> ClockWasSet(int year, int month = 1, int day = 1)
{
var date = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
return new AesSetupEvent<T>(() => Current.DateIs(date), "Test clock set to {0:yyyy-MM-dd}", date);
}
protected IEvent<T> GuidWasFixed(string guid)
{
return new AesSetupEvent<T>(() => Current.GuidIs(guid), "Guid provider fixed to " + guid);
}
public void Given(params IEvent<T>[] g)
{
_given.AddRange(g);
foreach (var @event in g)
{
var setup = @event as AesSetupEvent<T>;
if (setup != null)
{
setup.Apply();
}
else _expectedEvents.Add(@event);
}
}
public void When(ICommand<T> command)
{
_when = command;
}
[SetUp]
public void Clear()
{
_when = null;
_given.Clear();
_then.Clear();
_expectedEvents.Clear();
}
protected void Print()
{
Console.WriteLine("Test: {0}", GetType().Name.Replace("_"," "));
Console.WriteLine("Specification: {0}", TestContext.CurrentContext.Test.Name.Replace("_", " "));
Console.WriteLine();
if (_given.Any())
{
Console.WriteLine("GIVEN:");
for (int i = 0; i < _given.Count; i++)
{
PrintAdjusted(" " + (i + 1) + ". ", Describe.Object(_given[i]).Trim());
}
}
else
{
Console.WriteLine("GIVEN no events");
}
if (_when != null)
{
Console.WriteLine();
Console.WriteLine("WHEN:");
PrintAdjusted(" ", Describe.Object(_when).Trim());
}
Console.WriteLine();
if (_then.Any())
{
Console.WriteLine("THEN:");
for (int i = 0; i < _then.Count; i++)
{
PrintAdjusted(" " + (i + 1) + ". ", Describe.Object(_then[i]).Trim());
}
}
else
{
Console.WriteLine("THEN nothing.");
}
}
protected void PrintResults(ICollection<ExpectResult> exs)
{
var results = exs.ToArray();
var failures = results.Where(f => f.Failure != null).ToArray();
if (!failures.Any())
{
Console.WriteLine();
Console.WriteLine("Results: [Passed]");
return;
}
Console.WriteLine();
Console.WriteLine("Results: [Failed]");
for (int i = 0; i < results.Length; i++)
{
PrintAdjusted(" " + (i + 1) + ". ", results[i].Expectation);
PrintAdjusted(" ", results[i].Failure ?? "PASS");
}
}
protected abstract void ExecuteCommand(IEventStore store, ICommand<T> cmd);
public void Then(string error)
{
Then(new ExceptionThrown(error));
}
public void Then(params IEvent<T>[] g)
{
_then.AddRange(g);
IEnumerable<IEvent<T>> actual;
var store = new InMemoryStore(_expectedEvents.Cast<IEvent<IIdentity>>().ToArray());
try
{
ExecuteCommand(store, _when);
actual = store.Store.Skip(_expectedEvents.Count).Cast<IEvent<T>>().ToArray();
}
catch(DomainError e)
{
actual = new IEvent<T>[] {new ExceptionThrown(e.Name)};
}
var results = CompareAssert(_then.Cast<IEvent<IIdentity>>().ToArray(), actual.Cast<IEvent<IIdentity>>().ToArray()).ToArray();
Print();
PrintResults(results);
if (results.Any(r => r.Failure != null))
Assert.Fail("Specification failed");
}
public static string GetAdjusted(string adj, string text)
{
bool first = true;
var builder = new StringBuilder();
foreach (var s in text.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
{
builder.Append(first ? adj : new string(' ', adj.Length));
builder.AppendLine(s);
first = false;
}
return builder.ToString();
}
public static void PrintAdjusted(string adj, string text)
{
bool first = true;
foreach (var s in text.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
{
Console.Write(first ? adj : new string(' ', adj.Length));
Console.WriteLine(s);
first = false;
}
}
protected static IEnumerable<ExpectResult> CompareAssert(IEvent<IIdentity>[] expected, IEvent<IIdentity>[] actual)
{
for (int i = 0; i < expected.Length; i++)
{
var expectedHumanReadable = Describe.Object(expected[i]);
if (actual.Length > i)
{
var diffs = CompareObjects.FindDifferences(expected[i], actual[i]);
if (string.IsNullOrEmpty(diffs))
{
yield return new ExpectResult
{
Expectation = expectedHumanReadable
};
}
else
{
var actualHumanReadable = Describe.Object(actual[i]);
if (actualHumanReadable != expectedHumanReadable)
{
// there is a difference in textual representations
yield return new ExpectResult
{
Expectation = expectedHumanReadable,
Failure = GetAdjusted("Was: ", actualHumanReadable)
};
}
else
{
yield return new ExpectResult
{
Expectation = expectedHumanReadable,
Failure = diffs
};
}
}
}
else
{
yield return new ExpectResult()
{
Expectation = expectedHumanReadable,
Failure = " Message is missing"
};
}
}
for (int i = expected.Length; i < actual.Count(); i++)
{
var msg = GetAdjusted("Was: ", Describe.Object(actual[i]));
yield return new ExpectResult
{
Expectation = "Unexpected message",
Failure = msg
};
}
}
public class ExpectResult
{
public string Failure;
public string Expectation;
}
sealed class InMemoryStore : IEventStore
{
public readonly List<IEvent<IIdentity>> Store = new List<IEvent<IIdentity>>();
public InMemoryStore(IEnumerable<IEvent<IIdentity>> given)
{
Store.AddRange(given);
}
EventStream IEventStore.LoadEventStream(IIdentity id)
{
return new EventStream
{
Events = Store.Where(i => id.Equals(i.Id)).ToList(),
Version = Store.Count(i => id.Equals(i.Id))
};
}
void IEventStore.AppendToStream(IIdentity id, long originalVersion, ICollection<IEvent<IIdentity>> events, string explanation)
{
foreach (var @event in events)
{
Store.Add(@event);
}
}
}
}
public sealed class AesSetupEvent<T> : IEvent<T> where T : IIdentity
{
public T Id { get; set; }
readonly string _describe;
readonly Action _act;
public AesSetupEvent(Action act, string describe, params object[] args)
{
_act = act;
_describe = string.Format(describe, args);
}
public void Apply()
{
_act();
}
public override string ToString()
{
return _describe;
}
}
// aggregate-specific fixture base (for User aggregate in this case)
public abstract user_syntax : spec_syntax<UserId>
{
protected override void ExecuteCommand(IEventStore store, ICommand<UserId> cmd)
{
new UserApplicationService(store).Execute(cmd);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment