WORK IN PROGRESS
FIT is a .NET integration test framework where a case (i.e. as in use cases) is defined by a sequence of acts that acts on the system being tested when a case is run.
Unlike unit tests the acts in a case are not independent: An act start acting on the system in a state caused by the acts that acted before it in the case. Acts are defined by the interface IActor
.
An implementation of the interface can be used to perform many acts both in a case and in more than one case.
Implementations of the IACtor
interface not only act on the system being testet but make claims about (by writing to an instance of ISystemClaims
) the system state after the act.
After an act have acted the claims are checked to be true by implementations of the IAssertor
interface (by reading claims from an instance of ISystemClaims
and comparing claim values with the state read from the system itself).
Separating asserting from acting means also side effects can be caught not only that an act did work as expected.
Both IActor
and IAssertor
gets the part of the system they need to access through dependency injection.
In next sections are code examples from demo projects found here. Note that Fit is intended to be used from a unit test project, currently it is recommended to use XUnit framework since Fit provide support for that through the Fit.Xunit project but looking at the sample code here and Fit.Xunit code using another framework similar to XUnit shold be easy. An issue #3 has been made to provide NUnit support in the future.
Cases can be defined by a class that implements the interface ICaseDefiner
:
using Fit.Abstraction.Api;
using Fit.Abstraction.Client;
using Fit.Demo.Test.Actor;
namespace Fit.Demo.Test;
public class ToDoAndTagCase : ICaseDefiner
{
public void AddCases(IFit fit)
{
fit.First<AddToDo>().With("Name", "TestToDoItem1").And("State", "Next")
.Then<AddToDo>().With("Name", "TestToDoItem2").And("State", "SAP")
.Then<AddTag>().With("Name", "Tag1")
.Then<AddTag>().With("Name", "Tag2")
.Then("RemoveTag").With("Name", "Tag1")
.AsCase("ToDoAndTag");
}
}
As seen in the example a fluent syntax is used to define a case.
Even not knowing anything about the system being testet one learn quite a lot about it by reading this case code: It is a system where it is possible to add todo items and tags.
The parameter fit
of type IFit
is the API to define (what we do in this example) cases and run cases (later examples).
First
and Then
methods tell that a named IActor
is to act on the system with some parameters provided with the With
and And
methods.
Note that the methods that tell an IActor
implementation to act comes in two forms:
- Identifying the actor with a generic type argument as in
First<AddToDo>
andThen<AddTag>
. Using these methods theIActor
implementation must exist. - Identifying the actor pasing the class name as a parameter as in
First("AddToDo")
andThen("AddTag")
. Using these methods theIActor
implementation may not yet exist if run in a IgnoreMissingActors mode (shown in examples below). This is usefull in a test driven implementation strategy. Tip: If using this strategy consider to first use this form and then when actors get implemented switch to the generic parameter form to make it easy to read from code what has been implemented and what is planned.
using Fit.Abstraction;
using Fit.Demo.Business;
using Fit.Demo.Domain;
namespace Fit.Demo.Test.Actor;
public class AddToDo : IActor
{
private readonly ToDosViewController _toDoViewController;
public AddToDo(ToDosViewController toDoViewController) => _toDoViewController = toDoViewController;
public async Task ActAsync(ActorContext context)
{
var name = context.Parameters?.Get<string>("Name");
await _toDoViewController.LoadAsync();
_toDoViewController.NewToDo.Name = name;
var created = await _toDoViewController.CreateToDo();
context.StateClaims.ExpectedItemList<ToDo>().Add(created with { });
}
}
Fit does not implement any test runners but utilize unit test framework runners so to get IDE support and be used in a automatic testing scenario. The basic idea is that each case is run as one test. The following examples show use of the XUnit framework.
using Fit.Abstraction.Api;
using Fit.Demo.Business;
using Fit.Demo.Infrastructure.InMemory;
namespace Fit.Demo.Test;
public class DemoTest_Method1
{
private readonly IFit _fit = FluentIntegrationTest.Create(o =>
{
o.RunMode.Proto = false;
o.RunMode.IgnoreMissingActors = true;
o.Services.AddFitDemoInMemoryInfrastructure()
.AddFitDemoBusiness();
});
[Theory]
[InlineData("ToDoAndTag")]
public async Task Test(string caseName)
{
await _fit.RunCase(caseName);
}
}
In this example an instance of the Fit api is created using the static method FluentIntegrationTest.Create
that accept a function that receives an option object to set:
Boolean
propertyRunMode.Proto
that if true will not actual perform any acts. Default value is false.Boolean
propertyRunMode.IgnoreMissingActors
that if is true will not throw an exception if an actor is named but implementation is not found.IServiceCollection
propertyServices
to register the services that makes up the system being testet.
A single test method is then defined to run given cases, the idea is to use XUnit's Theory
and InlineData
attributes to list all the cases to run.
Pro
- In VS Studio each case get a node in the test explorer view's tree and can be run separetely.
- Each case will have an inline attribute and this will provide a nice list in code of all the cases. However maintining this stack of inlinedata attributes can be challenging, see the con list.
Con
- Maintaining
using Fit.Abstraction.Api;
using Fit.XUnit;
using Xunit.Abstractions;
namespace Fit.Demo.Test;
public class DemoTest_Method2 : FitXunitTestBase
{
public DemoTest_Method2(ITestOutputHelper testReporter) : base(testReporter) { }
[Theory]
[ClassData(typeof(TestSource))]
public async Task RunCases(IFit fit, string name) => await RunNamedCase(fit, name);
}
this depends on that we have implemented the source class feeding the cases:
using Fit.Demo.Business;
using Fit.Demo.Infrastructure.InMemory;
using Fit.XUnit;
namespace Fit.Demo.Test;
public class TestSource : FitXunitTestSource
{
public TestSource() : base(o =>
{
o.RunMode.Proto = true;
o.RunMode.IgnoreMissingActors = true;
o.Services.AddFitDemoInMemoryInfrastructure()
.AddFitDemoBusiness();
}) { }
}