Skip to main content

Test applications that use Zaris

When you test code that talks to Clustron Zaris, you don't need a running cluster, a container, or a network connection. Run the store in-process instead. An in-process store is just a connection string — zaris://inproc/<store> — that hosts the store inside your test process and exposes the same client API you use in production, so the code under test never knows the difference. This article shows you how to use an in-process connection string as a zero-infrastructure test harness.

The key property that makes this work is API parity: the client you resolve in a test is the identical IZaris interface you resolve against a distributed cluster. You swap only the connection string — zaris://inproc/... in a test, zaris://host.../... in production — so the same application code runs unchanged. For the full comparison, see In-process and remote.

Why in-process mode is a good test harness:

  • Identical API. The store you test against implements the same IZaris client as production. No mocks, no shims, no separate code path.
  • No server, no Docker. The store lives in your test process. There is nothing to install, start, or tear down.
  • Fast and deterministic. There is no network hop and no shared cluster, so tests run quickly and don't flake on connectivity.
note

An in-process store is ideal for unit and component tests of your own code. It is not a substitute for a real cluster when you need to verify distributed behavior — see Test against a real cluster below.

A minimal test

Register an in-process store, resolve the client, and exercise it. The registration call is AddClustronZaris(name, "zaris://inproc/<store>"), and you resolve the client through IZarisClientProvider, exactly as you would in an application. Cast to IZaris so the coordination features are available if the code under test uses them.

The following xUnit test round-trips a value through PutAsync and GetAsync and asserts on the returned KvResult.

using Clustron.Zaris.Abstractions;
using Clustron.Zaris.Client;
using Clustron.Zaris.Client.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

public class OrderStoreTests
{
[Fact]
public async Task Put_then_get_round_trips_a_value()
{
// Arrange: an in-process store — no server, no network.
var services = new ServiceCollection();
services.AddClustronZaris("tests", "zaris://inproc/tests");
await using var provider = services.BuildServiceProvider();

var client = (IZaris)await provider
.GetRequiredService<IZarisClientProvider>()
.GetAsync("tests");

// Act
var put = await client.PutAsync("order:42", "confirmed");
var got = await client.GetAsync<string>("order:42");

// Assert
Assert.True(put.IsSuccess);
Assert.Equal(KvStatus.Success, put.Status);

Assert.True(got.IsSuccess);
Assert.Equal("confirmed", got.Value);
}
}

A few points about the result type you assert on:

  • IsSuccess is true only when Status is Success and there is no error, so it's the single check to lead with.
  • Status is a KvStatus value (for example Success or NotFound), which lets you assert on the specific outcome of an operation.
  • Value carries the deserialized payload on KvResult<T> returned by GetAsync<T>. It is meaningful only when the read succeeded.
  • Error carries the failure message when an operation did not succeed.

To test time-to-live behavior, attach a TTL to the write with the Put options helper and assert on the result the same way:

var put = await client.PutAsync("session:abc", "token", Put.WithTtl(TimeSpan.FromMinutes(5)));
Assert.True(put.IsSuccess);

Isolate tests from each other

An in-process store holds its data in memory for as long as its host lives, so two tests that share a store also share state. Keep tests independent by giving each one its own store. There are two straightforward ways to do that.

Build a fresh service provider per test. Because the provider owns the store host, disposing the provider tears the store down with it. Bind it with await using so cleanup happens even if an assertion throws:

var services = new ServiceCollection();
services.AddClustronZaris("tests", "zaris://inproc/tests");
await using var provider = services.BuildServiceProvider();

Each test that builds its own provider starts from an empty store and disposes cleanly at the end. In xUnit, where a new test-class instance is created per test method, putting this in the constructor (with the provider disposed in IAsyncLifetime.DisposeAsync or an IDisposable) gives every test a clean store automatically.

Or use a distinct store name per test. If you prefer to share one provider across a class, register a uniquely named in-process store per test — give the connection string a unique store name and a matching DI key — so their key spaces don't collide:

var name = $"tests-{Guid.NewGuid():N}";
services.AddClustronZaris(name, $"zaris://inproc/{name}");
// ...
var client = (IZaris)await provider.GetRequiredService<IZarisClientProvider>()
.GetAsync(name);
note

Don't dispose the client itself between operations. The client is long-lived and its lifecycle is owned by the provider — you dispose the provider (or let the test host dispose it), not the client. Transactions and watch subscriptions are the exception: dispose or stop each one you create. See Client lifecycle for the full disposal rules.

What in-process mode covers — and what it doesn't

An in-process store runs a real store, not a stub. The in-process client is backed by the same store engine that the server uses, so the operations your code calls behave as they do in production for a single instance. That includes:

  • Key-value operations — PutAsync, GetAsync, DeleteAsync, their bulk variants, and batches.
  • TTL on writes through Put.WithTtl, and reading remaining TTL with GetTimeToLiveAsync.
  • Coordination features exposed by IZaris: counters, leases, locks, watch subscriptions, and scan or query.
  • Transactions through BeginTransactionAsync.

What in-process mode inherently cannot cover is anything that only exists when the store spans more than one node. A single process has no peers to replicate to, no partitions to move, and no failover to trigger, so the following are out of scope for in-process tests and belong in integration tests against a real cluster:

  • Replication, quorum, and the associated statuses (for example a write rejected because a quorum was not reached).
  • Rebalancing and ownership changes as partitions move between nodes.
  • Node failover and the client's reconnect and retry behavior against an unreachable node.
  • State shared across separate processes or application instances.
note

Treat this split as "single-instance semantics in-process, distributed semantics on a cluster." If you are unsure whether a particular operation or edge case reproduces the production behavior you care about in-process, verify it in your own environment before you rely on it — and move the check to a cluster-backed integration test if it depends on any of the distributed behaviors above.

Test against a real cluster

When a test genuinely needs distributed behavior — replication, failover, cross-instance sharing, or scale — point it at a running cluster instead of an in-process store. The application code doesn't change; you register the store with a remote connection string — AddClustronZaris("tests", "zaris://host:port/tests"), or an entry under ConnectionStrings — rather than a zaris://inproc/... one, and the same client API drives the test.

For integration tests, the simplest way to stand up a cluster on a developer machine or a CI runner is Docker Compose. See Run a secured cluster with Docker Compose for a deployment you can bring up before the test run and tear down after.

Next steps