Conversation
…lasticsearch clusters
The existing xUnit integration (`Elastic.Elasticsearch.Xunit`) requires substantial ceremony:
a custom `TestFramework`, a custom `TestAssemblyRunner`, a third-party partitioning library,
custom test discoverers, and marker interfaces. TUnit's built-in primitives eliminate all of
this, making integration tests against Elasticsearch dramatically simpler to set up and run.
## Why
Getting a one-node Elasticsearch cluster into a TUnit test should be a one-liner cluster
definition and a single attribute — no assembly-level wiring, no marker interfaces, no
special `[I]` / `[U]` attributes. This library makes that possible:
```csharp
public class MyCluster() : ElasticsearchCluster("latest-9");
[ClassDataSource<MyCluster>(Shared = SharedType.Keyed, Key = nameof(MyCluster))]
public class MyTests(MyCluster cluster)
{
[Test]
public async Task InfoReturnsNodeName()
{
var client = cluster.GetOrAddClient((c, output) =>
{
var settings = new ElasticsearchClientSettings(new StaticNodePool(c.NodesUris()))
.EnableDebugMode()
.OnRequestCompleted(call => output.WriteLine(call.DebugInformation));
return new ElasticsearchClient(settings);
});
await Assert.That(client.Info().Name).IsNotNull();
}
}
```
## Bootstrap diagnostics
Cluster startup takes 60+ seconds. Rather than silence or a wall of logs, the library
auto-detects the environment and picks the right output level:
**Interactive terminal (default)** — periodic heartbeat so you know it's alive:
```
Bootstrapping MyTestCluster in /tmp/es-tunit-abc [5s] [INFO ][o.e.n.Node] initializing...
Bootstrapping MyTestCluster in /tmp/es-tunit-abc [10s] [INFO ][o.e.e.NodeEnvironment] using [1] ...
Bootstrapping MyTestCluster in /tmp/es-tunit-abc [45s] [INFO ][o.e.h.AbstractHttpServerTransport] ...
```
**CI (GitHub Actions, TeamCity, Jenkins, etc.)** — full verbose ANSI-colored output of every
log line, matching what `LineHighlightWriter` produces but writing directly to the process
stdout to bypass TUnit's per-test capture.
On failure, node-level diagnostics (started status, port, version, last exception) are written
to both the terminal and TUnit's test output system.
## Per-test client diagnostics
`GetOrAddClient` accepts an optional `TextWriter` that dynamically routes to whichever TUnit
test is currently executing. The client is cached per-cluster, but each test sees its own
request/response debug output:
```csharp
var client = cluster.GetOrAddClient((c, output) =>
{
var settings = new ElasticsearchClientSettings(new StaticNodePool(c.NodesUris()))
.EnableDebugMode()
.OnRequestCompleted(call => output.WriteLine(call.DebugInformation));
return new ElasticsearchClient(settings);
});
```
## Version-skip and custom skip conditions
```csharp
[SkipVersion("<8.0.0", "Requires 8.x")]
[Test]
public async Task NewFeatureTest() { }
```
Evaluated via a `[BeforeEvery(Test)]` hook that resolves the cluster version from the test
class's `ClassDataSource` and checks `[SkipVersion]` / `SkipTestAttribute` subclasses.
Includes minimal and complex example projects demonstrating multi-cluster setups,
`[ParallelLimiter]`, version skipping, base class patterns, and `SeedCluster()` overrides.
Move client configuration from test methods to a `Client` property on the cluster class. Tests simply call `cluster.Client` instead of inlining `GetOrAddClient` in every test method. Removes the now-unused `EphemeralClusterExtensions` helper and `IMyCluster` interface from the complex example.
The non-generic ElasticsearchCluster now ships a virtual Client property with debug mode and per-test output wiring out of the box. Clusters that extend the non-generic base inherit it automatically — override to customize. This makes the minimal cluster definition a true one-liner. Transitive dependencies (Elastic.Clients.Elasticsearch, Ephemeral) are removed from the example csproj files since they flow through the TUnit package. README updated to reflect the simplified getting-started.
….Core` + `Elastic.TUnit.Elasticsearch` Separate the core TUnit cluster integration (lifecycle, hooks, skip attributes, diagnostics) from the `Elastic.Clients.Elasticsearch` convenience layer so users who don't need the official client can depend on the Core package alone. - `Elastic.TUnit.Elasticsearch.Core`: barebones package, no ES client dependency - `Elastic.TUnit.Elasticsearch`: recommended package with default Client and `.WireTUnitOutput()` extension
The `generateApiChanges` step fails for packages targeting net8.0/net10.0 because it looks for a netstandard2.0 build output that doesn't exist. Skip the diff when the source directory is absent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Elastic.Elasticsearch.TUnit— a new package that integrates ephemeral Elasticsearch clusters with TUnit.Define a cluster, write tests, and get a real Elasticsearch instance spun up automatically. No boilerplate, no custom test frameworks, no marker interfaces.
Minimal example — this is the entire setup
The base class provides a default
Clientwith debug mode enabled and per-request diagnostics routed to the currently executing test. OverrideClientwhen you need custom authentication, serialization, or connection settings.What you get out of the box
TestContext.Current, even though the client is shared across all tests. This means you can inspect individual test output in your IDEShowBootstrapDiagnostics(true= always full,false= silent,null= auto-detect)TestContext.Currentso it shows up in your IDE's test output. On failure, per-node diagnostics (started status, port, version, last exception) are included[SkipVersion("<8.0.0", "reason")]evaluates against the running cluster's actual version; extendSkipTestAttributefor arbitrary skip conditions[ParallelLimiter<ElasticsearchParallelLimit>]caps test concurrency perEnvironment.ProcessorCountSharedType.Keyedmeans the cluster starts once and is shared across all test classes that reference the same key, with serialized startup across different cluster typesMore involved example
Interactive output
Locally you will get progress updates every 5 seconds to see where it is in the bootstrapping without overwhelming the console.