dapr / dotnet-sdk

Dapr SDK for .NET
Apache License 2.0
1.1k stars 327 forks source link

Unit testing the Actors #1230

Open mandjeo opened 5 months ago

mandjeo commented 5 months ago

I wrote an application that uses dapr Actors in dotNet core. I'm trying to unit test the actors and I currently see no way to do it. I used ActorHost.CreateForTest method to create the host and set the ID, however when my Actor gets the state it fails with the InvalidOperationException and the following message:

The actor was initialized without a state provider, and so cannot interact with state. If this is inside a unit test, replace Actor.StateProvider with a mock.

As far as I see StateProvider property is available on the ActorHost but it's internal and can't be set from the outside.

Also, in my actor I use the Reminders feature, and I would like to unit test that part as well.

Am I missing something and what would be the intended way to unit test the code that uses the actors? Also, if it's not possible to mock the necessary components in order to write a unit test, how would you suggest to setup the infrastructure so that I can do integration tests with dapr runtime and the actors while also being able to debug my tests?

Any help is appreciated!

edmondbaloku commented 4 months ago

Did you find any solution to this @mandjeo ?

mandjeo commented 4 months ago

@edmondbaloku

So, what we ended up doing is the following:

These enabled us to do basic unit testing of the functionality with in the actor. However, it gets very tricky when you are relying on actor reminders to be executed during the test or when you need to work with protected fields/methods.

Even though I really like the Actors concept from dapr, I think a bit more attention should be put on making it more testable.

Also, I would love to hear from someone from the dapr team on how are we supposed to run integration tests which include this functionality, to give a (even high level) idea on how should we setup our test infrastructure for that?

m3nax commented 1 month ago

Below is a brief example of how I managed to write some unit tests. It use XUnit and Moq. I hope it can help you.

/// <summary>
/// Persistent state for the storage actor.
/// </summary>
public class StorageState
{
    /// <summary>
    /// List of items in the storage.
    /// </summary>
    public Collection<string> Items { get; set; } = [];
}

public class StorageActor : Actor, IStorage
{
    /// <summary>
    /// The name of the state used to store the storage state.
    /// </summary>
    public const string StateName = "state";

    /// <summary>
    /// Initializes a new instance of <see cref="Storage"/>.
    /// </summary>
    /// <param name="host"></param>
    /// <param name="actorStateManager">Used in unit test.</param>
    public StorageActor (ActorHost host, IActorStateManager? actorStateManager = null)
        : base(host)
    {
        if (actorStateManager != null)
        {
            this.StateManager = actorStateManager;
        }
    }

    /// <inheritdoc/>
    public async Task Add(ICollection<string> items)
    {
        ArgumentNullException.ThrowIfNull(items, nameof(items));

        if (items.Count == 0)
        {
            throw new InvalidOperationException("Sequence contains no elements");
        }

        var state = await this.StateManager.GetStateAsync<StorageState>(StateName);

        // Add items to the storage.
        foreach (var item in items)
        {
            state.Items.Add(item);
        }

        await this.StateManager.SetStateAsync(StateName, state);
    }
}

public class StorageTests
{
    [Fact]
    public async Task Add_EmptyItemsCollection_ThrowInvalidOperationException()
    {
        // arrange
        var mockStateManager = new Mock<IActorStateManager>(MockBehavior.Strict);
        var host = ActorHost.CreateForTest<StorageActor>();

        var itemsToAdd = new List<string>();
        var storageActor = new StorageActor(host, mockStateManager.Object);

        // act
        var act = () => storageActor.Add(itemsToAdd);

        // assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(act);
        Assert.Equal("Sequence contains no elements", ex.Message);
    }

        [Fact]
    public async Task Add_AddItemsToStorage()
    {
        // arrange
        var mockStateManager = new Mock<IActorStateManager>(MockBehavior.Strict);
        var host = ActorHost.CreateForTest<StorageActor>();

        var itemsToAdd = new List<string> { "item1", "item2" };
        var storageActor = new StorageActor(host, mockStateManager.Object);
        var storageState = new StorageState
        {
            Items = new Collection<string>
            {
                "item0",
            }
        };

        mockStateManager
            .Setup(x => x.GetStateAsync<StorageState>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(storageState);

        mockStateManager
            .Setup(x => x.SetStateAsync(It.IsAny<string>(), It.IsAny<StorageState>(), It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);

        // act
        await storageActor.Add(itemsToAdd);

        // assert
        mockStateManager.Verify(x => x.SetStateAsync(
            Storage.StateName,
            It.Is<StorageState>(x => x.Items.Count == 3 && x.Items.Intersect(itemsToAdd).Count() == itemsToAdd.Count),
            It.IsAny<CancellationToken>()),
            Times.Once);
    }
}
m3nax commented 4 weeks ago

In the next few days I will write an example project

m3nax commented 4 weeks ago

/assign