dapr / dotnet-sdk

Dapr SDK for .NET

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unit testing the Actors

mandjeo opened this issue · comments

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!

Did you find any solution to this @mandjeo ?

@edmondbaloku

So, what we ended up doing is the following:

  • create a mock of the ActorTimerManger which we pass to the ActorTestOptions in order to be able to test things related to reminders
  • use the ActorHost.CreateForTest method to create the host pass ActorTestOptions with timer manager mock as well as the ActorId
  • introduce an internal constructor which receives IActorStateManager and then sets the protected StateManager property
  • in the tests we use this internal constructor to pass the stub implementation of the state manager (we implemented the stub with an in memory state management)

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?

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);
	}
}

In the next few days I will write an example project