Files
db-subclass-to-dto/tests/BeforeAfter.Tests/BeforeTests.cs
T
mostalive 463245288a Yak: csharp before situation (Yak 01) — DB-backed domain classes
Add the C# BEFORE situation: a fake-EF DbContext + DbBase that Client/Order
inherit, the active-record DbContext back-reference (the dependency leak),
navigation fix-up on Client.Orders, and a WebApp that consumes the domain
classes directly. Adds an empty After project so the solution shape is final,
and an xUnit BeforeTests suite covering the required assertions.

- db-subclass-to-dto.sln (classic .sln; SDK 10 defaults to .slnx which breaks run-tests.sh)
- src/Before: DbBase, DbContext, Client (+ClientOrders fix-up), Order, WebApp
- src/After: empty placeholder (filled in Yak 02)
- tests/BeforeAfter.Tests/BeforeTests.cs: 8 tests (7 required + 1 companion)
2026-09-11 13:03:09 +01:00

145 lines
5.3 KiB
C#

using Before;
namespace BeforeAfter.Tests;
/// <summary>
/// Tests for the BEFORE situation (Yak 01): DB-backed domain classes that
/// inherit <see cref="DbBase"/> and leak their <see cref="DbContext"/>.
/// </summary>
public class BeforeTests
{
// ---------------------------------------------------------------------
// (1) Inheritance: the domain classes ARE DB-backed (they derive from
// DbBase). This is the "before" shape Yak 02 will undo.
// ---------------------------------------------------------------------
[Fact]
public void Client_and_Order_derive_from_DbBase()
{
Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Client)));
Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Order)));
}
// ---------------------------------------------------------------------
// (2) Fresh entities are unsaved: their Id is still Guid.Empty.
// ---------------------------------------------------------------------
[Theory]
[InlineData(typeof(Client))]
[InlineData(typeof(Order))]
public void New_entities_have_an_empty_id(Type type)
{
var entity = (DbBase)Activator.CreateInstance(type)!;
Assert.Equal(Guid.Empty, entity.Id);
}
// ---------------------------------------------------------------------
// (3) Save() fakes EF's SaveChanges: it assigns fresh, distinct Guids to
// entities that do not have one yet.
// ---------------------------------------------------------------------
[Fact]
public void Save_assigns_fresh_distinct_ids()
{
var db = new DbContext();
var acme = new Client { Name = "Acme" };
var globex = new Client { Name = "Globex" };
db.Attach(acme);
db.Attach(globex);
Assert.Equal(2, db.Save());
Assert.NotEqual(Guid.Empty, acme.Id);
Assert.NotEqual(Guid.Empty, globex.Id);
Assert.NotEqual(acme.Id, globex.Id);
}
// ---------------------------------------------------------------------
// (4) A saved entity is registered with its context, reachable via the
// active-record back-reference.
// ---------------------------------------------------------------------
[Fact]
public void Saved_entity_is_registered_with_its_context()
{
var db = new DbContext();
var client = new Client { Name = "Acme" };
db.Attach(client);
db.Save();
Assert.Same(db, client.DbContext);
Assert.True(db.IsTracked(client));
}
// ---------------------------------------------------------------------
// (5) Navigation fix-up: adding an order to client.Orders wires the
// order.Client back-reference.
// ---------------------------------------------------------------------
[Fact]
public void Adding_an_order_sets_the_client_back_reference()
{
var client = new Client { Name = "Acme" };
var order = new Order { Description = "first order" };
client.Orders.Add(order);
Assert.Same(client, order.Client);
Assert.Same(order, client.Orders[0]);
Assert.Single(client.Orders);
}
// ---------------------------------------------------------------------
// (6) THE LEAK: from a plain Client you can reach its DbContext.
// This is exactly what the "after" situation removes: a ClientDto has
// no DbContext to reach, so a consumer can never touch the persistence
// layer through it.
// ---------------------------------------------------------------------
[Fact]
public void A_client_can_reach_its_DbContext__the_leak()
{
var db = new DbContext();
var client = new Client { Name = "Acme" };
db.Attach(client);
db.Save();
// Reaching the persistence layer *through* the domain object:
Assert.NotNull(client.DbContext);
Assert.Same(db, client.DbContext);
Assert.True(client.DbContext!.IsTracked(client));
}
// ---------------------------------------------------------------------
// (7) The WebApp consumes the domain classes directly — including a method
// that rides the leak.
// ---------------------------------------------------------------------
[Fact]
public void WebApp_works_on_the_domain_classes_directly()
{
var app = new WebApp();
var db = new DbContext();
var client = new Client { Name = "Acme" };
var order = new Order { Description = "order one" };
client.Orders.Add(order);
db.Attach(client);
db.Attach(order);
db.Save();
// ShowClient consumes Client and its Orders directly:
var view = app.ShowClient(client);
Assert.Contains("Acme", view);
Assert.Contains("order one", view);
// IsPersisted rides the leak (client.DbContext) — and it is true here
// because the client was saved through the context:
Assert.True(app.IsPersisted(client));
}
// A companion check: a client that was never attached/saved exposes no
// context yet, so the leak is dormant until the entity meets a context.
[Fact]
public void A_bare_client_has_no_context_until_saved()
{
var app = new WebApp();
var client = new Client { Name = "Nobody" };
Assert.Null(client.DbContext);
Assert.False(app.IsPersisted(client));
}
}