Skip to content

Commit 55e0dba

Browse files
authored
Make Actors unit testable (#576)
Fixes: #574 Changes ActorHost constructor to be public again. Now the state manager and http interactor are set via properties. This means that code in unit tests won't be able to cover the methods that interact with timers or reminders - however this was already the case. Testing state management is covered by replacing the `IActorStateManager` with a mock. Logged an issue to improve this. Also added validation. If you try to use something that's not fully initialized it will result in an exception with a meaningful message instead of a null reference. Also skips tests that are failing due to #573. We know the cause of why these are failing, and these tests have not been stable since they were introduced. Failing additional CI builds is not giving us new information.
1 parent 7f9d23c commit 55e0dba

File tree

10 files changed

+79
-25
lines changed

10 files changed

+79
-25
lines changed

src/Dapr.Actors/Runtime/Actor.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ protected async Task<IActorReminder> RegisterReminderAsync(
207207
TimeSpan dueTime,
208208
TimeSpan period)
209209
{
210+
EnsureInteractorInitialized();
211+
210212
var reminderInfo = new ReminderInfo(state, dueTime, period);
211213
var reminder = new ActorReminder(this.Id, reminderName, reminderInfo);
212214
var serializedReminderInfo = await reminderInfo.SerializeAsync();
@@ -223,6 +225,7 @@ protected async Task<IActorReminder> RegisterReminderAsync(
223225
/// </returns>
224226
protected Task UnregisterReminderAsync(IActorReminder reminder)
225227
{
228+
EnsureInteractorInitialized();
226229
return this.Host.DaprInteractor.UnregisterReminderAsync(this.actorTypeName, this.Id.ToString(), reminder.Name);
227230
}
228231

@@ -235,6 +238,7 @@ protected Task UnregisterReminderAsync(IActorReminder reminder)
235238
/// </returns>
236239
protected Task UnregisterReminderAsync(string reminderName)
237240
{
241+
EnsureInteractorInitialized();
238242
return this.Host.DaprInteractor.UnregisterReminderAsync(this.actorTypeName, this.Id.ToString(), reminderName);
239243
}
240244

@@ -263,6 +267,8 @@ public async Task<ActorTimer> RegisterTimerAsync(
263267
TimeSpan dueTime,
264268
TimeSpan period)
265269
{
270+
EnsureInteractorInitialized();
271+
266272
// Validate that the timer callback specified meets all the required criteria for a valid callback method
267273
this.ValidateTimerCallback(this.Host, callback);
268274

@@ -287,6 +293,7 @@ public async Task<ActorTimer> RegisterTimerAsync(
287293
/// <returns>Task representing the Unregister timer operation.</returns>
288294
protected async Task UnregisterTimerAsync(ActorTimer timer)
289295
{
296+
EnsureInteractorInitialized();
290297
await this.Host.DaprInteractor.UnregisterTimerAsync(this.actorTypeName, this.Id.ToString(), timer.Name);
291298
}
292299

@@ -297,6 +304,7 @@ protected async Task UnregisterTimerAsync(ActorTimer timer)
297304
/// <returns>Task representing the Unregister timer operation.</returns>
298305
protected async Task UnregisterTimerAsync(string timerName)
299306
{
307+
EnsureInteractorInitialized();
300308
await this.Host.DaprInteractor.UnregisterTimerAsync(this.actorTypeName, this.Id.ToString(), timerName);
301309
}
302310

@@ -340,5 +348,15 @@ internal void ValidateTimerCallback(ActorHost host, string callback)
340348
throw new ArgumentException("Timer callback can only return type Task");
341349
}
342350
}
351+
352+
private void EnsureInteractorInitialized()
353+
{
354+
if (this.Host.DaprInteractor == null)
355+
{
356+
throw new InvalidOperationException(
357+
"The actor was initialized without an HTTP client, and so cannot interact with timers or reminders. " +
358+
"This is likely to happen inside a unit test.");
359+
}
360+
}
343361
}
344362
}

src/Dapr.Actors/Runtime/ActorHost.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,17 @@ public sealed class ActorHost
2222
/// <param name="jsonSerializerOptions">The <see cref="JsonSerializerOptions"/> to use for actor state persistence and message deserialization.</param>
2323
/// <param name="loggerFactory">The logger factory.</param>
2424
/// <param name="proxyFactory">The <see cref="ActorProxyFactory" />.</param>
25-
/// <param name="daprInteractor">The <see cref="IDaprInteractor" />.</param>
26-
internal ActorHost(
25+
public ActorHost(
2726
ActorTypeInformation actorTypeInfo,
2827
ActorId id,
2928
JsonSerializerOptions jsonSerializerOptions,
3029
ILoggerFactory loggerFactory,
31-
IActorProxyFactory proxyFactory,
32-
IDaprInteractor daprInteractor)
30+
IActorProxyFactory proxyFactory)
3331
{
3432
this.ActorTypeInfo = actorTypeInfo;
3533
this.Id = id;
3634
this.LoggerFactory = loggerFactory;
3735
this.ProxyFactory = proxyFactory;
38-
this.DaprInteractor = daprInteractor;
39-
this.StateProvider = new DaprStateProvider(this.DaprInteractor, jsonSerializerOptions);
4036
}
4137

4238
/// <summary>
@@ -64,7 +60,7 @@ internal ActorHost(
6460
/// </summary>
6561
public IActorProxyFactory ProxyFactory { get; }
6662

67-
internal DaprStateProvider StateProvider { get; }
63+
internal DaprStateProvider StateProvider { get; set; }
6864

6965
internal IDaprInteractor DaprInteractor { get; set; }
7066
}

src/Dapr.Actors/Runtime/ActorManager.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,11 @@ internal async Task ActivateActorAsync(ActorId actorId)
263263
private async Task<ActorActivatorState> CreateActorAsync(ActorId actorId)
264264
{
265265
this.logger.LogDebug("Creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId);
266-
var host = new ActorHost(this.ActorTypeInfo, actorId, this.jsonSerializerOptions, this.loggerFactory, this.proxyFactory, this.daprInteractor);
266+
var host = new ActorHost(this.ActorTypeInfo, actorId, this.jsonSerializerOptions, this.loggerFactory, this.proxyFactory)
267+
{
268+
DaprInteractor = this.daprInteractor,
269+
StateProvider = new DaprStateProvider(this.daprInteractor, this.jsonSerializerOptions),
270+
};
267271
var state = await this.activator.CreateAsync(host);
268272
this.logger.LogDebug("Finished creating Actor of type {ActorType} with ActorId {ActorId}", this.ActorTypeInfo.ImplementationType, actorId);
269273
return state;

src/Dapr.Actors/Runtime/ActorStateManager.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ internal ActorStateManager(Actor actor)
2727

2828
public async Task AddStateAsync<T>(string stateName, T value, CancellationToken cancellationToken)
2929
{
30+
EnsureStateProviderInitialized();
31+
3032
if (!(await this.TryAddStateAsync(stateName, value, cancellationToken)))
3133
{
3234
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName));
@@ -37,6 +39,8 @@ public async Task<bool> TryAddStateAsync<T>(string stateName, T value, Cancellat
3739
{
3840
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
3941

42+
EnsureStateProviderInitialized();
43+
4044
if (this.stateChangeTracker.ContainsKey(stateName))
4145
{
4246
var stateMetadata = this.stateChangeTracker[stateName];
@@ -62,6 +66,8 @@ public async Task<bool> TryAddStateAsync<T>(string stateName, T value, Cancellat
6266

6367
public async Task<T> GetStateAsync<T>(string stateName, CancellationToken cancellationToken)
6468
{
69+
EnsureStateProviderInitialized();
70+
6571
var condRes = await this.TryGetStateAsync<T>(stateName, cancellationToken);
6672

6773
if (condRes.HasValue)
@@ -75,6 +81,9 @@ public async Task<T> GetStateAsync<T>(string stateName, CancellationToken cancel
7581
public async Task<ConditionalValue<T>> TryGetStateAsync<T>(string stateName, CancellationToken cancellationToken)
7682
{
7783
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
84+
85+
EnsureStateProviderInitialized();
86+
7887
if (this.stateChangeTracker.ContainsKey(stateName))
7988
{
8089
var stateMetadata = this.stateChangeTracker[stateName];
@@ -101,6 +110,8 @@ public async Task SetStateAsync<T>(string stateName, T value, CancellationToken
101110
{
102111
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
103112

113+
EnsureStateProviderInitialized();
114+
104115
if (this.stateChangeTracker.ContainsKey(stateName))
105116
{
106117
var stateMetadata = this.stateChangeTracker[stateName];
@@ -124,6 +135,8 @@ public async Task SetStateAsync<T>(string stateName, T value, CancellationToken
124135

125136
public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken)
126137
{
138+
EnsureStateProviderInitialized();
139+
127140
if (!(await this.TryRemoveStateAsync(stateName, cancellationToken)))
128141
{
129142
throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, SR.ErrorNamedActorStateNotFound, stateName));
@@ -134,6 +147,8 @@ public async Task<bool> TryRemoveStateAsync(string stateName, CancellationToken
134147
{
135148
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
136149

150+
EnsureStateProviderInitialized();
151+
137152
if (this.stateChangeTracker.ContainsKey(stateName))
138153
{
139154
var stateMetadata = this.stateChangeTracker[stateName];
@@ -164,6 +179,8 @@ public async Task<bool> ContainsStateAsync(string stateName, CancellationToken c
164179
{
165180
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
166181

182+
EnsureStateProviderInitialized();
183+
167184
if (this.stateChangeTracker.ContainsKey(stateName))
168185
{
169186
var stateMetadata = this.stateChangeTracker[stateName];
@@ -182,6 +199,8 @@ public async Task<bool> ContainsStateAsync(string stateName, CancellationToken c
182199

183200
public async Task<T> GetOrAddStateAsync<T>(string stateName, T value, CancellationToken cancellationToken)
184201
{
202+
EnsureStateProviderInitialized();
203+
185204
var condRes = await this.TryGetStateAsync<T>(stateName, cancellationToken);
186205

187206
if (condRes.HasValue)
@@ -203,6 +222,8 @@ public async Task<T> AddOrUpdateStateAsync<T>(
203222
{
204223
ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName));
205224

225+
EnsureStateProviderInitialized();
226+
206227
if (this.stateChangeTracker.ContainsKey(stateName))
207228
{
208229
var stateMetadata = this.stateChangeTracker[stateName];
@@ -240,12 +261,16 @@ public async Task<T> AddOrUpdateStateAsync<T>(
240261

241262
public Task ClearCacheAsync(CancellationToken cancellationToken)
242263
{
264+
EnsureStateProviderInitialized();
265+
243266
this.stateChangeTracker.Clear();
244267
return Task.CompletedTask;
245268
}
246269

247270
public async Task SaveStateAsync(CancellationToken cancellationToken = default)
248271
{
272+
EnsureStateProviderInitialized();
273+
249274
if (this.stateChangeTracker.Count > 0)
250275
{
251276
var stateChangeList = new List<ActorStateChange>();
@@ -296,9 +321,20 @@ private bool IsStateMarkedForRemove(string stateName)
296321

297322
private Task<ConditionalValue<T>> TryGetStateFromStateProviderAsync<T>(string stateName, CancellationToken cancellationToken)
298323
{
324+
EnsureStateProviderInitialized();
299325
return this.actor.Host.StateProvider.TryLoadStateAsync<T>(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken);
300326
}
301327

328+
private void EnsureStateProviderInitialized()
329+
{
330+
if (this.actor.Host.StateProvider == null)
331+
{
332+
throw new InvalidOperationException(
333+
"The actor was initialized without a state provider, and so cannot interact with state. " +
334+
"If this is inside a unit test, replace Actor.StateProvider with a mock.");
335+
}
336+
}
337+
302338
private sealed class StateMetadata
303339
{
304340
private StateMetadata(object value, Type type, StateChangeKind changeKind)

test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public async Task CreateAsync_CanActivateWithDI()
3232
{
3333
var activator = CreateActivator(typeof(TestActor));
3434

35-
var host = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
35+
var host = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
3636
var state = await activator.CreateAsync(host);
3737
var actor = Assert.IsType<TestActor>(state.Actor);
3838

@@ -45,11 +45,11 @@ public async Task CreateAsync_CreatesNewScope()
4545
{
4646
var activator = CreateActivator(typeof(TestActor));
4747

48-
var host1 = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
48+
var host1 = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
4949
var state1 = await activator.CreateAsync(host1);
5050
var actor1 = Assert.IsType<TestActor>(state1.Actor);
5151

52-
var host2 = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
52+
var host2 = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
5353
var state2 = await activator.CreateAsync(host2);
5454
var actor2 = Assert.IsType<TestActor>(state2.Actor);
5555

@@ -62,7 +62,7 @@ public async Task DeleteAsync_DisposesScope()
6262
{
6363
var activator = CreateActivator(typeof(TestActor));
6464

65-
var host = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
65+
var host = new ActorHost(ActorTypeInformation.Get(typeof(TestActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
6666
var state = await activator.CreateAsync(host);
6767
var actor = Assert.IsType<TestActor>(state.Actor);
6868

@@ -78,7 +78,7 @@ public async Task DeleteAsync_Disposable()
7878
{
7979
var activator = CreateActivator(typeof(DisposableActor));
8080

81-
var host = new ActorHost(ActorTypeInformation.Get(typeof(DisposableActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
81+
var host = new ActorHost(ActorTypeInformation.Get(typeof(DisposableActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
8282
var state = await activator.CreateAsync(host);
8383
var actor = Assert.IsType<DisposableActor>(state.Actor);
8484

@@ -92,7 +92,7 @@ public async Task DeleteAsync_AsyncDisposable()
9292
{
9393
var activator = CreateActivator(typeof(AsyncDisposableActor));
9494

95-
var host = new ActorHost(ActorTypeInformation.Get(typeof(AsyncDisposableActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
95+
var host = new ActorHost(ActorTypeInformation.Get(typeof(AsyncDisposableActor)), ActorId.CreateRandom(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory);
9696
var state = await activator.CreateAsync(host);
9797
var actor = Assert.IsType<AsyncDisposableActor>(state.Actor);
9898

test/Dapr.Actors.Test/ApiTokenTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Dapr.Actors.Test
1616
{
1717
public class ApiTokenTests
1818
{
19-
[Fact]
19+
[Fact(Skip = "Failing due to #573")]
2020
public void CreateProxyWithRemoting_WithApiToken()
2121
{
2222
var actorId = new ActorId("abc");
@@ -34,7 +34,7 @@ public void CreateProxyWithRemoting_WithApiToken()
3434
headerValues.Should().Contain("test_token");
3535
}
3636

37-
[Fact]
37+
[Fact(Skip = "Failing due to #573")]
3838
public void CreateProxyWithRemoting_WithNoApiToken()
3939
{
4040
var actorId = new ActorId("abc");
@@ -48,7 +48,7 @@ public void CreateProxyWithRemoting_WithNoApiToken()
4848
action.Should().Throw<InvalidOperationException>();
4949
}
5050

51-
[Fact]
51+
[Fact(Skip = "Failing due to #573")]
5252
public void CreateProxyWithNoRemoting_WithApiToken()
5353
{
5454
var actorId = new ActorId("abc");
@@ -66,7 +66,7 @@ public void CreateProxyWithNoRemoting_WithApiToken()
6666
headerValues.Should().Contain("test_token");
6767
}
6868

69-
[Fact]
69+
[Fact(Skip = "Failing due to #573")]
7070
public void CreateProxyWithNoRemoting_WithNoApiToken()
7171
{
7272
var actorId = new ActorId("abc");

test/Dapr.Actors.Test/DaprHttpInteractorTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
6161
}
6262
}
6363

64-
[Fact]
64+
[Fact(Skip = "Failing due to #573")]
6565
public void GetState_ValidateRequest()
6666
{
6767
var handler = new TestHttpClientHandler();

test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public void TestRegisterActor_SavesActivator()
2020
{
2121
var actorType = typeof(TestActor);
2222
var actorTypeInformation = ActorTypeInformation.Get(actorType);
23-
var host = new ActorHost(actorTypeInformation, ActorId.CreateRandom(), JsonSerializerDefaults.Web, new LoggerFactory(), ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
23+
var host = new ActorHost(actorTypeInformation, ActorId.CreateRandom(), JsonSerializerDefaults.Web, new LoggerFactory(), ActorProxy.DefaultProxyFactory);
2424
var actor = new TestActor(host);
2525

2626
var activator = Mock.Of<ActorActivator>();

test/Dapr.Actors.Test/Runtime/ActorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ private TestActor CreateTestDemoActor(IActorStateManager actorStateManager)
116116
{
117117
var actorTypeInformation = ActorTypeInformation.Get(typeof(TestActor));
118118
var loggerFactory = new LoggerFactory();
119-
var host = new ActorHost(actorTypeInformation, ActorId.CreateRandom(), JsonSerializerDefaults.Web, loggerFactory, ActorProxy.DefaultProxyFactory, new DaprHttpInteractor());
119+
var host = new ActorHost(actorTypeInformation, ActorId.CreateRandom(), JsonSerializerDefaults.Web, loggerFactory, ActorProxy.DefaultProxyFactory);
120120
var testActor = new TestActor(host, actorStateManager);
121121
return testActor;
122122
}

0 commit comments

Comments
 (0)