Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions TUnit.Engine/TestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(

executableTest.Context.ClassContext.RestoreExecutionContext();

// Initialize test objects (IAsyncInitializer) AFTER BeforeClass hooks
// This ensures resources like Docker containers are not started until needed
await testInitializer.InitializeTestObjectsAsync(executableTest, cancellationToken).ConfigureAwait(false);

#if NET
if (TUnitActivitySource.Source.HasListeners())
{
Expand All @@ -136,10 +140,6 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync(
}
#endif

// Initialize test objects (IAsyncInitializer) AFTER BeforeClass hooks
// This ensures resources like Docker containers are not started until needed
await testInitializer.InitializeTestObjectsAsync(executableTest, cancellationToken).ConfigureAwait(false);

executableTest.Context.RestoreExecutionContext();

// Early stage test start receivers run before instance-level hooks
Expand Down
25 changes: 24 additions & 1 deletion TUnit.Engine/TestInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using TUnit.Core;
using TUnit.Engine.Services;
#if NET
using System.Diagnostics;
#endif

namespace TUnit.Engine;

Expand Down Expand Up @@ -32,7 +35,27 @@ public void PrepareTest(AbstractExecutableTest test, CancellationToken cancellat

public async ValueTask InitializeTestObjectsAsync(AbstractExecutableTest test, CancellationToken cancellationToken)
{
// Initialize test objects (IAsyncInitializer) - called after BeforeClass hooks
// Data source initialization runs before the test case span starts, so any spans it
// creates (container startup, auth calls, connection pools, etc.) do not appear nested
// inside the individual test's trace timeline. We briefly set Activity.Current to the
// session span so those spans are parented there instead.
#if NET
var sessionActivity = test.Context.ClassContext.AssemblyContext.TestSessionContext.Activity;
var previousActivity = Activity.Current;
if (sessionActivity is not null)
{
Activity.Current = sessionActivity;
}
try
{
await _objectLifecycleService.InitializeTestObjectsAsync(test.Context, cancellationToken);
}
finally
{
Activity.Current = previousActivity;
}
#else
await _objectLifecycleService.InitializeTestObjectsAsync(test.Context, cancellationToken);
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using CloudShop.Shared.Contracts;
using CloudShop.Tests.Infrastructure;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

namespace CloudShop.Tests.Tests.Traces;

/// <summary>
/// Demonstrates TUnit's trace capture in the HTML report.
///
/// The test drives the API using HTTP calls — the application handles its own
/// Redis caching and database queries internally. Custom spans provide semantic
/// context around the steps of the checkout flow so the trace timeline tells
/// a readable story.
///
/// Run explicitly to see the trace timeline in the generated HTML report.
/// </summary>
[Category("Integration"), Category("Traces")]
public class CheckoutTraceDemoTests
{
private static readonly ActivitySource Source = new("CloudShop.Tests.Checkout");

[ClassDataSource<CustomerApiClient>(Shared = SharedType.PerTestSession)]
public required CustomerApiClient Customer { get; init; }

[ClassDataSource<AdminApiClient>(Shared = SharedType.PerTestSession)]
public required AdminApiClient Admin { get; init; }

[Test]
[Explicit]
[DisplayName("Checkout fails when requested quantity exceeds available stock")]
public async Task Checkout_FailsWhenRequestedQuantityExceedsStock()
{
// Step 1: Browse the catalogue — the API queries PostgreSQL and returns products
ProductResponse product;
using (var browseSpan = Source.StartActivity("browse.list_products"))
{
var catalogue = await Customer.Client
.GetFromJsonAsync<PagedResult<ProductResponse>>("/api/products?category=electronics&pageSize=1");
product = catalogue!.Items.First();
browseSpan?.SetTag("product.id", product.Id);
browseSpan?.SetTag("product.stock_available", product.StockQuantity);
}

// Step 2: View the product detail — the API checks Redis, falls back to PostgreSQL on
// a cold cache, then caches the result for subsequent requests
using (var detailSpan = Source.StartActivity("browse.get_product_detail"))
{
await Customer.Client.GetAsync($"/api/products/{product.Id}");
detailSpan?.SetTag("product.id", product.Id);
}

// Step 3: View it again — this time the API serves it directly from Redis
using (var cachedSpan = Source.StartActivity("browse.get_product_detail_cached"))
{
await Customer.Client.GetAsync($"/api/products/{product.Id}");
cachedSpan?.SetTag("product.id", product.Id);
cachedSpan?.SetTag("cache.expected", true);
}

// Step 4: Attempt to order more units than are in stock — the API validates against
// the database and rejects the request
var requestedQuantity = product.StockQuantity + 500;
using var orderSpan = Source.StartActivity("checkout.create_order");
orderSpan?.SetTag("product.id", product.Id);
orderSpan?.SetTag("order.quantity_requested", requestedQuantity);
orderSpan?.SetTag("order.stock_available", product.StockQuantity);

var orderResponse = await Customer.Client.PostAsJsonAsync("/api/orders",
new CreateOrderRequest(
[new OrderItemRequest(product.Id, requestedQuantity)],
"credit_card",
"standard"));

if (!orderResponse.IsSuccessStatusCode)
{
var body = await orderResponse.Content.ReadAsStringAsync();
orderSpan?.SetStatus(ActivityStatusCode.Error, "Order rejected: insufficient stock");
orderSpan?.SetTag("error.message", body);
}

// This assertion fails — we deliberately requested more than was available
await Assert.That(orderResponse.StatusCode).IsEqualTo(HttpStatusCode.Created);
}
}
Loading