Created
September 14, 2012 14:01
-
-
Save abdullin/3722071 to your computer and use it in GitHub Desktop.
Rough cuts of improved Simple Testing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | |
-------------------------------------------------------------------- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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