Testing
Test handlers as plain classes, fake IJobDispatcher in code that dispatches, and run end-to-end tests against a real SQLite file.
On this page
Runwick's public surface is small and made of interfaces, so most tests need no special library: a handler is a plain class, and dispatching goes through IJobDispatcher. For the full path (dispatch, claim, run, finish), run a real worker against a temporary SQLite file.
About Runwick.Testing
The Runwick.Testing package is reserved for test helpers (a recording dispatcher and a direct handler invoker), but in this release it does not contain any public types yet. The patterns on this page use only the main packages.
Testing a handler
A handler is a class with a HandleAsync method. Construct it with its dependencies and call it:
Test the things Runwick relies on too:
- Repeats are harmless. Call
HandleAsynctwice with the same job and check that the outside effect happened once, or that the same idempotency key was used both times. Runwick can and will run a handler more than once; see Delivery guarantees. - Cancellation is observed. Pass a cancelled token and check that the handler stops with an
OperationCanceledExceptioninstead of finishing the work. - Permanent failures are permanent. If the handler throws
PermanentJobFailureExceptionfor bad input, assert that it does.
Handlers that use IJobContext
IJobContext is an interface, so a test can pass its own implementation. A small fake with settable properties is enough:
Then test, for example, that a handler uses the job ID as its idempotency key, or behaves differently on the last attempt:
Testing code that dispatches
IJobDispatcher has two methods. A recording fake that returns a receipt is a dozen lines:
Use it in place of the real dispatcher:
A fake cannot tell you whether the real store would accept the job: unknown queues, keys that are too long and payloads over 1 MiB are only refused by the real dispatcher. Cover those with an end-to-end test.
To test how your code handles failures, make the fake throw the real exception types, for example DispatchOutcomeUnknownException or LocalCapacityExceededException.
End-to-end with a real SQLite file
SQLite needs no server, so a test can create a fresh database file, start a real host with a worker, dispatch a job and wait for the handler. The handler signals a TaskCompletionSource from dependency injection:
Tips:
- Give every test its own file. Runwick refuses in-memory databases, so a temporary file is the way to isolate tests.
- Keep the temp directory on a local disk. A network share is refused.
- Leave a generous timeout. Workers poll, and a fresh worker needs a moment to start.
- Use the same registration code as production where you can, and swap only the store line.
When to test against PostgreSQL or Redis
All three stores give the same job behaviour, so SQLite covers most application tests. Test against your production store when the test depends on store-specific behaviour: staging jobs or completing them in your PostgreSQL transaction, connection limits, or Redis server settings. A disposable server per test run (for example with Testcontainers) works well. Remember to run the store's migration first.