Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make DB interactions in TESTS scoped #8

Merged
merged 5 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
{
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,10 +32,11 @@
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,
c.Invoices

Check warning on line 39 in Domain/Services/Client/ClientQueryService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 39 in Domain/Services/Client/ClientQueryService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 39 in Domain/Services/Client/ClientQueryService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 39 in Domain/Services/Client/ClientQueryService.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
.OrderByDescending(i => i.Date)
.Take(1)
.SingleOrDefault().Date
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
Loading