From 08b52806de0a2ac2296cb1fb495646c3ea8913d8 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 16 Sep 2026 22:32:13 +0100 Subject: [PATCH] TestDataBuilders: ValidOrder()/ValidClient() with explicit Build(); After tests migrated Domain objects in After-side tests are constructed only through the builders, so the upcoming OrderLines evolution changes builder internals instead of every test. Before keeps object initializers (legacy world). yak: testdatabuilders-validordervalidclient-after-test-migration-91vl --- EXERCISE.md | 17 +++--- .../AfterPersistenceTests.cs | 40 +++++++------- tests/BeforeAfter.Tests/AfterTests.cs | 53 ++++++++++--------- .../DiscountCalculatorTests.cs | 26 ++++----- .../TestData/ClientBuilder.cs | 40 ++++++++++++++ .../TestData/OrderBuilder.cs | 47 ++++++++++++++++ 6 files changed, 158 insertions(+), 65 deletions(-) create mode 100644 tests/BeforeAfter.Tests/TestData/ClientBuilder.cs create mode 100644 tests/BeforeAfter.Tests/TestData/OrderBuilder.cs diff --git a/EXERCISE.md b/EXERCISE.md index 1889365..19ba25a 100644 --- a/EXERCISE.md +++ b/EXERCISE.md @@ -22,6 +22,11 @@ explicit — and that is where this exercise lives. percentage *actually applied* (0 when the total does not strictly exceed the barrier), and the discounted total. Rounding is half-up, in integer math. - Unit tests for all of the above already pass. +- `ValidOrder()` / `ValidClient()` test data builders in the test project + (`tests/BeforeAfter.Tests/TestData/`) — construct domain objects in tests + through them, so tests survive domain evolution. Import their static + factories with `using static BeforeAfter.Tests.TestData.OrderBuilder;` + (and `...ClientBuilder;`). ## The challenge @@ -61,6 +66,8 @@ instead of assuming there is only one order around. ```csharp using After; +using static BeforeAfter.Tests.TestData.ClientBuilder; +using static BeforeAfter.Tests.TestData.OrderBuilder; namespace BeforeAfter.Tests; @@ -74,9 +81,8 @@ public class DiscountedOrderMappingTests public void A_discounted_order_can_be_correlated_onto_its_order() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "big order", TotalInCents = 150_00 }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("big order").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); @@ -98,9 +104,8 @@ public class DiscountedOrderMappingTests public void Saving_a_discounted_order_keeps_the_stored_total_untouched() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "big order", TotalInCents = 150_00 }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("big order").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); diff --git a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs index eb859a3..08db56b 100644 --- a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs +++ b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs @@ -1,4 +1,6 @@ using After; +using static BeforeAfter.Tests.TestData.ClientBuilder; +using static BeforeAfter.Tests.TestData.OrderBuilder; namespace BeforeAfter.Tests; @@ -15,9 +17,8 @@ public class AfterPersistenceTests public void Save_maps_domain_state_into_data_records() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one", TotalInCents = 15000 }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); @@ -29,7 +30,7 @@ public class AfterPersistenceTests var orderRow = Assert.Single(db.Orders); Assert.Equal(order.Id, orderRow.Id); Assert.Equal("one", orderRow.Description); - Assert.Equal(15000, orderRow.TotalInCents); + Assert.Equal(150_00, orderRow.TotalInCents); Assert.Equal(client.Id, orderRow.ClientId); // the graph is flattened to a key } @@ -37,7 +38,7 @@ public class AfterPersistenceTests public void An_order_without_client_stores_an_empty_client_id() { var db = new DbContext(); - var order = new Order { Description = "orphan" }; + var order = ValidOrder().WithDescription("orphan").Build(); db.Save(order); @@ -48,14 +49,13 @@ public class AfterPersistenceTests public void Saving_again_refreshes_the_row_instead_of_duplicating_it() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); order.Description = "one (edited)"; - order.TotalInCents = 9900; + order.TotalInCents = 99_00; client.Name = "Acme GmbH"; db.Save(order); db.Save(client); @@ -64,7 +64,7 @@ public class AfterPersistenceTests var orderRow = Assert.Single(db.Orders); Assert.Equal(order.Id, orderRow.Id); Assert.Equal("one (edited)", orderRow.Description); - Assert.Equal(9900, orderRow.TotalInCents); + Assert.Equal(99_00, orderRow.TotalInCents); Assert.Equal(client.Id, orderRow.ClientId); var clientRow = Assert.Single(db.Clients); @@ -75,9 +75,8 @@ public class AfterPersistenceTests public void Rows_are_reachable_through_Set_and_Find() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); @@ -98,9 +97,8 @@ public class AfterPersistenceTests public void Loading_an_order_restores_its_state_and_its_client() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one", TotalInCents = 150_00 }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); db.Save(client); db.Save(order); @@ -122,12 +120,12 @@ public class AfterPersistenceTests public void Loading_a_client_restores_it_with_all_of_its_orders() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - client.Orders.Add(new Order { Description = "one", TotalInCents = 150_00 }); - client.Orders.Add(new Order { Description = "two", TotalInCents = 250_00 }); + var first = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); + var second = ValidOrder().WithDescription("two").WithTotal(250_00).Build(); + var client = ValidClient().WithName("Acme").WithOrder(first).WithOrder(second).Build(); db.Save(client); - db.Save(client.Orders[0]); - db.Save(client.Orders[1]); + db.Save(first); + db.Save(second); var loaded = db.LoadClient(client.Id); diff --git a/tests/BeforeAfter.Tests/AfterTests.cs b/tests/BeforeAfter.Tests/AfterTests.cs index 5e78e81..b703456 100644 --- a/tests/BeforeAfter.Tests/AfterTests.cs +++ b/tests/BeforeAfter.Tests/AfterTests.cs @@ -1,4 +1,6 @@ using After; +using static BeforeAfter.Tests.TestData.ClientBuilder; +using static BeforeAfter.Tests.TestData.OrderBuilder; using System.Reflection; namespace BeforeAfter.Tests; @@ -7,6 +9,11 @@ namespace BeforeAfter.Tests; /// Tests for the AFTER situation (Yak 02): plain domain objects (no /// ), DTOs (records) carrying the data including the /// relations, hand-written mappers, and a WebApp that consumes only DTOs. +/// +/// Domain objects in these tests are constructed through the TestDataBuilders +/// ( / ) +/// so the tests survive domain evolution; DTO initializers stay literal — +/// DTOs are the transport shapes under test. /// public class AfterTests { @@ -42,7 +49,7 @@ public class AfterTests public void ToClientDto_maps_all_scalar_fields() { var id = Guid.NewGuid(); - var client = new Client { Id = id, Name = "Acme" }; + var client = ValidClient().WithId(id).WithName("Acme").Build(); var dto = Mappers.ToClientDto(client); @@ -54,13 +61,13 @@ public class AfterTests public void ToOrderDto_maps_all_scalar_fields() { var id = Guid.NewGuid(); - var order = new Order { Id = id, Description = "one", TotalInCents = 15000 }; + var order = ValidOrder().WithId(id).WithDescription("one").WithTotal(150_00).Build(); var dto = Mappers.ToOrderDto(order); Assert.Equal(id, dto.Id); Assert.Equal("one", dto.Description); - Assert.Equal(15000, dto.TotalInCents); + Assert.Equal(150_00, dto.TotalInCents); } [Fact] @@ -79,13 +86,13 @@ public class AfterTests public void FromOrderDto_maps_all_scalar_fields() { var id = Guid.NewGuid(); - var dto = new OrderDto { Id = id, Description = "one", TotalInCents = 15000 }; + var dto = new OrderDto { Id = id, Description = "one", TotalInCents = 150_00 }; var order = Mappers.FromOrderDto(dto); Assert.Equal(id, order.Id); Assert.Equal("one", order.Description); - Assert.Equal(15000, order.TotalInCents); + Assert.Equal(150_00, order.TotalInCents); } // --------------------------------------------------------------------- @@ -95,9 +102,8 @@ public class AfterTests [Fact] public void ToClientDto_maps_orders_and_keeps_the_back_reference_consistent() { - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); var dto = Mappers.ToClientDto(client); var orderDto = Assert.Single(dto.Orders); @@ -130,9 +136,8 @@ public class AfterTests [Fact] public void Domain_round_trip_preserves_all_state() { - var client = new Client { Id = Guid.NewGuid(), Name = "Acme" }; - var order = new Order { Id = Guid.NewGuid(), Description = "one", TotalInCents = 15000 }; - client.Orders.Add(order); + var order = ValidOrder().WithId(Guid.NewGuid()).WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithId(Guid.NewGuid()).WithName("Acme").WithOrder(order).Build(); var roundTrip = Mappers.FromClientDto(Mappers.ToClientDto(client)); @@ -149,9 +154,8 @@ public class AfterTests [Fact] public void Dto_round_trip_preserves_all_state() { - var client = new Client { Id = Guid.NewGuid(), Name = "Acme" }; - var order = new Order { Id = Guid.NewGuid(), Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithId(Guid.NewGuid()).WithDescription("one").WithTotal(150_00).Build(); + var client = ValidClient().WithId(Guid.NewGuid()).WithName("Acme").WithOrder(order).Build(); var dto = Mappers.ToClientDto(client); var dtoOrder = dto.Orders[0]; @@ -172,9 +176,8 @@ public class AfterTests [Fact] public void ToClientDto_does_not_mutate_the_domain() { - var client = new Client { Id = Guid.NewGuid(), Name = "Acme" }; - var order = new Order { Id = Guid.NewGuid(), Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithId(Guid.NewGuid()).WithDescription("one").Build(); + var client = ValidClient().WithId(Guid.NewGuid()).WithName("Acme").WithOrder(order).Build(); var ordersCount = client.Orders.Count; Mappers.ToClientDto(client); @@ -247,9 +250,8 @@ public class AfterTests public void After_WebApp_consumes_dtos() { var app = new WebApp(); - var client = new Client { Id = Guid.NewGuid(), Name = "Acme" }; - var order = new Order { Id = Guid.NewGuid(), Description = "order one", TotalInCents = 15000 }; - client.Orders.Add(order); + var order = ValidOrder().WithId(Guid.NewGuid()).WithDescription("order one").WithTotal(150_00).Build(); + var client = ValidClient().WithId(Guid.NewGuid()).WithName("Acme").WithOrder(order).Build(); var view = app.ShowClient(Mappers.ToClientDto(client)); @@ -266,9 +268,8 @@ public class AfterTests public void DbContext_saves_plain_domain_objects_and_tracks_them() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; - var order = new Order { Description = "one" }; - client.Orders.Add(order); + var order = ValidOrder().WithDescription("one").Build(); + var client = ValidClient().WithName("Acme").WithOrder(order).Build(); Assert.Equal(Guid.Empty, client.Id); db.Save(client); @@ -286,7 +287,7 @@ public class AfterTests public void Saving_twice_does_not_reassign_the_id() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; + var client = ValidClient().WithName("Acme").Build(); db.Save(client); var id = client.Id; @@ -300,7 +301,7 @@ public class AfterTests public void A_saved_domain_object_keeps_no_reference_to_the_context() { var db = new DbContext(); - var client = new Client { Name = "Acme" }; + var client = ValidClient().WithName("Acme").Build(); db.Save(client); // Saved (the id was assigned by the context) ... @@ -313,4 +314,4 @@ public class AfterTests var dto = Mappers.ToClientDto(client); Assert.Equal(client.Id, dto.Id); // the id flows on into the DTO } -} \ No newline at end of file +} diff --git a/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs b/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs index aa3b2e3..0a6beee 100644 --- a/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs +++ b/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs @@ -1,4 +1,6 @@ using After; +using static BeforeAfter.Tests.TestData.ClientBuilder; +using static BeforeAfter.Tests.TestData.OrderBuilder; namespace BeforeAfter.Tests; @@ -16,7 +18,7 @@ public class DiscountCalculatorTests [Fact] public void A_total_below_the_barrier_gets_no_discount() { - var order = new Order { Id = Guid.NewGuid(), Description = "small", TotalInCents = 50_00 }; + var order = ValidOrder().WithTotal(50_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10); @@ -28,7 +30,7 @@ public class DiscountCalculatorTests [Fact] public void A_total_exactly_at_the_barrier_gets_no_discount() { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = 100_00 }; + var order = ValidOrder().WithTotal(100_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10); @@ -42,7 +44,7 @@ public class DiscountCalculatorTests [Fact] public void A_total_above_the_barrier_gets_the_percentage_discount() { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 }; + var order = ValidOrder().WithTotal(150_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10); @@ -54,7 +56,7 @@ public class DiscountCalculatorTests [Fact] public void A_zero_percentage_discounts_nothing_even_above_the_barrier() { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 }; + var order = ValidOrder().WithTotal(150_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 0); @@ -65,7 +67,7 @@ public class DiscountCalculatorTests [Fact] public void A_hundred_percent_discount_leaves_nothing() { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 }; + var order = ValidOrder().WithTotal(150_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 100); @@ -83,7 +85,7 @@ public class DiscountCalculatorTests [InlineData(151, 15, 136)] // 15.1 rounds down to 15 public void The_discount_amount_is_rounded_half_up(int totalInCents, int expectedDiscountCents, int expectedDiscountedTotal) { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = totalInCents }; + var order = ValidOrder().WithTotal(totalInCents).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100, discountPercentage: 10); @@ -98,7 +100,7 @@ public class DiscountCalculatorTests public void The_result_is_correlated_to_the_order_by_id() { var id = Guid.NewGuid(); - var order = new Order { Id = id, TotalInCents = 150_00 }; + var order = ValidOrder().WithId(id).WithTotal(150_00).Build(); var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10); @@ -108,7 +110,7 @@ public class DiscountCalculatorTests [Fact] public void Calculating_does_not_mutate_the_order() { - var order = new Order { Id = Guid.NewGuid(), Description = "one", TotalInCents = 150_00 }; + var order = ValidOrder().WithDescription("one").WithTotal(150_00).Build(); DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10); @@ -120,13 +122,13 @@ public class DiscountCalculatorTests // Input validation: invalid arguments throw (loudly, not silently). // --------------------------------------------------------------------- [Theory] - [InlineData(-1, 100_00, 10)] // negative total - [InlineData(150_00, -1, 10)] // negative barrier - [InlineData(150_00, 100_00, -1)]// percentage below 0 + [InlineData(-1, 100_00, 10)] // negative total + [InlineData(150_00, -1, 10)] // negative barrier + [InlineData(150_00, 100_00, -1)] // percentage below 0 [InlineData(150_00, 100_00, 101)]// percentage above 100 public void Invalid_arguments_throw(int totalInCents, int discountBarrierCents, int discountPercentage) { - var order = new Order { Id = Guid.NewGuid(), TotalInCents = totalInCents }; + var order = ValidOrder().WithTotal(totalInCents).Build(); Assert.Throws( () => DiscountCalculator.CalculateDiscount(order, discountBarrierCents, discountPercentage)); diff --git a/tests/BeforeAfter.Tests/TestData/ClientBuilder.cs b/tests/BeforeAfter.Tests/TestData/ClientBuilder.cs new file mode 100644 index 0000000..be16bc1 --- /dev/null +++ b/tests/BeforeAfter.Tests/TestData/ClientBuilder.cs @@ -0,0 +1,40 @@ +using After; + +namespace BeforeAfter.Tests.TestData; + +/// +/// Test data builder for a with its orders +/// (TestDataBuilder pattern). starts from a +/// fresh client, exactly what new Client() gives. +/// +public sealed class ClientBuilder +{ + private readonly Client _client = new(); + + /// A valid, fresh client — the neutral starting point for tests. + public static ClientBuilder ValidClient() => new(); + + public ClientBuilder WithId(Guid id) + { + _client.Id = id; + return this; + } + + public ClientBuilder WithName(string name) + { + _client.Name = name; + return this; + } + + /// Adds an order to the client (navigation fix-up wires the back-reference). + public ClientBuilder WithOrder(Order order) + { + _client.Orders.Add(order); + return this; + } + + /// Builds the given builder and adds its order — chaining sugar for .WithOrder(builder.Build()). + public ClientBuilder WithOrder(OrderBuilder order) => WithOrder(order.Build()); + + public Client Build() => _client; +} diff --git a/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs b/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs new file mode 100644 index 0000000..d9a33a6 --- /dev/null +++ b/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs @@ -0,0 +1,47 @@ +using After; + +namespace BeforeAfter.Tests.TestData; + +/// +/// Test data builder for an (TestDataBuilder pattern). +/// starts from a fresh order — +/// id, empty description, zero total, exactly what +/// new Order() gives — and every With... returns the builder for +/// chaining; hands back the order. +/// +/// This class is the only place tests touch an order's *construction*: when +/// the domain evolves (e.g. OrderLines making the total computed), only these +/// internals change and the tests keep reading the same. +/// +public sealed class OrderBuilder +{ + private readonly Order _order = new(); + + /// A valid, fresh order — the neutral starting point for tests. + public static OrderBuilder ValidOrder() => new(); + + public OrderBuilder WithId(Guid id) + { + _order.Id = id; + return this; + } + + public OrderBuilder WithDescription(string description) + { + _order.Description = description; + return this; + } + + /// + /// Sets the total in cents (150_00 = €150.00). How the requested + /// total is realised inside the order is an implementation detail that may + /// change as the domain evolves; this API stays. + /// + public OrderBuilder WithTotal(int totalInCents) + { + _order.TotalInCents = totalInCents; + return this; + } + + public Order Build() => _order; +}