Skip to content

Instantly share code, notes, and snippets.

@andrewdavey
Created May 25, 2011 09:53
Show Gist options
  • Save andrewdavey/990690 to your computer and use it in GitHub Desktop.
Save andrewdavey/990690 to your computer and use it in GitHub Desktop.
Different approach to C# unit testing
// Sorry to stir things up, but I really don't get on with the current *Unit test frameworks
// in C#. I hate creating_long_semi_english method names. I hate then repeating myself
// in code e.g. public void It_sould_equal_10() { Assert.That(it == 10); }
// I think the problem stems from making a method the atom of a test.
// This leads to crap method names and hard to understand tests.
// What follows is a different approach. (Not real code yet, just ideas!)
// One Test class per class under test.
// Inherits framework `Tests` class to provide test-DSL methods.
class AccountTests : Tests {
// One test method per specification i.e. method under test.
// Multiple scenarios a defined within.
public void Creation() {
// Given -> Then (no When needed here for simple constructor tests).
// Given establishes the context/subject-under-test
// Then takes an Expression<Func<T, bool>>
// * it can create a decent human readable string.
// * it compiles and executes as an assertion.
Given(new Account()).Then(the => the.Balance == 0);
Given(new Account(100)).Then(the => the.Balance == 100);
// Then can also take a more general 'assertion' object
// seen here to check for exceptions.
Given(new Account(-1)).Then(Expect.Exception<NegativeBalanceException>());
// These test helper methods are building up a collection of scenarios to be run.
// They don't actually run when this method is called. A runner will collect
// all the scenarios and execute them later.
}
public void Deposit() {
// Can provide a string to describe the context better.
Given("An empty account", new Account())
.When(a => a.Deposit(100))
.Then(a => a.Balance == 100);
}
public void Withdraw() {
Given(new Account(100))
.When(a => a.Withdraw(50))
.Then(a => a.Balance == 50);
Given(new Account(100))
.When(a => a.Withdraw(200))
.Then(Expect.Exception<InsufficientFundsException>());
}
public void Reward() {
// A more complex 'context' can be established.
// For example, creating mocks and other helper objects.
Given(delegate {
var context = new {
account = new Account(),
bonusCalc = new Mock<IBonusCalc>()
};
context.bonusCalc.Setup(c => c.GetBonus()).Returns(42);
return context;
}).
When(c => c.account.Reward(c.bonusCalc.Object)).
Then(c => c.account.Balance == 42);
}
// ** Other ideas **
// Clean up using a .Finally(<action>) method call.
// Given(context).When(action).Then(assertion).Finally(clean_up);
// Parameterization is now freaking easy. Just call Given/When/Then in a loop!
// foreach (var row in data) {
// Given(context).When(c => c.Process(row.Input)).Then((result,c) => result == row.Output);
// }
// Nest contexts to reduce duplication
}
@kieransenior
Copy link

I'd definitely use this, it makes more sense, especially when you're using fluent APIs in the rest of your codebase. And you're right, tests are very atomic in that you test very individual parts making the test code far more verbose, but I guess the idea is increased specificity to ensure your tests are stringent.

@andrewdavey
Copy link
Author

@Kezzer - Thanks - glad to hear that it's not just me thinking this way :)

Check out (https://github.com/andrewdavey/FluentTest) for the code based on the ideas in this gist.

@wangvnn
Copy link

wangvnn commented Dec 18, 2016

There is a beautiful thing like this in .net world, but it is under-developed:
https://github.com/fschwiet/DreamNJasmine
My example: https://github.com/wangvnn/TheDCIBabyIDE/blob/master/TheDCIBabyIDE.Test/Example.cs
Note: I want nested when (Given A, When B, Then C, Given A, When B, When B' Then C' ...) without creating reused code...)
And my favourite: https://github.com/philsquared/Catch/blob/master/docs/tutorial.md

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