Skip to content

Instantly share code, notes, and snippets.

@arturaz
Last active November 20, 2017 21:11
Show Gist options
  • Save arturaz/6adeb65b8efe6894179cf039e1ce537b to your computer and use it in GitHub Desktop.
Save arturaz/6adeb65b8efe6894179cf039e1ce537b to your computer and use it in GitHub Desktop.
Ultra short RSpec/NSpec/Specs style testing framework implementation on C#
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);
}
}
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;
}
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"] = () => {};
};
};
});
}
}
@DovydasNavickas
Copy link

Nice :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment