rewrite comments in neutral, terse style
Doc comments, inline comments, and README no longer editorialize about the code (ActiveRecord, leak, fake, Yak before/after). They now describe behavior only. Test A_client_can_reach_its_DbContext__the_leak renamed to A_client_can_reach_its_DbContext. No behavior changes; all 27 tests pass.
This commit is contained in:
@@ -3,15 +3,11 @@ 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"/>.
|
||||
/// Tests for the Client, Order, DbBase, DbContext, and WebApp classes.
|
||||
/// </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.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client and Order derive from DbBase.
|
||||
[Fact]
|
||||
public void Client_and_Order_derive_from_DbBase()
|
||||
{
|
||||
@@ -19,9 +15,7 @@ public class BeforeTests
|
||||
Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Order)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (2) Fresh entities are unsaved: their Id is still Guid.Empty.
|
||||
// ---------------------------------------------------------------------
|
||||
// New entities have Id == Guid.Empty.
|
||||
[Theory]
|
||||
[InlineData(typeof(Client))]
|
||||
[InlineData(typeof(Order))]
|
||||
@@ -31,10 +25,7 @@ public class BeforeTests
|
||||
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.
|
||||
// ---------------------------------------------------------------------
|
||||
// Save assigns fresh, distinct Guids to unsaved entities.
|
||||
[Fact]
|
||||
public void Save_assigns_fresh_distinct_ids()
|
||||
{
|
||||
@@ -51,10 +42,7 @@ public class BeforeTests
|
||||
Assert.NotEqual(acme.Id, globex.Id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (4) A saved entity is registered with its context, reachable via the
|
||||
// active-record back-reference.
|
||||
// ---------------------------------------------------------------------
|
||||
// A saved entity references its context and is tracked by it.
|
||||
[Fact]
|
||||
public void Saved_entity_is_registered_with_its_context()
|
||||
{
|
||||
@@ -67,10 +55,7 @@ public class BeforeTests
|
||||
Assert.True(db.IsTracked(client));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (5) Navigation fix-up: adding an order to client.Orders wires the
|
||||
// order.Client back-reference.
|
||||
// ---------------------------------------------------------------------
|
||||
// Adding an order to client.Orders sets order.Client.
|
||||
[Fact]
|
||||
public void Adding_an_order_sets_the_client_back_reference()
|
||||
{
|
||||
@@ -84,30 +69,21 @@ public class BeforeTests
|
||||
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.
|
||||
// ---------------------------------------------------------------------
|
||||
// A client exposes the context it was attached to.
|
||||
[Fact]
|
||||
public void A_client_can_reach_its_DbContext__the_leak()
|
||||
public void A_client_can_reach_its_DbContext()
|
||||
{
|
||||
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.
|
||||
// ---------------------------------------------------------------------
|
||||
// WebApp works on domain classes directly.
|
||||
[Fact]
|
||||
public void WebApp_works_on_the_domain_classes_directly()
|
||||
{
|
||||
@@ -121,17 +97,14 @@ public class BeforeTests
|
||||
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 bare client has no context until saved.
|
||||
// A bare client has no context until saved.
|
||||
[Fact]
|
||||
public void A_bare_client_has_no_context_until_saved()
|
||||
{
|
||||
@@ -142,29 +115,21 @@ public class BeforeTests
|
||||
Assert.False(app.IsPersisted(client));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// (8) Static Context (Singleton): DbBase exposes a class-level
|
||||
// shared DbContext. Setting it makes the context accessible
|
||||
// from any entity via its base type.
|
||||
// -----------------------------------------------------------------
|
||||
// DbBase exposes a static, shared Context reference.
|
||||
[Fact]
|
||||
public void DbBase_has_a_static_Context_singleton()
|
||||
{
|
||||
Assert.Null(DbBase.Context); // fresh — not set yet
|
||||
Assert.Null(DbBase.Context);
|
||||
|
||||
var db = new DbContext();
|
||||
DbBase.Context = db;
|
||||
|
||||
Assert.Same(db, DbBase.Context);
|
||||
|
||||
DbBase.Context = null; // cleanup
|
||||
DbBase.Context = null; // cleanup
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (9) ActiveRecord finder: DbBase.Find<T> walks the context's tracked
|
||||
// entities, looks for the first whose runtime type matches T,
|
||||
// and returns it when <paramref name="predicate"/> matches.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Find returns the first tracked entity of type T matching the predicate.
|
||||
[Fact]
|
||||
public void DbBase_Find_finds_matching_entity()
|
||||
{
|
||||
@@ -180,10 +145,7 @@ public class BeforeTests
|
||||
Assert.Same(globex, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (10) DbBase.Find returns null when no entity of type T satisfies
|
||||
// the predicate.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Find returns null when there is no match.
|
||||
[Fact]
|
||||
public void DbBase_Find_returns_null_when_no_match()
|
||||
{
|
||||
@@ -195,10 +157,7 @@ public class BeforeTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (11) DbBase.Find filters by runtime type — an Order attached to the
|
||||
// context does NOT match a Client predicate.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Find only matches entities of type T.
|
||||
[Fact]
|
||||
public void DbBase_Find_ignores_non_matching_types()
|
||||
{
|
||||
@@ -210,9 +169,7 @@ public class BeforeTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (12) DbBase.Find returns the FIRST matching entity only.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Find returns the first match only.
|
||||
[Fact]
|
||||
public void DbBase_Find_returns_first_match()
|
||||
{
|
||||
@@ -228,10 +185,7 @@ public class BeforeTests
|
||||
Assert.NotSame(second, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (13) DbBase.Find handles a null context gracefully — with no explicit
|
||||
// context AND no singleton configured, it returns default (null).
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Find returns null when no context is available.
|
||||
[Fact]
|
||||
public void DbBase_Find_with_null_context_returns_default()
|
||||
{
|
||||
@@ -239,8 +193,7 @@ public class BeforeTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// (13b) When no explicit context is passed, Find falls back to the
|
||||
// DbBase.Context singleton instead of returning null.
|
||||
// DbBase.Find falls back to DbBase.Context when db is null.
|
||||
[Fact]
|
||||
public void DbBase_Find_falls_back_to_Context_singleton_when_db_is_null()
|
||||
{
|
||||
@@ -262,9 +215,7 @@ public class BeforeTests
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (14) Client.FindByName delegates to DbBase.Find and works correctly.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByName delegates to DbBase.Find.
|
||||
[Fact]
|
||||
public void Client_FindByName_finds_by_name()
|
||||
{
|
||||
@@ -278,9 +229,7 @@ public class BeforeTests
|
||||
Assert.Same(acme, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (15) Client.FindByName returns null when the name does not match.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByName returns null when there is no match.
|
||||
[Fact]
|
||||
public void Client_FindByName_returns_null_when_not_found()
|
||||
{
|
||||
@@ -292,7 +241,7 @@ public class BeforeTests
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// (15b) FindByName with no context argument uses the Context singleton.
|
||||
// Client.FindByName uses DbBase.Context when db is omitted.
|
||||
[Fact]
|
||||
public void Client_FindByName_uses_Context_singleton_when_db_omitted()
|
||||
{
|
||||
@@ -314,12 +263,7 @@ public class BeforeTests
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (16) The class-level DbBase.Context singleton can be used as the
|
||||
// implicit context source for Find operations — the classic
|
||||
// ActiveRecord pattern where any entity method reaches the shared
|
||||
// context without parameter passing.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.Context can be passed as the context argument to Find.
|
||||
[Fact]
|
||||
public void DbBase_Context_singleton_is_used_in_Find()
|
||||
{
|
||||
@@ -329,9 +273,6 @@ public class BeforeTests
|
||||
|
||||
DbBase.Context = db;
|
||||
|
||||
// Query using the singleton instead of passing the context:
|
||||
// (In practice, callers often do this to avoid threading db through
|
||||
// every call — the whole point of the ActiveRecord leak.)
|
||||
var queryResult = DbBase.Find<Client>(c => c.Name == "TargetCo", DbBase.Context);
|
||||
|
||||
Assert.NotNull(queryResult);
|
||||
@@ -340,9 +281,7 @@ public class BeforeTests
|
||||
DbBase.Context = null; // cleanup
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (17) DbBase.FindRequired<T> finds the matching entity — same result as Find<T>.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.FindRequired returns the matching entity.
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_finds_matching_entity()
|
||||
{
|
||||
@@ -357,10 +296,7 @@ public class BeforeTests
|
||||
Assert.Same(globex, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (18) DbBase.FindRequired<T> throws ObjectNotFoundException when no match
|
||||
// is found (the domain-specific lookup miss, not a BCL exception).
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.FindRequired throws ObjectNotFoundException when there is no match.
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_throws_when_no_match()
|
||||
{
|
||||
@@ -374,13 +310,8 @@ public class BeforeTests
|
||||
Assert.Contains("no matching Client found", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (19) DbBase.FindRequired<T> throws with a useful message when no context is
|
||||
// configured (both explicit null and singleton null).
|
||||
//
|
||||
// Deliberately InvalidOperationException, not ObjectNotFoundException:
|
||||
// a missing context is a configuration error, not a lookup miss.
|
||||
// ---------------------------------------------------------------------
|
||||
// DbBase.FindRequired throws InvalidOperationException when no context is
|
||||
// configured — InvalidOperationException, not ObjectNotFoundException.
|
||||
[Fact]
|
||||
public void DbBase_FindRequired_T_throws_when_no_context_configured()
|
||||
{
|
||||
@@ -393,9 +324,7 @@ public class BeforeTests
|
||||
Assert.Contains("DbBase.Context", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (20) Client.FindByNameRequired finds the matching client by name.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByNameRequired returns the matching client.
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_finds_by_name()
|
||||
{
|
||||
@@ -408,10 +337,8 @@ public class BeforeTests
|
||||
Assert.Same(acme, result);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (21) Client.FindByNameRequired throws ObjectNotFoundException when no
|
||||
// match — message includes name.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByNameRequired throws ObjectNotFoundException when there is no
|
||||
// match; the message includes the name.
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_throws_with_name_when_not_found()
|
||||
{
|
||||
@@ -425,9 +352,7 @@ public class BeforeTests
|
||||
Assert.Contains("no matching Client found", ex.Message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (22) Client.FindByNameRequired uses Context singleton when no context arg.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByNameRequired uses DbBase.Context when no context is passed.
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_uses_Context_singleton()
|
||||
{
|
||||
@@ -447,11 +372,8 @@ public class BeforeTests
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (23) Client.FindByNameRequired throws with a useful message when the
|
||||
// singleton is not configured (no arg, no singleton).
|
||||
// Stays InvalidOperationException: configuration error, not a miss.
|
||||
// ---------------------------------------------------------------------
|
||||
// Client.FindByNameRequired throws InvalidOperationException when no
|
||||
// context is configured.
|
||||
[Fact]
|
||||
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
|
||||
{
|
||||
@@ -463,4 +385,4 @@ public class BeforeTests
|
||||
Assert.Contains("no DbContext configured", ex.Message);
|
||||
Assert.Contains("DbBase.Context", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user