Fakes

This page covers the in-memory test doubles: FakeRepository<T, TKey>, AsyncFakeRepository<T, TKey> and FakeScheduler.

This page covers the in-memory test doubles: FakeRepository<T, TKey>, AsyncFakeRepository<T, TKey> and FakeScheduler.

FakeRepository<T, TKey>

FakeRepository<T, TKey> is an in-memory implementation of IRepository<T, TKey> (from ArturRios.Data.Relational.Core). It stores entities in a backing list so you can exercise services that depend on a repository without touching a database. T must derive from ArturRios.Data.Relational.Core.Entities.Entity<TKey>, and TKey is the entity’s key type (long, int, Guid, string, …). Every method except Query() returns a DataOutput<...>, with the outcomes EfRepository<T, TKey> produces, so a test that passes against the fake passes for the reason it would in production: a lookup that finds nothing succeeds with no data, and an update or delete that would affect no row fails with RelationalErrors.ConcurrencyMessage and changes nothing.

MethodBehavior
Create(T)Assigns the entity’s id (see How ids are assigned), stores it, returns the id
GetById(TKey)Returns a successful output with the matching entity, or a successful output with null data when the id is unknown
GetAll()Returns a snapshot of every stored entity (later writes don’t change it)
Query()Exposes the stored entities as IQueryable<T>
Update(T)Copies writable properties (except Id) onto the stored entity; fails with RelationalErrors.ConcurrencyMessage when the id is unknown or the concurrency stamp is stale (see Versioned entities)
Delete(T)Removes the entity with the matching id; fails with RelationalErrors.ConcurrencyMessage when the id is unknown or the concurrency stamp is stale
CreateRange(IEnumerable<T>)Stores every entity, assigning each its id; returns the ids. If any id is missing or duplicated, stores nothing and returns a failed output
UpdateRange(IEnumerable<T>)Updates every entity, all or none: if any id is unknown or any stamp stale, updates nothing and fails with RelationalErrors.ConcurrencyMessage; returns the updated entities
DeleteRange(IEnumerable<TKey>)Removes every entity whose id is listed; returns the ids that were actually removed
public class Person : Entity<long>
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}

var repository = new FakeRepository<Person, long>();

var annId = repository.Create(new Person { Name = "Ann", Age = 30 }).Data;   // annId == 1
var bobId = repository.Create(new Person { Name = "Bob", Age = 25 }).Data;   // bobId == 2

repository.Update(new Person { Id = annId, Name = "Ann Smith", Age = 31 });

var ann = repository.GetById(annId).Data;      // Name == "Ann Smith"
var all = repository.GetAll().Data!.ToList();  // two people

repository.DeleteRange([annId, bobId]);

Update uses reflection to copy every writable property from the incoming entity onto the stored one, so it mirrors the behavior of a real ORM update where the identifier is preserved.

Versioned entities

For an entity implementing IVersionedEntity (such as one deriving from VersionedEntity<TKey>), Update, UpdateRange and Delete check the incoming ConcurrencyStamp against the stored one, as the real repository’s UPDATE/DELETE does. A stale stamp — including the fresh one a newly constructed entity gets — fails with RelationalErrors.ConcurrencyMessage and changes nothing. A successful update issues a new stamp, on the stored entity and on the one passed in, as BaseDbContext does on save. So an entity rebuilt from a request must carry the stamp it was read with, in tests as in production:

var accounts = new FakeRepository<Account, long>();   // Account : VersionedEntity<long>
var id = accounts.Create(new Account { Owner = "Ann" }).Data;
var stamp = accounts.GetById(id).Data!.ConcurrencyStamp;

accounts.Update(new Account { Id = id, Owner = "Bob" });                           // fails: stale stamp
accounts.Update(new Account { Id = id, Owner = "Bob", ConcurrencyStamp = stamp }); // succeeds, new stamp

How ids are assigned

Create and CreateRange (and their async counterparts) pick the id by key type, the way a database would for the usual key columns:

Key typeId assigned on create
long, intSequentially, starting at 1 per repository instance; ids are never reused after a delete
GuidGuid.NewGuid()
Anything else (e.g. string)None: the caller sets Id before creating. A missing (null/default) id or one that is already stored returns a failed output

When the fake generates the id, it overwrites any Id the caller set, so long-keyed tests behave exactly as they did in 3.x. To choose the ids yourself — for any key type — pass a generator to the constructor; it is called once per created entity:

public class Country : Entity<string>
{
    public string Name { get; set; } = string.Empty;
}

// Caller-assigned string keys:
var countries = new FakeRepository<Country, string>();
countries.Create(new Country { Id = "BR", Name = "Brazil" });   // Data == "BR"
countries.Create(new Country { Name = "Nowhere" });            // failed output: no id

// Generated string keys:
var next = 0;
var generated = new FakeRepository<Country, string>(() => $"C{++next}");
generated.Create(new Country { Name = "Brazil" });             // Data == "C1"

A generator that returns an id already in the store makes the create fail with RelationalErrors.UniqueViolationMessage, as a unique-key violation would.

AsyncFakeRepository<T, TKey>

AsyncFakeRepository<T, TKey> is the asynchronous counterpart of FakeRepository<T, TKey>. It implements IAsyncRepository<T, TKey> (from ArturRios.Data.Relational.Core) with the same in-memory storage, the same id assignment and the same semantics, so you can exercise services that depend on an async repository without touching a database. T must derive from ArturRios.Data.Relational.Core.Entities.Entity<TKey>.

Every method returns a Task<DataOutput<...>> that completes synchronously, and each accepts an optional CancellationToken that is observed before the operation runs (a cancelled token throws OperationCanceledException). Query() keeps the synchronous signature the interface declares, but the IQueryable<T> it returns is backed by an async query provider, so EF Core’s async operators (ToListAsync, FirstOrDefaultAsync, CountAsync, …) can be composed on top of it.

MethodBehavior
CreateAsync(T, CancellationToken)Assigns the entity’s id (see How ids are assigned), stores it, returns the id
GetByIdAsync(TKey, CancellationToken)Returns a successful output with the matching entity, or a successful output with null data when the id is unknown
GetAllAsync(CancellationToken)Returns a snapshot of every stored entity (later writes don’t change it)
Query()Exposes the stored entities as an async-capable IQueryable<T> (supports ToListAsync, FirstOrDefaultAsync, …)
UpdateAsync(T, CancellationToken)Copies writable properties (except Id) onto the stored entity; fails with RelationalErrors.ConcurrencyMessage when the id is unknown or the concurrency stamp is stale
DeleteAsync(T, CancellationToken)Removes the entity with the matching id; fails with RelationalErrors.ConcurrencyMessage when the id is unknown or the concurrency stamp is stale
CreateRangeAsync(IEnumerable<T>, CancellationToken)Stores every entity, assigning each its id; returns the ids. If any id is missing or duplicated, stores nothing and returns a failed output
UpdateRangeAsync(IEnumerable<T>, CancellationToken)Updates every entity, all or none: if any id is unknown or any stamp stale, updates nothing and fails with RelationalErrors.ConcurrencyMessage; returns the updated entities
DeleteRangeAsync(IEnumerable<TKey>, CancellationToken)Removes every entity whose id is listed; returns the ids that were actually removed
var repository = new AsyncFakeRepository<Person, long>();

var annId = (await repository.CreateAsync(new Person { Name = "Ann", Age = 30 })).Data;   // annId == 1
var bobId = (await repository.CreateAsync(new Person { Name = "Bob", Age = 25 })).Data;   // bobId == 2

await repository.UpdateAsync(new Person { Id = annId, Name = "Ann Smith", Age = 31 });

var ann = (await repository.GetByIdAsync(annId)).Data;      // Name == "Ann Smith"
var all = (await repository.GetAllAsync()).Data!.ToList();  // two people

// Query() composes with EF Core's async operators:
var adults = await repository.Query().Where(p => p.Age >= 18).ToListAsync();

await repository.DeleteRangeAsync([annId, bobId]);

FakeScheduler

FakeScheduler simulates a scheduled (delayed) dispatch of a command or query through an ArturRios.Mediator.CommandQueryMediator. It waits for a configurable delay and then dispatches, which is useful for testing code paths that would otherwise run on a timer or background schedule.

FakeScheduler(CommandQueryMediator mediator, int waitTimeInSeconds = 60)
MethodBehavior
CreateCommandSchedule<TCommand, TCommandOutput>(TCommand)Waits, then dispatches the command
CreateQuerySchedule<TQuery, TQueryOutput>(TQuery)Waits, then dispatches the query

Pass a small waitTimeInSeconds (for example 0) to keep tests fast:

var services = new ServiceCollection();
services.AddSingleton<ICommandHandlerAsync<PingCommand, PingCommandOutput>>(handler);
var provider = services.BuildServiceProvider();

var mediator = new CommandQueryMediator(provider.GetRequiredService<IServiceScopeFactory>());
var scheduler = new FakeScheduler(mediator, waitTimeInSeconds: 0);

await scheduler.CreateCommandSchedule<PingCommand, PingCommandOutput>(new PingCommand { Value = "ping" });
// the handler has now been invoked through the mediator

Upgrading to 4.0

The changes the 4.0 fakes need in your tests are listed in Upgrading from 3.x to 4.0 in the changelog.