Skip to content

Commit 28c2a68

Browse files
committed
refactor: update examples to use ClassDataSource for nested data sources
1 parent 900b9c2 commit 28c2a68

File tree

3 files changed

+124
-184
lines changed

3 files changed

+124
-184
lines changed

docs/docs/test-authoring/nested-data-sources.md

Lines changed: 58 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,11 @@ public class TestApplication : IAsyncInitializer, IAsyncDisposable
9090
}
9191
}
9292

93-
// 3. Create a data source attribute
94-
public class TestApplicationAttribute : DataSourceGeneratorAttribute<TestApplication>
95-
{
96-
public override IEnumerable<TestApplication> GenerateDataSources(DataGeneratorMetadata metadata)
97-
{
98-
yield return new TestApplication();
99-
}
100-
}
101-
102-
// 4. Use in tests
93+
// 3. Use in tests - TUnit automatically handles nested initialization
10394
public class UserApiTests
10495
{
10596
[Test]
106-
[TestApplication]
97+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
10798
public async Task CreateUser_Should_Cache_In_Redis(TestApplication app)
10899
{
109100
// Arrange
@@ -173,76 +164,84 @@ public class CompleteTestEnvironment : IAsyncInitializer, IAsyncDisposable
173164

174165
## Sharing Resources
175166

176-
Expensive resources like test containers should be shared across tests:
167+
Expensive resources like test containers should be shared across tests using the `Shared` parameter:
177168

178169
```csharp
179-
// Share the same instance across all tests in a class
180-
[SharedType(SharedType.PerClass)]
181-
public class SharedTestApplicationAttribute : TestApplicationAttribute
170+
public class OrderApiTests
182171
{
172+
[Test]
173+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
174+
public async Task Test1(TestApplication app)
175+
{
176+
// First test - creates new instance
177+
}
178+
179+
[Test]
180+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
181+
public async Task Test2(TestApplication app)
182+
{
183+
// Reuses the same instance as Test1
184+
}
183185
}
184186

185-
// Or share with a specific key for fine-grained control
186-
[SharedType(SharedType.Keyed, "integration-tests")]
187-
public class KeyedTestApplicationAttribute : TestApplicationAttribute
187+
// Or share with a specific key for fine-grained control across multiple test classes
188+
public class UserApiTests
188189
{
190+
[Test]
191+
[ClassDataSource<TestApplication>(Shared = SharedType.Keyed, Key = "integration-tests")]
192+
public async Task CreateUser(TestApplication app) { /* ... */ }
189193
}
190194

191-
// Usage
192-
[TestClass]
193-
public class OrderApiTests
195+
public class ProductApiTests
194196
{
195197
[Test]
196-
[SharedTestApplication] // Reuses the same instance for all tests in this class
197-
public async Task Test1(TestApplication app) { /* ... */ }
198-
199-
[Test]
200-
[SharedTestApplication] // Same instance as Test1
201-
public async Task Test2(TestApplication app) { /* ... */ }
198+
[ClassDataSource<TestApplication>(Shared = SharedType.Keyed, Key = "integration-tests")]
199+
public async Task CreateProduct(TestApplication app)
200+
{
201+
// Shares the same TestApplication instance with UserApiTests.CreateUser
202+
}
202203
}
203204
```
204205

205-
## Async Data Generation with Dependencies
206+
## Combining with Method Data Sources
206207

207-
You can also use async data source generators that depend on initialized resources:
208+
You can combine nested data sources with parameterized tests using method data sources:
208209

209210
```csharp
210-
public class UserTestDataAttribute : AsyncDataSourceGeneratorAttribute<UserTestData>
211+
public class UserPermissionTests
211212
{
212-
// This will be initialized first
213-
[ClassDataSource<TestApplication>]
214-
public required TestApplication App { get; init; }
215-
216-
public override async IAsyncEnumerable<UserTestData> GenerateDataSourcesAsync(
217-
DataGeneratorMetadata metadata)
213+
[Test]
214+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
215+
[MethodDataSource(nameof(UserScenarios))]
216+
public async Task User_Should_Have_Correct_Permissions(
217+
TestApplication app,
218+
string userEmail,
219+
string role,
220+
string[] expectedPermissions)
218221
{
219-
// App is fully initialized here, including database
220-
var dbContext = App.Services.GetRequiredService<AppDbContext>();
221-
222-
// Create test users
223-
var adminUser = new User { Email = "admin@test.com", Role = "Admin" };
224-
var regularUser = new User { Email = "user@test.com", Role = "User" };
225-
226-
dbContext.Users.AddRange(adminUser, regularUser);
227-
await dbContext.SaveChangesAsync();
228-
229-
yield return new UserTestData
230-
{
231-
User = adminUser,
232-
App = App,
233-
ExpectedPermissions = new[] { "read", "write", "delete" }
234-
};
235-
236-
yield return new UserTestData
237-
{
238-
User = regularUser,
239-
App = App,
240-
ExpectedPermissions = new[] { "read" }
241-
};
222+
// Create user through API
223+
var createResponse = await app.Client.PostAsJsonAsync("/api/users",
224+
new { Email = userEmail, Role = role });
225+
createResponse.EnsureSuccessStatusCode();
226+
227+
// Verify permissions
228+
var permissionsResponse = await app.Client.GetAsync($"/api/users/{userEmail}/permissions");
229+
var permissions = await permissionsResponse.Content.ReadFromJsonAsync<string[]>();
230+
231+
await Assert.That(permissions).IsEquivalentTo(expectedPermissions);
232+
}
233+
234+
private static IEnumerable<(string Email, string Role, string[] Permissions)> UserScenarios()
235+
{
236+
yield return ("admin@test.com", "Admin", new[] { "read", "write", "delete" });
237+
yield return ("user@test.com", "User", new[] { "read" });
238+
yield return ("guest@test.com", "Guest", Array.Empty<string>());
242239
}
243240
}
244241
```
245242

243+
For more complex scenarios where you need to query the initialized application during data generation, you can access it through the test context metadata.
244+
246245
## How It Works
247246

248247
1. TUnit detects properties marked with data source attributes (like `[ClassDataSource<T>]`)

docs/examples/nested-data-sources-example.md

Lines changed: 64 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -178,27 +178,13 @@ public class TestApplication : IAsyncInitializer, IAsyncDisposable
178178
}
179179
```
180180

181-
### 3. Create a Data Source Attribute
181+
### 3. Write Integration Tests
182182

183183
```csharp
184-
// This attribute will provide a fully initialized TestApplication to tests
185-
public class TestApplicationAttribute : DataSourceGeneratorAttribute<TestApplication>
186-
{
187-
public override IEnumerable<TestApplication> GenerateDataSources(DataGeneratorMetadata metadata)
188-
{
189-
// Return a single instance that will be initialized by TUnit
190-
yield return new TestApplication();
191-
}
192-
}
193-
194-
### 4. Write Integration Tests
195-
196-
```csharp
197-
[TestClass]
198184
public class UserApiIntegrationTests
199185
{
200186
[Test]
201-
[TestApplication]
187+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
202188
public async Task CreateUser_Should_Store_In_Database(TestApplication app)
203189
{
204190
// Arrange
@@ -224,7 +210,7 @@ public class UserApiIntegrationTests
224210
}
225211

226212
[Test]
227-
[TestApplication]
213+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
228214
public async Task CreateUser_Should_Cache_In_Redis(TestApplication app)
229215
{
230216
// Arrange
@@ -247,135 +233,101 @@ public class UserApiIntegrationTests
247233
}
248234
}
249235

250-
### 5. Advanced Scenario: Custom Test Data with Initialized Context
236+
### 4. Advanced Scenario: Parameterized Tests with Seeded Data
237+
238+
For parameterized tests that need pre-seeded data, combine `ClassDataSource` with `MethodDataSource`:
251239

252240
```csharp
253-
// Data source that provides test scenarios with pre-populated data
254-
public class OrderTestScenarioAttribute : AsyncDataSourceGeneratorAttribute<OrderTestScenario>
241+
public class OrderProcessingTests
255242
{
256-
[ClassDataSource<TestApplication>]
257-
public required TestApplication App { get; init; }
258-
259-
public override async IAsyncEnumerable<OrderTestScenario> GenerateDataSourcesAsync(DataGeneratorMetadata metadata)
260-
{
261-
// App is already initialized here with Redis and SQL Server running
262-
263-
// Seed test data
264-
var customerId = await CreateTestCustomer();
265-
var productIds = await CreateTestProducts();
266-
267-
yield return new OrderTestScenario
268-
{
269-
Name = "Valid order with single item",
270-
App = App,
271-
CustomerId = customerId,
272-
OrderItems = new[]
273-
{
274-
new OrderItem { ProductId = productIds[0], Quantity = 1 }
275-
},
276-
ExpectedTotal = 29.99m
277-
};
278-
279-
yield return new OrderTestScenario
280-
{
281-
Name = "Valid order with multiple items",
282-
App = App,
283-
CustomerId = customerId,
284-
OrderItems = new[]
285-
{
286-
new OrderItem { ProductId = productIds[0], Quantity = 2 },
287-
new OrderItem { ProductId = productIds[1], Quantity = 1 }
288-
},
289-
ExpectedTotal = 89.97m
290-
};
291-
292-
yield return new OrderTestScenario
293-
{
294-
Name = "Order exceeding stock",
295-
App = App,
296-
CustomerId = customerId,
297-
OrderItems = new[]
298-
{
299-
new OrderItem { ProductId = productIds[0], Quantity = 1000 }
300-
},
301-
ExpectedException = typeof(InsufficientStockException)
302-
};
303-
}
304-
305-
private async Task<Guid> CreateTestCustomer()
243+
// Helper method to seed test data
244+
private static async Task<(Guid CustomerId, Guid[] ProductIds)> SeedTestData(TestApplication app)
306245
{
307-
using var scope = App.Services.CreateScope();
246+
using var scope = app.Services.CreateScope();
308247
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
309-
248+
249+
// Create test customer
310250
var customer = new Customer
311251
{
312252
Id = Guid.NewGuid(),
313253
Name = "Test Customer",
314254
Email = "customer@test.com"
315255
};
316-
317256
dbContext.Customers.Add(customer);
318-
await dbContext.SaveChangesAsync();
319-
320-
return customer.Id;
321-
}
322-
323-
private async Task<Guid[]> CreateTestProducts()
324-
{
325-
using var scope = App.Services.CreateScope();
326-
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
327-
257+
258+
// Create test products
328259
var products = new[]
329260
{
330261
new Product { Id = Guid.NewGuid(), Name = "Widget", Price = 29.99m, Stock = 100 },
331262
new Product { Id = Guid.NewGuid(), Name = "Gadget", Price = 59.99m, Stock = 50 }
332263
};
333-
334264
dbContext.Products.AddRange(products);
265+
335266
await dbContext.SaveChangesAsync();
336-
337-
return products.Select(p => p.Id).ToArray();
267+
268+
return (customer.Id, products.Select(p => p.Id).ToArray());
338269
}
339-
}
340270

341-
public class OrderTestScenario
342-
{
343-
public required string Name { get; init; }
344-
public required TestApplication App { get; init; }
345-
public required Guid CustomerId { get; init; }
346-
public required OrderItem[] OrderItems { get; init; }
347-
public decimal? ExpectedTotal { get; init; }
348-
public Type? ExpectedException { get; init; }
349-
}
271+
// Provide test scenarios
272+
private static IEnumerable<(string Name, Func<Guid, Guid[], OrderItem[]> Items, decimal? ExpectedTotal, bool ShouldFail)> OrderScenarios()
273+
{
274+
yield return (
275+
"Valid order with single item",
276+
(customerId, productIds) => new[] { new OrderItem { ProductId = productIds[0], Quantity = 1 } },
277+
29.99m,
278+
false
279+
);
280+
281+
yield return (
282+
"Valid order with multiple items",
283+
(customerId, productIds) => new[]
284+
{
285+
new OrderItem { ProductId = productIds[0], Quantity = 2 },
286+
new OrderItem { ProductId = productIds[1], Quantity = 1 }
287+
},
288+
89.97m,
289+
false
290+
);
291+
292+
yield return (
293+
"Order exceeding stock",
294+
(customerId, productIds) => new[] { new OrderItem { ProductId = productIds[0], Quantity = 1000 } },
295+
null,
296+
true
297+
);
298+
}
350299

351-
// Use the test scenarios
352-
[TestClass]
353-
public class OrderProcessingTests
354-
{
355300
[Test]
356-
[OrderTestScenario]
357-
public async Task ProcessOrder_Scenarios(OrderTestScenario scenario)
301+
[ClassDataSource<TestApplication>(Shared = SharedType.PerClass)]
302+
[MethodDataSource(nameof(OrderScenarios))]
303+
public async Task ProcessOrder_Scenarios(
304+
TestApplication app,
305+
string scenarioName,
306+
Func<Guid, Guid[], OrderItem[]> itemsFactory,
307+
decimal? expectedTotal,
308+
bool shouldFail)
358309
{
359-
// Arrange
310+
// Arrange - Seed data first
311+
var (customerId, productIds) = await SeedTestData(app);
360312
var orderRequest = new CreateOrderRequest
361313
{
362-
CustomerId = scenario.CustomerId,
363-
Items = scenario.OrderItems
314+
CustomerId = customerId,
315+
Items = itemsFactory(customerId, productIds)
364316
};
365-
317+
366318
// Act
367-
var response = await scenario.App.Client.PostAsJsonAsync("/api/orders", orderRequest);
368-
319+
var response = await app.Client.PostAsJsonAsync("/api/orders", orderRequest);
320+
369321
// Assert
370-
if (scenario.ExpectedException != null)
322+
if (shouldFail)
371323
{
372-
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
324+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
373325
}
374326
else
375327
{
376-
response.StatusCode.Should().Be(HttpStatusCode.Created);
328+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Created);
377329
var order = await response.Content.ReadFromJsonAsync<OrderResponse>();
378-
order!.Total.Should().Be(scenario.ExpectedTotal);
330+
await Assert.That(order!.Total).IsEqualTo(expectedTotal);
379331
}
380332
}
381333
}

0 commit comments

Comments
 (0)