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;
+}