Skip to content

Commit

Permalink
Make DB interactions in TESTS scoped (#8)
Browse files Browse the repository at this point in the history
* Added `AsNoTracking()` where no need in the tracking/caching of DB records
* Made data seeding and assertions in the tests scoped
  • Loading branch information
AKlaus authored Dec 29, 2024
1 parent 4c5421d commit a2f7ace
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 51 deletions.
28 changes: 20 additions & 8 deletions Domain.Tests/Services/Client/ClientCreateUpdateDeleteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ public async Task Create_Client_Works()

// THEN client appears in the DB
Assert.True(result.IsSuccess);
var client = await DataContext.Clients.FindAsync(clientId);
Assert.NotNull(client);
Assert.Equal("Test", client.Name);
await ScopedDataContextExecAsync(
async context =>
{
var client = await context.Clients.FindAsync(clientId);
Assert.NotNull(client);
Assert.Equal("Test", client.Name);
});
}

[Fact]
Expand All @@ -40,9 +44,13 @@ public async Task Update_Client_Works()

// THEN the name is updated
Assert.True(result.IsSuccess);
var client = await DataContext.Clients.FindAsync(clientId);
Assert.NotNull(client);
Assert.Equal("XYZ", client.Name);
await ScopedDataContextExecAsync(
async context =>
{
var client = await context.Clients.FindAsync(clientId);
Assert.NotNull(client);
Assert.Equal("XYZ", client.Name);
});
}

[Fact]
Expand All @@ -56,7 +64,11 @@ public async Task Delete_Client_Works()

// THEN the client cease to exist
Assert.True(result.IsSuccess);
var client = await DataContext.Clients.FindAsync(clientId);
Assert.Null(client);
await ScopedDataContextExecAsync(
async context =>
{
var client = await context.Clients.FindAsync(clientId);
Assert.Null(client);
});
}
}
2 changes: 1 addition & 1 deletion Domain.Tests/Services/Client/ClientQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public async Task Get_Clients_List_Works()

// THEN get 2 clients
Assert.Equal(2, clients.Length);
Assert.True(new[] {"Name1", "Name2"}.SequenceEqual(clients.OrderBy(c => c.Name).Select(c=>c.Name)));
Assert.Equal(new[] {"Name1", "Name2"}, clients.OrderBy(c => c.Name).Select(c=>c.Name));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ public async Task Create_Client_With_Non_Unique_Name_Fails()

// THEN operation fails
Assert.False(result.IsSuccess);
var clientCount = await DataContext.Clients.CountAsync();
Assert.Equal(1, clientCount);
await ScopedDataContextExecAsync(
async context =>
{
var clientCount = await context.Clients.CountAsync();
Assert.Equal(1, clientCount);
});
}

[Fact]
Expand Down
42 changes: 27 additions & 15 deletions Domain.Tests/Services/Invoice/InvoiceCreateUpdateDeleteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,56 @@ public async Task Create_Invoice_Works()

// THEN client appears in the DB
Assert.True(result.IsSuccess);
var invoice = (await DataContext.Invoices.FindAsync(invoiceId))!;
Assert.NotNull(invoice);
Assert.Equal("INV-01", invoice.Number);
Assert.Equal(DateOnly.Parse("2020-07-07"), invoice.Date);
Assert.Equal(clientId, invoice.ClientId);
Assert.Equal(20, invoice.Amount);
await ScopedDataContextExecAsync(
async context =>
{
var invoice = await context.Invoices.FindAsync(invoiceId);
Assert.NotNull(invoice);
Assert.Equal("INV-01", invoice.Number);
Assert.Equal(DateOnly.Parse("2020-07-07"), invoice.Date);
Assert.Equal(clientId, invoice.ClientId);
Assert.Equal(20, invoice.Amount);
});
}

[Fact]
public async Task Update_Invoice_Works()
{
// GIVEN a client & an invoice
var clientId = await SeedClient("Name");
var (invoiceId, _) = await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), clientId, 20));
await SeedInvoice("INV-01", clientId, DateOnly.Parse("2020-07-07"), 20);

// WHEN update amount of the invoice
var result = await InvoiceCommandService.Update(invoiceId, new UpdateInvoiceRequest(DateOnly.Parse("2020-07-07"), 30));
var result = await InvoiceCommandService.Update("INV-01", new UpdateInvoiceRequest(DateOnly.Parse("2020-07-07"), 30));

// THEN the amount is updated
Assert.True(result.IsSuccess);
var invoice = (await DataContext.Invoices.FindAsync(invoiceId))!;
Assert.NotNull(invoice);
Assert.Equal(30, invoice.Amount);
await ScopedDataContextExecAsync(
async context =>
{
var invoice = await context.Invoices.FindAsync("INV-01");
Assert.NotNull(invoice);
Assert.Equal(30, invoice.Amount);
});
}

[Fact]
public async Task Delete_Invoice_Works()
{
// GIVEN a client & an invoice
var clientId = await SeedClient("Name");
var (invoiceId, _) = await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), clientId, 20));
await SeedInvoice("INV-01", clientId, DateOnly.Parse("2020-07-07"), 20);

// WHEN delete the invoice
var result = await InvoiceCommandService.Delete(invoiceId);
var result = await InvoiceCommandService.Delete("INV-01");

// THEN the invoice cease to exist
Assert.True(result.IsSuccess);
var client = await DataContext.Invoices.FindAsync(invoiceId);
Assert.Null(client);
await ScopedDataContextExecAsync(
async context =>
{
var client = await context.Invoices.FindAsync("INV-01");
Assert.Null(client);
});
}
}
18 changes: 9 additions & 9 deletions Domain.Tests/Services/Invoice/InvoiceQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public async Task Get_Invoice_By_Number_Works()
{
// GIVEN a client & an invoice
var clientId = await SeedClient("Name");
var (invoiceId, _) = await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), clientId, 20));
await SeedInvoice("INV-01", clientId, DateOnly.Parse("2020-07-07"), 20);

// WHEN get invoice by number
var (invoice, result) = await InvoiceQueryService.GetByNumber(invoiceId);
var (invoice, result) = await InvoiceQueryService.GetByNumber("INV-01");

// THEN invoice gets resolved
Assert.True(result.IsSuccess);
Expand All @@ -33,29 +33,29 @@ public async Task Get_Invoice_List_Works()
{
// GIVEN a client & 2 invoices
var clientId = await SeedClient("Name");
await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), clientId, 10));
await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-02", DateOnly.Parse("2020-08-07"), clientId, 20));
await SeedInvoice("INV-01", clientId, DateOnly.Parse("2020-07-07"), 10);
await SeedInvoice("INV-02", clientId, DateOnly.Parse("2020-08-07"), 20);

// WHEN get a list of invoices
var invoices = await InvoiceQueryService.GetList(new GetInvoiceListRequest());

// THEN get 2 invoices
Assert.Equal(2, invoices.Length);
var orderedList = invoices.OrderBy(c => c.Number).ToArray();
Assert.True(new[] {"INV-01", "INV-02"}.SequenceEqual(orderedList.Select(c => c.Number)));
Assert.True(new[] {DateOnly.Parse("2020-07-07"), DateOnly.Parse("2020-08-07")}.SequenceEqual(orderedList.Select(c => c.Date)));
Assert.True(new[] {10m, 20m}.SequenceEqual(orderedList.Select(c => c.Amount)));
Assert.Equal(new[] {"INV-01", "INV-02"}, orderedList.Select(c => c.Number));
Assert.Equal(new[] {DateOnly.Parse("2020-07-07"), DateOnly.Parse("2020-08-07")}, orderedList.Select(c => c.Date));
Assert.Equal(new[] {10m, 20m}, orderedList.Select(c => c.Amount));
}

[Fact]
public async Task Get_Invoice_List_Filtered_By_Client_Works()
{
// GIVEN a client 1 with an invoice
var client1Id = await SeedClient("Homer Simpson");
await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), client1Id, 10));
await SeedInvoice("INV-01", client1Id, DateOnly.Parse("2020-07-07"), 10);
// and a client 2 with an invoice
var client2Id = await SeedClient("Marge Simpson");
await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-02", DateOnly.Parse("2020-08-07"), client2Id, 20));
await SeedInvoice("INV-02", client2Id, DateOnly.Parse("2020-08-07"), 20);

// WHEN get a list of invoices for client 1
var invoices = await InvoiceQueryService.GetList(new GetInvoiceListRequest(client1Id));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ public async Task Create_Invoice_With_Non_Unique_Number_Fails()
{
// GIVEN a client & an invoice
var clientId = await SeedClient("Name");
await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-07-07"), clientId, 20));
await SeedInvoice("INV-01", clientId, DateOnly.Parse("2020-07-07"), 20);

// WHEN create a new invoice with the same number
var (_, result) = await InvoiceCommandService.Create(new CreateInvoiceRequest("INV-01", DateOnly.Parse("2020-08-01"), clientId, 10));

// THEN operation fails
Assert.False(result.IsSuccess);
var invoiceCount = await DataContext.Invoices.CountAsync();
Assert.Equal(1, invoiceCount);
await ScopedDataContextExecAsync(
async context =>
{
var invoiceCount = await context.Invoices.CountAsync();
Assert.Equal(1, invoiceCount);
});
}
}
52 changes: 42 additions & 10 deletions Domain.Tests/TestDbBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ namespace AK.DbSample.Domain.Tests;
/// Base test class with a DI container and DB connection.
/// Derive from it when need DI and DB connection
/// </summary>
public abstract class TestDbBase(ITestOutputHelper output) : TestBase, IAsyncLifetime
// ReSharper disable once InconsistentNaming
public abstract class TestDbBase(ITestOutputHelper _output) : TestBase, IAsyncLifetime
{
protected DataContext DataContext => Container.GetRequiredService<DataContext>();
protected readonly ITestOutputHelper Output = output;
private DataContext DataContext => Container.GetRequiredService<DataContext>();

/// <summary>
/// Tables that shouldn't be touched on whipping out the DB
Expand All @@ -42,12 +42,21 @@ protected override void ConfigureIocContainer(IServiceCollection services)
base.ConfigureIocContainer(services);
}

protected async Task SeedData(params object[] entities)
{
await DataContext.AddRangeAsync(entities);
await DataContext.SaveChangesAsync();
}
/// <summary>
/// Seed an invoice in the DB
/// </summary>
protected Task SeedInvoice(string number, long clientId, DateOnly date, decimal amount)
=> SeedData (new Invoice
{
Number = number,
ClientId = clientId,
Amount = amount,
Date = date
});

/// <summary>
/// Seed a client in the DB
/// </summary>
protected async Task<long> SeedClient(string name = "Test Client 1")
{
await SeedData(new Client { Name = name });
Expand Down Expand Up @@ -91,12 +100,12 @@ private async Task WipeOutDbAsync()
}
catch (ArgumentNullException ergExc)
{
Output.WriteLine(ergExc.Message);
_output.WriteLine(ergExc.Message);
throw;
}
catch(Exception e)
{
Output.WriteLine(e.Message +" \n"+ (respawn?.DeleteSql ?? "no delete SQL"));
_output.WriteLine(e.Message +" \n"+ (respawn?.DeleteSql ?? "no delete SQL"));
throw;
}
}
Expand All @@ -110,6 +119,29 @@ protected override void Dispose(bool disposing)

base.Dispose(disposing);
}

/// <summary>
/// Perform <paramref name="dataContextFunc"/> on a new scope of DataContext, to avoid skewed test results due to the EF cache.
/// </summary>
/// <param name="dataContextFunc"> The function to perform on the newly created DAtaContext scope </param>
protected async Task ScopedDataContextExecAsync(Func<DataContext, Task> dataContextFunc)
{
using var assertionScope = Container.CreateScope();
await using var dataContext = assertionScope.ServiceProvider.GetRequiredService<DataContext>();
{
// Disable change tracking
dataContext.ChangeTracker.AutoDetectChangesEnabled = false;
await dataContextFunc(dataContext);
}
}

private Task SeedData(params object[] entities)
=> ScopedDataContextExecAsync(
async context =>
{
await context.AddRangeAsync(entities);
await context.SaveChangesAsync();
});

private string GetSqlConnectionStringFromConfiguration()
{
Expand Down
2 changes: 1 addition & 1 deletion Domain/Services/Client/ClientCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private async Task<IDomainResult> UniqueNameCheck(long? id, string name)
if (string.IsNullOrWhiteSpace(name))
return IDomainResult.Failed("Name can't be empty");

var clientsQuery = DataContext.Clients.Where(c => c.Name == name);
var clientsQuery = DataContext.Clients.AsNoTracking().Where(c => c.Name == name);
if (id.HasValue)
clientsQuery = clientsQuery.Where(c => c.Id != id);

Expand Down
2 changes: 2 additions & 0 deletions Domain/Services/Client/ClientQueryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ClientQueryService(DataContext dataContext) : BaseService(dataConte
{
var client = await DataContext.Clients
.Include(c => c.Invoices)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Id == clientId);
if (client == null)
return IDomainResult.NotFound<GetClientByIdResponse>("Client not found");
Expand All @@ -31,6 +32,7 @@ public class ClientQueryService(DataContext dataContext) : BaseService(dataConte
public async Task<GetClientListResponse[]> GetList(GetClientListRequest filter)
{
var query = from c in DataContext.Clients.Include(c => c.Invoices)
.AsNoTracking()
select new GetClientListResponse(
c.Id,
c.Name,
Expand Down
4 changes: 2 additions & 2 deletions Domain/Services/Invoice/InvoiceCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private async Task<IDomainResult> UniqueNumberCheck(string newNumber, bool creat
if (string.IsNullOrWhiteSpace(newNumber))
return IDomainResult.Failed("Number can't be empty");

if (await DataContext.Invoices.Where(c => c.Number == newNumber).AnyAsync()
if (await DataContext.Invoices.AsNoTracking().Where(c => c.Number == newNumber).AnyAsync()
&& creatingNew)
return IDomainResult.Failed($"Invoice number '{newNumber}' already exists");

Expand All @@ -82,7 +82,7 @@ private async Task<IDomainResult> UniqueNumberCheck(string newNumber, bool creat

private async Task<IDomainResult> ClientExistsCheck(long clientId)
{
return !await DataContext.Clients.Where(c => c.Id == clientId).AnyAsync()
return !await DataContext.Clients.AsNoTracking().Where(c => c.Id == clientId).AnyAsync()
? IDomainResult.NotFound("Client not found")
: IDomainResult.Success();
}
Expand Down
2 changes: 2 additions & 0 deletions Domain/Services/Invoice/InvoiceQueryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class InvoiceQueryService(DataContext dataContext) : BaseService(dataCont
{
var invoice = await DataContext.Invoices
.Include(i => i.Client)
.AsNoTracking()
.SingleOrDefaultAsync(c => c.Number == number);
if (invoice == null)
return IDomainResult.NotFound<GetInvoiceByNumberResponse>("Invoice not found");
Expand All @@ -36,6 +37,7 @@ public async Task<GetInvoiceListResponse[]> GetList(GetInvoiceListRequest filter
{
var query = DataContext.Invoices
.Include(i => i.Client)
.AsNoTracking()
.Select(i => i);
if (filter.ClientId != null)
query = query.Where(i => i.ClientId == filter.ClientId);
Expand Down

0 comments on commit a2f7ace

Please sign in to comment.