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.
389 lines
11 KiB
C#
389 lines
11 KiB
C#
using Before;
|
|
|
|
namespace BeforeAfter.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for the Client, Order, DbBase, DbContext, and WebApp classes.
|
|
/// </summary>
|
|
public class BeforeTests
|
|
{
|
|
// Client and Order derive from DbBase.
|
|
[Fact]
|
|
public void Client_and_Order_derive_from_DbBase()
|
|
{
|
|
Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Client)));
|
|
Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Order)));
|
|
}
|
|
|
|
// New entities have Id == 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);
|
|
}
|
|
|
|
// Save assigns fresh, distinct Guids to unsaved entities.
|
|
[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);
|
|
}
|
|
|
|
// A saved entity references its context and is tracked by it.
|
|
[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));
|
|
}
|
|
|
|
// Adding an order to client.Orders sets order.Client.
|
|
[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);
|
|
}
|
|
|
|
// A client exposes the context it was attached to.
|
|
[Fact]
|
|
public void A_client_can_reach_its_DbContext()
|
|
{
|
|
var db = new DbContext();
|
|
var client = new Client { Name = "Acme" };
|
|
db.Attach(client);
|
|
db.Save();
|
|
|
|
Assert.NotNull(client.DbContext);
|
|
Assert.Same(db, client.DbContext);
|
|
Assert.True(client.DbContext!.IsTracked(client));
|
|
}
|
|
|
|
// WebApp works on domain classes directly.
|
|
[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();
|
|
|
|
var view = app.ShowClient(client);
|
|
Assert.Contains("Acme", view);
|
|
Assert.Contains("order one", view);
|
|
|
|
Assert.True(app.IsPersisted(client));
|
|
}
|
|
|
|
// A bare client has no context until saved.
|
|
[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));
|
|
}
|
|
|
|
// DbBase exposes a static, shared Context reference.
|
|
[Fact]
|
|
public void DbBase_has_a_static_Context_singleton()
|
|
{
|
|
Assert.Null(DbBase.Context);
|
|
|
|
var db = new DbContext();
|
|
DbBase.Context = db;
|
|
|
|
Assert.Same(db, DbBase.Context);
|
|
|
|
DbBase.Context = null; // cleanup
|
|
}
|
|
|
|
// DbBase.Find returns the first tracked entity of type T matching the predicate.
|
|
[Fact]
|
|
public void DbBase_Find_finds_matching_entity()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "Acme" };
|
|
var globex = new Client { Name = "Globex" };
|
|
db.Attach(acme);
|
|
db.Attach(globex);
|
|
|
|
var result = DbBase.Find<Client>(c => c.Name == "Globex", db);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Same(globex, result);
|
|
}
|
|
|
|
// DbBase.Find returns null when there is no match.
|
|
[Fact]
|
|
public void DbBase_Find_returns_null_when_no_match()
|
|
{
|
|
var db = new DbContext();
|
|
db.Attach(new Client { Name = "Acme" });
|
|
|
|
var result = DbBase.Find<Client>(c => c.Name == "Nobody", db);
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
// DbBase.Find only matches entities of type T.
|
|
[Fact]
|
|
public void DbBase_Find_ignores_non_matching_types()
|
|
{
|
|
var db = new DbContext();
|
|
db.Attach(new Order { Description = "test" });
|
|
|
|
var result = DbBase.Find<Client>(_ => true, db);
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
// DbBase.Find returns the first match only.
|
|
[Fact]
|
|
public void DbBase_Find_returns_first_match()
|
|
{
|
|
var db = new DbContext();
|
|
var first = new Client { Name = "SameName" };
|
|
var second = new Client { Name = "SameName" };
|
|
db.Attach(first);
|
|
db.Attach(second);
|
|
|
|
var result = DbBase.Find<Client>(c => c.Name == "SameName", db);
|
|
|
|
Assert.Same(first, result); // first one inserted wins
|
|
Assert.NotSame(second, result);
|
|
}
|
|
|
|
// DbBase.Find returns null when no context is available.
|
|
[Fact]
|
|
public void DbBase_Find_with_null_context_returns_default()
|
|
{
|
|
var result = DbBase.Find<Client>(_ => true, null);
|
|
Assert.Null(result);
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "SingletonCo" };
|
|
db.Attach(acme);
|
|
|
|
DbBase.Context = db;
|
|
try
|
|
{
|
|
var result = DbBase.Find<Client>(c => c.Name == "SingletonCo", null);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Same(acme, result);
|
|
}
|
|
finally
|
|
{
|
|
DbBase.Context = null; // cleanup
|
|
}
|
|
}
|
|
|
|
// Client.FindByName delegates to DbBase.Find.
|
|
[Fact]
|
|
public void Client_FindByName_finds_by_name()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "Acme Corp" };
|
|
db.Attach(acme);
|
|
|
|
var result = Client.FindByName("Acme Corp", db);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Same(acme, result);
|
|
}
|
|
|
|
// Client.FindByName returns null when there is no match.
|
|
[Fact]
|
|
public void Client_FindByName_returns_null_when_not_found()
|
|
{
|
|
var db = new DbContext();
|
|
db.Attach(new Client { Name = "Acme Corp" });
|
|
|
|
var result = Client.FindByName("Nobody", db);
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
// Client.FindByName uses DbBase.Context when db is omitted.
|
|
[Fact]
|
|
public void Client_FindByName_uses_Context_singleton_when_db_omitted()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "Acme Corp" };
|
|
db.Attach(acme);
|
|
|
|
DbBase.Context = db;
|
|
try
|
|
{
|
|
var result = Client.FindByName("Acme Corp");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Same(acme, result);
|
|
}
|
|
finally
|
|
{
|
|
DbBase.Context = null; // cleanup
|
|
}
|
|
}
|
|
|
|
// DbBase.Context can be passed as the context argument to Find.
|
|
[Fact]
|
|
public void DbBase_Context_singleton_is_used_in_Find()
|
|
{
|
|
var db = new DbContext();
|
|
var targeted = new Client { Name = "TargetCo" };
|
|
db.Attach(targeted);
|
|
|
|
DbBase.Context = db;
|
|
|
|
var queryResult = DbBase.Find<Client>(c => c.Name == "TargetCo", DbBase.Context);
|
|
|
|
Assert.NotNull(queryResult);
|
|
Assert.Same(targeted, queryResult);
|
|
|
|
DbBase.Context = null; // cleanup
|
|
}
|
|
|
|
// DbBase.FindRequired returns the matching entity.
|
|
[Fact]
|
|
public void DbBase_FindRequired_T_finds_matching_entity()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "Acme" };
|
|
var globex = new Client { Name = "Globex" };
|
|
db.Attach(acme);
|
|
db.Attach(globex);
|
|
|
|
var result = DbBase.FindRequired<Client>(c => c.Name == "Globex", db, "name == Globex");
|
|
|
|
Assert.Same(globex, result);
|
|
}
|
|
|
|
// DbBase.FindRequired throws ObjectNotFoundException when there is no match.
|
|
[Fact]
|
|
public void DbBase_FindRequired_T_throws_when_no_match()
|
|
{
|
|
var db = new DbContext();
|
|
db.Attach(new Client { Name = "Acme" });
|
|
|
|
var ex = Assert.Throws<ObjectNotFoundException>(() =>
|
|
DbBase.FindRequired<Client>(c => c.Name == "Nobody", db, "name == Nobody"));
|
|
|
|
Assert.Contains("Nobody", ex.Message);
|
|
Assert.Contains("no matching Client found", ex.Message);
|
|
}
|
|
|
|
// DbBase.FindRequired throws InvalidOperationException when no context is
|
|
// configured — InvalidOperationException, not ObjectNotFoundException.
|
|
[Fact]
|
|
public void DbBase_FindRequired_T_throws_when_no_context_configured()
|
|
{
|
|
DbBase.Context = null;
|
|
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
DbBase.FindRequired<Client>(_ => true, null, "true"));
|
|
|
|
Assert.Contains("no DbContext configured", ex.Message);
|
|
Assert.Contains("DbBase.Context", ex.Message);
|
|
}
|
|
|
|
// Client.FindByNameRequired returns the matching client.
|
|
[Fact]
|
|
public void Client_FindByNameRequired_finds_by_name()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "Acme Corp" };
|
|
db.Attach(acme);
|
|
|
|
var result = Client.FindByNameRequired("Acme Corp", db);
|
|
|
|
Assert.Same(acme, result);
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
var db = new DbContext();
|
|
db.Attach(new Client { Name = "Acme Corp" });
|
|
|
|
var ex = Assert.Throws<ObjectNotFoundException>(() =>
|
|
Client.FindByNameRequired("Nobody", db));
|
|
|
|
Assert.Contains("Nobody", ex.Message);
|
|
Assert.Contains("no matching Client found", ex.Message);
|
|
}
|
|
|
|
// Client.FindByNameRequired uses DbBase.Context when no context is passed.
|
|
[Fact]
|
|
public void Client_FindByNameRequired_uses_Context_singleton()
|
|
{
|
|
var db = new DbContext();
|
|
var acme = new Client { Name = "SingletonCo" };
|
|
db.Attach(acme);
|
|
|
|
DbBase.Context = db;
|
|
try
|
|
{
|
|
var result = Client.FindByNameRequired("SingletonCo");
|
|
Assert.Same(acme, result);
|
|
}
|
|
finally
|
|
{
|
|
DbBase.Context = null;
|
|
}
|
|
}
|
|
|
|
// Client.FindByNameRequired throws InvalidOperationException when no
|
|
// context is configured.
|
|
[Fact]
|
|
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
|
|
{
|
|
DbBase.Context = null;
|
|
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
Client.FindByNameRequired("SomeBody"));
|
|
|
|
Assert.Contains("no DbContext configured", ex.Message);
|
|
Assert.Contains("DbBase.Context", ex.Message);
|
|
}
|
|
}
|