Testing with bUnit and Moq a razor component that uses Fluxor

1.9k views Asked by At

I used Fluxor for state management in in one of the razor components in my Blazor app and I'm not really sure how to set it up for testing with Moq. I can't just assing a value to students because it's an init only property. Is there a specific way to set up Fluxor for testing?

code

var teacherService = new Mock<ITeacherService>();
            var  localStorage = new Mock<ILocalStorageService>();
            var  studentService = new Mock<IStudentService>();
            var  toastService = new Mock<IToastService>();
            var  NavigationManager = new Mock<NavigationManager>();
            var  Dispatcher = new Mock<IDispatcher>();
            var actionSubscriber = new Mock<IActionSubscriber>();

            var StudentsState = new Mock<IState<StudentsState>>();
            var TeacherState = new Mock<IState<TeacherState>>();

            // Help here
            StudentsState.Setup(t => t.Value.Students = )
2

There are 2 answers

3
Monky Monk On

I was stuck on this for quite a while, but went looking through Fluxor's glitter community and found something that might help! I understand that you'd like to test using Moq, but perhaps what I found might be easier/suit your purposes for testing Fluxor components!

Instead of using Moq, I followed this format and managed to get something working:

Test.cs

public class InhousePetOwnerStateTests
{
    private readonly IServiceProvider ServiceProvider;
    private readonly IStore Store;
    private readonly IState<InhousePetOwnerListState> State;
    public InhousePetOwnerStateTests()
    {
        var services = new ServiceCollection();
        services.AddFluxor(x =>
        x
            .ScanAssemblies(GetType().Assembly)
            .ScanTypes(typeof(InhousePetOwnerListState), typeof(Reducers)));
    
        ServiceProvider = services.BuildServiceProvider();
        Store = ServiceProvider.GetRequiredService<IStore>();
        State = ServiceProvider.GetRequiredService<IState<InhousePetOwnerListState>>();
        Store.InitializeAsync().Wait();
    }

    [Fact]
    public void SetInhousePetOwnerList_Empty_StateUpdated()
    {
        Store.Dispatch(new SetInhousePetOwnerListAction(new()));
        Assert.Empty(State.Value.InhousePetOwners);
    }
        
    [Fact]
    public void SetInhousePetOwnerList_MultipleInhouseOwners_StateUpdated()
    {
        List<InhousePetOwner> inhousePetOwners = new List<InhousePetOwner>
        {
            new InhousePetOwner()
            {
                InhousePetOwnerId = 1,
                PetOwnerId = 1,
                TimeIn = System.DateTime.Now
            },
            new InhousePetOwner()
            {
                InhousePetOwnerId = 2,
                PetOwnerId = 2,
                TimeIn = System.DateTime.Now
            },
        };
        Store.Dispatch(new SetInhousePetOwnerListAction(inhousePetOwners));
        Assert.Equal(2, State.Value.InhousePetOwners.Count);
    }
}

In the constructor, we create a service collection and add Fluxor (just like you would to your Startup.cs), then scan for assemblies.

In my code above, I scanned for the Reducers and the State. Please refer to my directory structure here (you'll get an idea of how I decided what to include).

Actions.cs

public class SetInhousePetOwnerListAction
{
    public List<InhousePetOwner> InhousePetOwners { get; }
    public SetInhousePetOwnerListAction(List<InhousePetOwner> inhousePetOwners)
    {
        InhousePetOwners = inhousePetOwners;
    }
}

Reducers.cs (scanned in the constructor)

public static class Reducers
{
    [ReducerMethod]
    public static InhousePetOwnerListState ReduceSetInhousePetOwnerListAction(InhousePetOwnerListState state, SetInhousePetOwnerListAction action)
    {
        return new InhousePetOwnerListState(inhousePetOwners: action.InhousePetOwners);
    }
}

InhousePetOwnerState.cs (scanned in the constructor)

public class InhousePetOwnerListState
{
    public List<InhousePetOwner> InhousePetOwners { get; }

    private InhousePetOwnerListState() { }

    public InhousePetOwnerListState(List<InhousePetOwner> inhousePetOwners)
    {
        InhousePetOwners = inhousePetOwners;
    }
}

What this solution does is it allows you to inject all your Fluxor states, actions and reducers, enabling you to call them as you normally would - I've found it pretty easy to test this way!

2
Kaine On

The way I did this was to use the set up to return a whole state record, configured the way that I wanted it.

So if you have a IState where StudentsState is readonly

You can do:

        var studentState = new Mock<IState<StudentsState>>();
        studentState.Setup(s => s.Value)
            .Returns(
                new StudentsState
                {
                    Students = new List<StudentRecord>
                    {
                        new StudentRecord(),
                        new StudentRecord(),
                    }
                });

        var TestContext = new Bunit.TestContext();
        TestContext.Services.AddScoped(_ => studentState.Object);

        var cut = TestContext.RenderComponent<StudentListComponent>();

        var studentRows = cut.FindAll(".student-row");

        Assert.AreEqual(2, studentRows.Count, "Should have one row for each student in the state");

If you need more complext interaction with the state you need to actually set up Fluxor to run on the TestContext something like:

    var TestContext = new Bunit.TestContext();
    TestContext.Services.AddFluxor(options =>
    {
        options.ScanAssemblies(Assembly.GetExecutingAssembly(), typeof(StudentsState).Assembly);
    });

    var store = TestContext.Services.GetService<IStore>();
    await store.InitializeAsync();

    IDispatcher dispatcher = TestContext.Services.GetService<IDispatcher>();
    dispatcher.Dispatch(new SetUpStudentsStateAction());

(Note that if you set up the Fluxor like this then you'll need to mock up your data source so that fluxor can load up the data in the Effect)