How-to guide

Test a train with in-memory effects

Source ServiceExtensions.cs

A train test builds a container with the same Trax registration as Program.cs, swaps the data provider for UseInMemory() and the application's outside dependencies for fakes, and runs the train through ITrainBus the way the application does. The junctions run for real. For a test of one junction on its own, see Unit-test a junction.

Build the container

Register logging, the fakes, and Trax with the in-memory provider (package Trax.Effect.Data.InMemory). AddMediator takes the assembly that holds the trains, as in Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Trax.Effect.Data.InMemory.Extensions;
using Trax.Effect.Extensions;
using Trax.Mediator.Extensions;
using Trax.Mediator.Services.TrainBus;
 
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IUserRepository, FakeUserRepository>();
services.AddTrax(trax =>
    trax.AddEffects(effects => effects.UseInMemory())
        .AddMediator(typeof(CreateUserTrain).Assembly)
);
 
await using var provider = services.BuildServiceProvider();
var bus = provider.GetRequiredService<ITrainBus>();
 
var user = await bus.RunAsync<User>(new CreateUserRequest { Email = "test@example.com" });

In an NUnit fixture, build the container in [SetUp] and dispose it in [TearDown], so each test starts on an empty store: every UseInMemory() call creates its own, which every scope of that container shares.

Assert the output or the exception

RunAsync<TOut> returns the train's output. When a junction throws, the train stops and RunAsync throws that exception, so a failing run is asserted with Assert.ThrowsAsync:

Assert.ThrowsAsync<ValidationException>(
    () => bus.RunAsync<User>(new CreateUserRequest { Email = "taken@example.com" })
);

Read the run's record

Every run writes a Metadata row, which a test reads through the scoped IDataContext. Metadata.Name is the full name of the train's interface. With SaveTrainParameters(), Metadata.Input holds the input as JSON, so a unique value in the input finds the run among any others in the same container. The trax-hub template's train test does both:

From Trax.Samples/templates/content/Trax.Samples.Hub/tests/Trax.Samples.Hub.Tests/IntegrationTests/HelloWorldTrainTests.cs

    [SetUp]
    public void BuildTheContainer()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddTrax(trax =>
            trax.AddEffects(effects => effects.UseInMemory().SaveTrainParameters())
                .AddMediator(typeof(HelloWorldTrain).Assembly)
        );
        _provider = services.BuildServiceProvider();
    }
 
    [TearDown]
    public ValueTask DisposeTheContainer() => _provider.DisposeAsync();
 
    [Test]
    public async Task RunAsync_WithAName_CompletesAndIsRecorded()
    {
        // A name no other run uses, to find this run's record among any others.
        var name = Guid.NewGuid().ToString();
        var bus = _provider.GetRequiredService<ITrainBus>();
 
        await bus.RunAsync<Unit>(new HelloWorldInput { Name = name });
 
        using var scope = _provider.CreateScope();
        var data = scope.ServiceProvider.GetRequiredService<IDataContext>();
        var run = data
            .Metadatas.Where(m => m.Name == typeof(IHelloWorldTrain).FullName)
            .AsEnumerable()
            .SingleOrDefault(m => m.Input?.Contains(name) == true);
 
        Assert.That(run, Is.Not.Null, "every run of a train is recorded, with its input");
        Assert.That(run!.TrainState, Is.EqualTo(TrainState.Completed));
    }

A failed run's row has TrainState.Failed, and its FailureJunction, FailureException and FailureReason say which junction threw and why. The columns are in Metadata.

Pick the data provider

The in-memory provider runs no SQL. When the code under test does, run the test on a provider that does:

The code under test usesProvider
Model logic only, no queriesUseInMemory()
LINQ queries EF translates (Where, OrderBy, Select)UseInMemory() or UseSqlite(connectionString)
Raw SQL (ExecuteSqlRawAsync, Database.GetDbConnection())UseSqlite(connectionString) or UsePostgres(connectionString)
Postgres features (FOR UPDATE SKIP LOCKED, advisory locks, pg_class)UsePostgres(connectionString)

A Postgres test needs a database of its own; Test a host end to end sets one up.

SDK Reference

AddTrax / AddEffects | UseInMemory | UseSqlite | AddMediator | RunAsync | SaveTrainParameters