Skip to content

Testing jobs

Test scheduling and execution deterministically with fake time, draining, captures and workflow assertions.

1 min read
dotnet add package Immediate.Jobs.Testing --prerelease

Execute with fake time

JobTestHarness builds an in-memory, full-capability scheduler around a FakeTimeProvider. Register the generated jobs and their dependencies:

await using var harness = new JobTestHarness(services =>
{
	services.AddMyAppHandlers();
	services.AddMyAppJobs(options => options.UseInMemory());
	services.AddSingleton<IEmailSender, RecordingEmailSender>();
});

await using var scope = harness.Services.CreateAsyncScope();
var scheduler = scope.ServiceProvider.GetRequiredService<SendWelcomeEmail.Scheduler>();
var handle = await scheduler.ScheduleAsync(
	new(userId, "v2"),
	TimeSpan.FromMinutes(10),
	cancellationToken
);

var queued = await harness.AssertEnqueuedAsync<SendWelcomeEmail.Payload>(
	handle,
	JobState.Scheduled,
	cancellationToken
);
Assert.Equal(userId, queued.Payload.UserId);

await harness.AdvanceTimeAndDrainAsync(TimeSpan.FromMinutes(10), cancellationToken);
Assert.Equal(JobState.Succeeded, (await harness.GetJobAsync(handle, cancellationToken)).State);

DrainAsync runs everything due without wall-clock sleeps. AdvanceTimeAndDrainAsync accepts a TimeSpan or absolute DateTimeOffset. QueryJobsAsync and GetJobAsync inspect durable records; AssertEnqueuedAsync<TPayload> validates state and deserializes the payload. The harness also exposes Storage, TimeProvider, Services and Batches.

For graphs, use AssertBatchCommittedAtomicallyAsync, AssertContinuationReleasedAfterAsync and AssertCascadeCancelledAsync. Call RunThroughPipelineAsync<TPayload> when a test already has a record/payload and needs to execute its generated invoker through the real DI/behavior pipeline.

Capture scheduling only

Use CaptureOnlyJobScheduler<TPayload> when the subject should decide what to schedule but no worker should run:

var scheduler = new CaptureOnlyJobScheduler<SendWelcomeEmail.Payload>();
var payload = new SendWelcomeEmail.Payload(userId, "v2");
var handle = await scheduler.EnqueueAsync(
	payload,
	groupId: "tenant-a",
	cancellationToken: cancellationToken
);

var capture = scheduler.Last!;
Assert.Equal(handle.Id, capture.Id);
Assert.Equal(payload, capture.Payload);
Assert.Equal("tenant-a", capture.GroupId);

Captures preserves call order, Last returns the newest call, and Clear() resets it. Captures include payload, run time, group ID and generated handle. CaptureOnlyRecurringJobScheduler records add/update/remove/trigger operations for payloadless dynamic schedules. Override the capture scheduler’s ID creation when stable IDs make assertions clearer.

Testing helpers throw JobTestAssertionException with job/batch-specific mismatch details. They exercise the same generated JSON metadata and storage state machine as production without relying on wall-clock delays.