Last active
November 20, 2017 21:11
-
-
Save arturaz/6adeb65b8efe6894179cf039e1ce537b to your computer and use it in GitHub Desktop.
Ultra short RSpec/NSpec/Specs style testing framework implementation on C#
This file contains hidden or 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
using System; | |
using System.Collections.Generic; | |
using System.Collections.Immutable; | |
using System.Linq; | |
using com.tinylabproductions.TLPLib.Data; | |
using com.tinylabproductions.TLPLib.Extensions; | |
using com.tinylabproductions.TLPLib.Functional; | |
using NUnit.Framework; | |
namespace com.tinylabproductions.TLPLib.Test { | |
public class Specification : TestBase { | |
public static void describe(Act<SpecificationBuilder> buildTests) { | |
var builder = new SpecificationBuilder(); | |
buildTests(builder); | |
builder.execute(); | |
} | |
} | |
public class ImplicitSpecification : Specification { | |
SpecificationBuilder _currentBuilder; | |
SpecificationBuilder currentBuilder { | |
get { | |
if (_currentBuilder == null) throw new IllegalStateException( | |
"Implicit specification builder is not set! " + | |
"You should be in a describe() block before calling this method." | |
); | |
return _currentBuilder; | |
} | |
set { _currentBuilder = value; } | |
} | |
protected SpecificationBuilder.When when => currentBuilder.when; | |
protected SpecificationBuilder.It it => currentBuilder.it; | |
protected event Action beforeEach { | |
add { currentBuilder.beforeEach += value; } | |
remove { currentBuilder.beforeEach -= value; } | |
} | |
protected event Action afterEach { | |
add { currentBuilder.afterEach += value; } | |
remove { currentBuilder.afterEach -= value; } | |
} | |
protected SimpleRef<A> let<A>(A initialValue) => currentBuilder.let(initialValue); | |
protected SimpleRef<A> let<A>(Fn<A> initialValue) => currentBuilder.let(initialValue); | |
protected void describe(Action buildTests) { | |
describe(builder => { | |
currentBuilder = builder; | |
buildTests(); | |
currentBuilder = null; | |
}); | |
} | |
} | |
public class SpecTestFailedException : Exception { | |
public SpecTestFailedException( | |
ImmutableList<SpecificationBuilder.Test<Exception>> failures | |
) : base( | |
"Following tests failed:\n\n" + failures.Select(f => { | |
var err = F.opt(f.a as AssertionException).fold(f.a.ToString, e => e.Message); | |
return $"### {f.name} ###\n{err}"; | |
}).mkString("\n") | |
) { } | |
} | |
public sealed class SpecificationBuilder { | |
public struct Test<A> { | |
public readonly string name; | |
public readonly A a; | |
public Test(string name, A a) { | |
this.name = name; | |
this.a = a; | |
} | |
public override string ToString() => $"{nameof(Test)}: {name} ({a})"; | |
} | |
class Context { | |
public static readonly Context root = new Context( | |
"", ImmutableList<Action>.Empty, ImmutableList<Action>.Empty | |
); | |
public readonly string name; | |
public readonly ImmutableList<Action> beforeEach, afterEach; | |
public Context(string name, ImmutableList<Action> beforeEach, ImmutableList<Action> afterEach) { | |
this.name = name; | |
this.beforeEach = beforeEach; | |
this.afterEach = afterEach; | |
} | |
public bool isRoot => name == root.name; | |
public Context addBeforeEach(Action action) => | |
new Context(name, beforeEach.Add(action), afterEach); | |
public Context addAfterEach(Action action) => | |
new Context(name, beforeEach, afterEach.Add(action)); | |
public Context child(string childName) => | |
new Context(concatName(name, childName, " and "), beforeEach, afterEach); | |
public Test<Action> test(string testName, Action testAction) => | |
new Test<Action>( | |
concatName(name, testName), | |
() => { | |
foreach (var a in beforeEach) a(); | |
testAction(); | |
foreach (var a in afterEach) a(); | |
} | |
); | |
static string concatName(string context, string name, string joiner = " ") => | |
context.nonEmptyOpt(true).fold(name, s => $"{s}{joiner}{name}"); | |
} | |
public class When { | |
readonly SpecificationBuilder self; | |
public When(SpecificationBuilder self) { this.self = self; } | |
public Action this[string name] { | |
set { | |
var prevContext = self.currentContext; | |
self.currentContext = self.currentContext.child( | |
self.currentContext.isRoot ? $"when {name}" : name | |
); | |
value(); | |
self.currentContext = prevContext; | |
} | |
} | |
} | |
public class It { | |
readonly SpecificationBuilder self; | |
public It(SpecificationBuilder self) { this.self = self; } | |
public Action this[string name] { | |
set { | |
self.tests.Add(self.currentContext.test($"it {name}", value)); | |
} | |
} | |
} | |
readonly List<Test<Action>> tests = new List<Test<Action>>(); | |
public readonly When when; | |
public readonly It it; | |
public SpecificationBuilder() { | |
when = new When(this); | |
it = new It(this); | |
} | |
Context currentContext = Context.root; | |
public SimpleRef<A> let<A>(A initialValue) => let(() => initialValue); | |
public SimpleRef<A> let<A>(Fn<A> createInitialValue) { | |
var r = new SimpleRef<A>(createInitialValue()); | |
Action reinit = () => r.value = createInitialValue(); | |
currentContext = currentContext.addBeforeEach(reinit); | |
return r; | |
} | |
public event Action beforeEach { | |
add { currentContext = currentContext.addBeforeEach(value); } | |
remove { throw new NotImplementedException(); } | |
} | |
public event Action afterEach { | |
add { currentContext = currentContext.addAfterEach(value); } | |
remove { throw new NotImplementedException(); } | |
} | |
public void execute() { | |
var failures = tests.SelectMany(test => { | |
try { | |
test.a(); | |
return Enumerable.Empty<Test<Exception>>(); | |
} | |
catch (Exception e) { | |
return new Test<Exception>(test.name, e).Yield(); | |
} | |
}).ToImmutableList(); | |
if (failures.nonEmpty()) throw new SpecTestFailedException(failures); | |
} | |
} |
This file contains hidden or 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
public interface Val<out A> { | |
A value { get; } | |
} | |
public interface Ref<A> : Val<A> { | |
new A value { get; set; } | |
} | |
/* Simple heap-allocated reference. */ | |
public class SimpleRef<A> : Ref<A> { | |
public A value { get; set; } | |
public SimpleRef(A value) { | |
this.value = value; | |
} | |
public static implicit operator A(SimpleRef<A> r) => r.value; | |
} |
This file contains hidden or 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
class TestSpec : Specification { | |
public void foo() => describe(_ => { | |
_.when["foo"] = () => { | |
var x = _.let(3); | |
var y = _.let(() => x + 3); | |
_.beforeEach += () => { }; | |
_.when["bar"] = () => { | |
_.it["should do stuff"] = () => {}; | |
}; | |
}; | |
}); | |
} | |
class TestSpec2 : ImplicitSpecification { | |
public void foo() => describe(() => { | |
when["foo"] = () => { | |
var x = let(3); | |
var y = let(() => x + 3); | |
beforeEach += () => { }; | |
when["bar"] = () => { | |
it["should do stuff"] = () => {}; | |
}; | |
}; | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice :)