Testing with Local Functions 🐯
in .NET / C# ⚙️
with your favorite Unit Testing Framework ✔️
Use the traditional Unit Testing Frameworks for BDD.
Use local functions to structure tests with patterns like:
Arrange
/Act
/Assert
Given
/When
/Then
Context
/Specification
Use Auto-Mocking Containers to Mock
/ Fake
/ Stub
dependencies.
LoFuUnit consists of a few packages that make it convenient for developers to write tests with collaboration & communication in mind.
Why LoFu?
LoFu
stands for Local Functions.
Why the tiger logo?
老虎 translates to tiger in English and is pronounced lou5 fu2
in Cantonese.
What are local functions?
Starting with C# 7.0, C# supports local functions.
Local functions are private methods of a type that are nested in another member.
Local functions can use the
async
modifier.
An example of a test with LoFuUnit.NUnit:
using FluentAssertions;
using LoFuUnit.NUnit;
using NUnit.Framework;
namespace LoFuUnit.Tests.Documentation
{
public class AuthenticationTests
{
SecurityService Subject;
UserToken Token;
[LoFu, Test]
public void Authenticate_admin_users()
{
Subject = new SecurityService();
void when_authenticating_an_admin_user() =>
Token = Subject.Authenticate("username", "password");
void should_indicate_the_user_s_role() =>
Token.Role.Should().Be(Roles.Admin);
void should_have_a_unique_session_id() =>
Token.SessionId.Should().NotBeNull();
}
}
}
Output:
Authenticate admin users
when authenticating an admin user
should indicate the user's role
should have a unique session id
Terminology:
- Test fixture – a class that contains tests
- e.g.
AuthenticationTests
- e.g.
- Test method – a method in a test fixture that represents a test
- e.g.
Authenticate_admin_users
- e.g.
- Test function – a local function in a containing test method, that does something along the lines of arrange, act or assert
- e.g.
when_authenticating_an_admin_user
,should_indicate_the_user_s_role
andshould_have_a_unique_session_id
- e.g.
README | Test Framework | NuGet | Sample |
---|---|---|---|
LoFuUnit | - | LoFuUnit.Sample |
|
LoFuUnit.MSTest | MSTest | LoFuUnit.Sample.MSTest |
|
LoFuUnit.NUnit | NUnit | LoFuUnit.Sample.NUnit |
|
LoFuUnit.Xunit | Xunit | LoFuUnit.Sample.Xunit |
Test fixtures can inherit the class LoFuTest
.
Test methods can contain local functions that are invoked implicitly. These test functions can perform the arrange, act or assert steps of the test.
The LoFuTest
base class provides two important methods for test fixtures.
The Assert
and AssertAsync
methods invokes the test functions in the containing test method.
The invocations will occur in the order that the test functions are declared.
If a test function fails, the test method fails directly. Any subsequent test functions in the test method will not be invoked.
Make sure that all test methods actually invoke Assert
or AssertAsync
.
Test fixtures that does not inherit the LoFuTest
base class can invoke the extension methods:
this.Assert();
await this.AssertAsync();
These methods has a [CallerMemberName]
parameter.
The caller of these methods will implicitly be used, so don't set this parameter explicitly.
Test fixtures can also be implemented so Assert
or AssertAsync
is invoked for all test methods in a tear down / cleanup / dispose method.
The LoFuUnit.NUnit
package also contains the [LoFuTest]
and [LoFu]
attributes to mark test methods with.
This will automatically invoke Assert
or AssertAsync
when the test method runs.
Examples of all these patterns can be found in the samples folder.
Succinct test functions can be implemented as one-liners by refactoring to expression body and omitting the curly brackets.
The naming of the test methods and test functions are important, because the names are the basis for the test output.
Remember:
- Do not explicitly invoke the test functions in a test method
- Only invoke the
Assert
orAssertAsync
method once per test method
The naming of the test methods and test functions determines the test output.
The naming convention is to use snake_case
for test methods and test functions.
An underscore will be replaced by space in the test output:
foo_bar
will be formatted asfoo bar
Surrounding a word with double underscores will put quotes around that word in the test output:
__Foo__
will be formatted as"Foo"
Suffixing a word with _s_
will add possessive form to the word in the test output:
Foo_s_
will be formatted asFoo's
Take the opportunity to invent your own convention for naming test methods and test functions.
Consider using keywords like:
given
when
then
should
What characters can be used when naming test methods and test functions? Stack Overflow has the answer!
You can use the Log
method from the LoFuTest
base class to write custom messages to the test output.
The naming of assemblies
, namespaces
and classes
can also make the test suite more readable in your test runner.
Rule #1: Test functions must return void
or Task
The Assert
method can only invoke local functions that:
- are synchronous and returns
void
The AssertAsync
method can only invoke local functions that:
- are synchronous and returns
void
, or - are asynchronous and returns
Task
Rule #2: Test functions must be parameterless
The Assert
and AssertAsync
methods can only invoke local functions that:
- has no parameters
Rule #3: Test functions must not use variables declared at test method scope, i.e. local variables in the containing method (including its parameters)
The solution for this is to use test fixture members, e.g. fields, properties, methods etc.
When a test function needs access to data from the test method or another test function:
- use a variable declared at test fixture scope, i.e. a field
It is easy to break rule #3 by mistake, because:
Note that all local variables that are defined in the containing member, including its method parameters, are accessible in the local function.
Test functions that break these abovementioned rules can not be invoked when the test method is running.
The Assert
and AssertAsync
methods will first validate the local functions in the test method scope, before they are invoked.
If any violations of the rules are found, an InconclusiveLoFuTestException
is thrown.
The exception includes a message with the local functions that should be reviewed.
With the [LoFu]
and [LoFuTest]
attributes in LoFuUnit.NUnit, the test runner clearly shows inconclusive tests:
A list of Best Practices with patterns to consider and avoid.
An example of a test with LoFuUnit.AutoNSubstitute and LoFuUnit.NUnit:
using System;
using FluentAssertions;
using LoFuUnit.AutoNSubstitute;
using LoFuUnit.NUnit;
using NSubstitute;
using NUnit.Framework;
namespace LoFuUnit.Tests.Documentation
{
public class MoodTests : LoFuTest<MoodIdentifier>
{
string _mood;
[LoFu, Test]
public void Identify_mood_on_mondays()
{
void given_the_current_day_is_monday()
{
var monday = new DateTime(2011, 2, 14);
Use<ISystemClock>()
.CurrentTime
.Returns(monday);
}
void when_identifying_my_mood() =>
_mood = Subject.IdentifyMood();
void should_be_pretty_bad() =>
_mood.Should().Be("Pretty bad");
}
}
}
Output:
Identify mood on mondays
given the current day is monday
when identifying my mood
should be pretty bad
README | Mock Framework | NuGet | Sample |
---|---|---|---|
LoFuUnit.AutoFakeItEasy | FakeItEasy | LoFuUnit.Sample.AutoFakeItEasy |
|
LoFuUnit.AutoMoq | Moq | LoFuUnit.Sample.AutoMoq |
|
LoFuUnit.AutoNSubstitute | NSubstitute | LoFuUnit.Sample.AutoNSubstitute |
LoFuUnit
uses AutoFixture
as Auto-Mocking Container.
Test fixtures that inherit the LoFuTest<TSubject>
base class can use mocks.
The generic type parameter defines what kind of subject under test to create.
The Use<TDependency>
method creates a mock / dependency that the subject is dependent upon.
Use the API from the mock framework to configure the behavior of the mock.
The The<TDependency>
method returns a previously created mock.
Use the API from the mock framework to verify the interaction with the mock.
The Subject
property returns an auto-mocked instance of the subject under test.
Use the subject for the act
or when
test steps.
The Clear
method reset the Auto-Mocking Container, and clears the mocks and subject under test.
Make test methods isolated from each other, by clearing the state between runs.
Test fixtures can also be implemented so Clear
is invoked for all test methods in a tear down / cleanup / dispose method.
The Fixture
property is exposed to the test fixtures. Use it in scenarios where the methods described above are inadequate.
Consult the AutoFixture documentation for more information.
Examples of usage can be found in the samples folder.
Before you can access a mock / dependency via the The<TDependency>
method,
you must first call one of the Use<TDependency>
methods for that specific type.
The The<TDependency>
method will return null
for unknown mocks / dependencies.
The test result output can be used as documentation.
With the dotnet test
command, the output can be captured via:
dotnet test --logger:"console;verbosity=detailed"
dotnet test --logger:trx
If you see something like this:
LoFuUnit.InconclusiveLoFuTestException : Invocation of test method 'when_Assert_on_inconclusive_test_method' aborted. One or more test functions are inconclusive. Test functions must be parameterless, and cannot use variables declared at test method scope. Please review the following local functions:
should_not_invoke_test_function_that_access_test_method_variables
at LoFuUnit.LoFuTest.ThrowInconclusive(MethodBase method, IEnumerable`1 names) in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 146
at LoFuUnit.LoFuTest.Validate(MethodBase method) in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 118
at LoFuUnit.LoFuTest.Assert(Object testFixture, MethodBase testMethod) in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 41
at LoFuUnit.LoFuTest.Assert() in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 24
at LoFuUnit.Tests.Integration.LoFuTestTests.when_Assert_on_inconclusive_test_method() in C:\work\github\LoFuUnit\tests\LoFuUnit.Tests\Integration\LoFuTestTests.cs:line 89
Then you broke Rule #2 or #3, described above. Make sure that the local functions do not have any parameters. And furthermore, that the local functions do not access any variables declared in the containing method. Rewrite the test so that data is passed to the local functions via fields declared in the containing class.
If you see something like this:
LoFuUnit.InconclusiveLoFuTestException : Invocation of test function 'should_not_invoke_async_test_function_that_returns_void' failed. The asynchronous local function does not have a valid return type. Asynchronous test functions must return a Task, and cannot return void or Task<TResult>.
at LoFuUnit.LoFuTest.AssertAsync(Object testFixture, MethodBase testMethod) in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 84
at LoFuUnit.LoFuTest.AssertAsync() in C:\work\github\LoFuUnit\src\LoFuUnit\LoFuTest.cs:line 36
at LoFuUnit.Tests.Integration.LoFuTestTests.when_AssertAsync_on_invalid_test_function() in C:\work\github\LoFuUnit\tests\LoFuUnit.Tests\Integration\LoFuTestTests.cs:line 129
at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult()
at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context)
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context)
at NUnit.Framework.Internal.Commands.BeforeAndAfterTestCommand.<>c__DisplayClass1_0.<Execute>b__0()
at NUnit.Framework.Internal.Commands.BeforeAndAfterTestCommand.RunTestMethodInThreadAbortSafeZone(TestExecutionContext context, Action action)
Then you broke Rule #1, described above.
Make sure that the async
local functions does not return void
.
Rewrite the test so that the asynchronous local functions return a Task
.
LoFuUnit is standing on the shoulders of giants.
It is inspired by https://github.com/machine/machine.specifications and https://github.com/machine/machine.specifications.fakes
It builds upon:
- https://github.com/nunit/nunit
- https://github.com/xunit/xunit
- https://github.com/Microsoft/testfx
- https://github.com/AutoFixture/AutoFixture
- https://github.com/FakeItEasy/FakeItEasy
- https://github.com/moq/moq4
- https://github.com/nsubstitute/NSubstitute