Unit testing your Orleans actors in isolation is not easy. The problem is that they all have to inherit from a Grain or Grain<T> base class. This class has a lot of properties that you probably are using during the lifetime of your actor(e.g. accessing state, writing state, using the GrainFactory to talk to other actors,ā¦)
I first tried to avoid the unit testing problem by focussing more on integration testing and using the TestingSiloHost but the need to test some functionality in isolation remained. Time for a better solutionā¦
I searched around on the web to see how other people tackled this issue. Some falled back to mocking frameworks but it didnāt help to make the tests more readible. In the end I ended on a simple approach where we use some simple composition and extract the real actor logic into a separate class:
public class CalculatorGrain : Grain<CalculatorGrain.GrainState>, ICalculatorGrain | |
{ | |
private CalculatorLogic _logic; | |
public class GrainState | |
{ | |
public List<int> Values { get; set; } | |
} | |
public override Task OnActivateAsync() | |
{ | |
_logic = new CalculatorLogic( | |
state: State, | |
grainFactory: GrainFactory, | |
writeState: WriteStateAsync | |
); | |
return base.OnActivateAsync(); | |
} | |
public Task EnterValue(int value) => _logic.EnterValue(value); | |
private Task<int> CalculatSum() => _logic.CalculateSum(); | |
} |
public class CalculatorLogic | |
{ | |
private readonly CalculatorGrain.GrainState _state; | |
private readonly IGrainFactory _grainFactory; | |
private readonly Func<Task> _writeState; | |
public CalculatorLogic( | |
CalculatorGrain.GrainState state, | |
IGrainFactory grainFactory, | |
Func<Task> writeState | |
) | |
{ | |
_state = state; | |
_grainFactory = grainFactory; | |
_writeState = writeState; | |
} | |
public async Task EnterValue(int value) | |
{ | |
_state.Values.Add(value); | |
await _writeState(); | |
} | |
public async Task<int> CalculateSum() | |
{ | |
var sum=_state.Values.Sum(); | |
return Task.FromResult(sum); | |
} | |
} |
public interface ICalculatorGrain : IGrainWithStringKey | |
{ | |
Task EnterValue(int value); | |
Task<int> CalculateSum(); | |
} |