Unit Testing Dapr Actors

Learn how to unit test Dapr actors in .NET

Dapr actors encapsulate business logic, state transitions, and inter-actor communication. Unit testing catches bugs early before deploying to a Dapr-enabled cluster.

This guide covers practical patterns for testing actors without a running Dapr sidecar, using standard .NET test frameworks.

Prerequisites

Testable API surface

An actor class inherits from Actor (namespace Dapr.Actors.Runtime). Key members available for testing:

MemberRole
StateManagerTyped key-value state store (IActorStateManager)
IdThe actor unique identifier (ActorId)
HostThe ActorHost with registered type info
RegisterTimerAsync()Register a periodic callback
RegisterReminderAsync()Register a persistent reminder

Unit testing with ActorHost

The simplest unit test creates an actor with a lightweight ActorHost:

using Dapr.Actors.Runtime;
using Xunit;

public class BankActorTests
{
    private readonly ActorHost _host = new(ActorTypeName: "BankActor");

    [Fact]
    public async Task NewActor_HasZeroBalance()
    {
        var actor = new BankActor(_host);
        Assert.Equal(0m, await actor.GetBalanceAsync());
    }

    [Fact]
    public async Task Deposit_IncreasesBalance()
    {
        var actor = new BankActor(_host);
        await actor.DepositAsync(100m);
        Assert.Equal(100m, await actor.GetBalanceAsync());
    }

    [Fact]
    public async Task Withdraw_DecreasesBalance()
    {
        var actor = new BankActor(_host);
        await actor.DepositAsync(100m);
        await actor.WithdrawAsync(30m);
        Assert.Equal(70m, await actor.GetBalanceAsync());
    }

    [Fact]
    public async Task Withdraw_ThrowsWhenInsufficientFunds()
    {
        var actor = new BankActor(_host);
        await Assert.ThrowsAsync<InvalidOperationException>(
            () => actor.WithdrawAsync(10m));
    }
}

Tip: Each test creates a fresh actor instance, so state does not leak between tests.

Mocking StateManager

When your actor’s constructor accepts IActorStateManager via dependency injection, use a mock framework:

using Moq;

[Fact]
public async Task Deposit_SavesState()
{
    var mockState = new Mock<IActorStateManager>();
    var host = new ActorHost(ActorTypeName: "Test");
    var actor = new BankActor(host, mockState.Object);

    await actor.DepositAsync(100m);

    mockState.Verify(
        s => s.SetStateAsync("balance", 100m, It.IsAny<CancellationToken>()),
        Times.Once);
}

If your actor uses the base class StateManager property directly, test state persistence via TrySaveStateAsync() or verify behavior through public methods.

Mocking cross-actor calls

Actors call other actors through IActorProxyFactory. Inject the factory via constructor for testability:

public class OrderActor : Actor, IOrderActor
{
    private readonly IActorProxyFactory _proxyFactory;

    public OrderActor(ActorHost host, IActorProxyFactory proxyFactory)
        : base(host)
    {
        _proxyFactory = proxyFactory;
    }

    public async Task<OrderResult> PlaceOrderAsync(Order order)
    {
        var bankProxy = _proxyFactory.CreateActorProxy<IBankActor>(
            new ActorId(order.CustomerId), "BankActor");
        await bankProxy.WithdrawAsync(order.Amount);
        // ... additional logic
    }
}

Now mock the proxy factory in tests:

[Fact]
public async Task PlaceOrder_WithdrawsFromBank()
{
    var mockBank = new Mock<IBankActor>();
    var mockFactory = new Mock<IActorProxyFactory>();
    mockFactory
        .Setup(f => f.CreateActorProxy<IBankActor>(
            It.IsAny<ActorId>(), "BankActor"))
        .Returns(mockBank.Object);

    var host = new ActorHost(ActorTypeName: "Test");
    var actor = new OrderActor(host, mockFactory.Object);

    await actor.PlaceOrderAsync(new Order { Amount = 50m });

    mockBank.Verify(b => b.WithdrawAsync(50m, default), Times.Once);
}

Testing timers and reminders

Test that your actor registers timers and reminders correctly on activation:

[Fact]
public async Task OnActivate_RegistersReminder()
{
    var host = new ActorHost(ActorTypeName: "Test");
    var actor = new MyActor(host);

    await actor.OnActivateAsync();

    // Assert reminder registration behavior
    // (verify via mock IActorReminderManager if injected)
}

Note: Timer callbacks execute asynchronously. Test the registration in unit tests; test execution in integration tests with a Dapr sidecar.

Integration testing with Dapr

For full integration with the actor runtime, use the Dapr sidecar:

[Collection("Dapr")]
public class ActorIntegrationTests
{
    private readonly DaprContainer _dapr;

    public ActorIntegrationTests()
    {
        _dapr = new DaprBuilder()
            .WithAppPort(5001)
            .Build();
        _dapr.StartAsync().GetAwaiter().GetResult();
    }

    [Fact]
    public async Task Actor_RetainsStateAcrossCalls()
    {
        var proxy = ActorProxy.Create<IBankActor>(
            new ActorId("test-123"), "BankActor");

        await proxy.DepositAsync(50m);
        decimal balance = await proxy.GetBalanceAsync();

        Assert.Equal(50m, balance);
    }
}

Best practices

PrincipleWhy
Inject dependenciesMakes mocks trivial: IActorStateManager, IActorProxyFactory, etc.
Avoid timer execution in unit testsTimers run callbacks asynchronously; test registration only
Use lightweight ActorHostFast creation; no Dapr sidecar needed for unit tests
Test state isolationEach test creates a fresh actor instance
Separate unit and integration testsUnit tests stay fast; integration tests use Dapr

See also