tests: add test for unit of work

This commit is contained in:
Stevan Freeborn
2024-08-23 08:32:34 -05:00
parent 886eb2f855
commit 3739164960
14 changed files with 428 additions and 36 deletions
@@ -14,7 +14,7 @@ public class AddressRepositoryTests : RepositoryTest
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProvider.Setup(x => x.GetUtcNow()).Returns(now);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(now);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -35,7 +35,7 @@ public class AddressRepositoryTests : RepositoryTest
var createdTimeStamp = DateTimeOffset.UtcNow;
var updatedTimeStamp = createdTimeStamp.AddSeconds(2);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -51,7 +51,7 @@ public class AddressRepositoryTests : RepositoryTest
createdEntity!.CreatedAt.Should().Be(createdTimeStamp.DateTime);
createdEntity.UpdatedAt.Should().Be(createdTimeStamp.DateTime);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
// Update the entity
entity.StreetAddress = "Updated";
@@ -14,7 +14,7 @@ public class AliasRepositoryTests : RepositoryTest
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProvider.Setup(x => x.GetUtcNow()).Returns(now);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(now);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -35,7 +35,7 @@ public class AliasRepositoryTests : RepositoryTest
var createdTimeStamp = DateTimeOffset.UtcNow;
var updatedTimeStamp = createdTimeStamp.AddSeconds(2);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -51,7 +51,7 @@ public class AliasRepositoryTests : RepositoryTest
createdEntity!.CreatedAt.Should().Be(createdTimeStamp.DateTime);
createdEntity.UpdatedAt.Should().Be(createdTimeStamp.DateTime);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
// Update the entity
entity.Remarks = "Updated";
@@ -14,7 +14,7 @@ public class CommentRepositoryTests : RepositoryTest
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProvider.Setup(x => x.GetUtcNow()).Returns(now);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(now);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -35,7 +35,7 @@ public class CommentRepositoryTests : RepositoryTest
var createdTimeStamp = DateTimeOffset.UtcNow;
var updatedTimeStamp = createdTimeStamp.AddSeconds(2);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
var entity = _faker.Generate();
entity.SdnId = SdnId;
@@ -51,7 +51,7 @@ public class CommentRepositoryTests : RepositoryTest
createdEntity!.CreatedAt.Should().Be(createdTimeStamp.DateTime);
createdEntity.UpdatedAt.Should().Be(createdTimeStamp.DateTime);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
// Update the entity
entity.Remarks = "Updated";
@@ -0,0 +1,33 @@
namespace SanctionsSearch.Worker.Tests.Integration;
public class DatabaseTest : IAsyncLifetime
{
private readonly AppDbContext _appDbContext;
protected readonly Mock<TimeProvider> _timeProviderMock = new();
protected readonly DbContext _context;
protected readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders());
public DatabaseTest()
{
var options = new DbOptions { DatabaseName = $"{Guid.NewGuid()}.db" };
var context = new AppDbContext(options, _timeProviderMock.Object);
_appDbContext = context;
_context = context;
}
public virtual async Task InitializeAsync()
{
await _context.Database.MigrateAsync();
}
public virtual async Task DisposeAsync()
{
await _appDbContext.DisposeAsync();
SqliteConnection.ClearAllPools();
File.Delete(_appDbContext.DatabasePath);
}
}
@@ -0,0 +1,81 @@
namespace SanctionsSearch.Worker.Tests.Integration;
public class EfUnitOfWorkTests : DatabaseTest
{
private readonly EfUnitOfWork _uow;
private readonly SdnFaker _sdnFaker = new();
private readonly AddressFaker _addressFaker = new();
private readonly AliasFaker _aliasFaker = new();
private readonly CommentFaker _commentFaker = new();
public EfUnitOfWorkTests()
{
var logger = LoggerFactory.Create(c => c.ClearProviders());
_uow = new(_context, logger);
}
[Fact]
public async Task SaveChangesAsync_WhenCalled_ItShouldPersistChangesToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProviderMock
.Setup(m => m.GetUtcNow())
.Returns(now);
var sdn = _sdnFaker.Generate();
var address = _addressFaker.Generate();
address.SdnId = sdn.Id;
var alias = _aliasFaker.Generate();
alias.SdnId = sdn.Id;
var comment = _commentFaker.Generate();
comment.SdnId = sdn.Id;
await _uow.Sdns.Upsert(sdn);
await _uow.Addresses.Upsert(address);
await _uow.Aliases.Upsert(alias);
await _uow.Comments.Upsert(comment);
await _uow.SaveChangesAsync();
var savedSdn = await _context.Set<Sdn>()
.Include(s => s.Addresses)
.Include(s => s.Aliases)
.Include(s => s.Comments)
.FirstOrDefaultAsync();
savedSdn.Should().NotBeNull();
savedSdn.Should().BeEquivalentTo(
sdn,
opts => opts
.Excluding(s => s.Addresses)
.Excluding(s => s.Aliases)
.Excluding(s => s.Comments)
);
savedSdn!.Addresses.Should().HaveCount(1);
savedSdn.Aliases.Should().HaveCount(1);
savedSdn.Comments.Should().HaveCount(1);
var savedAddress = savedSdn.Addresses.First(a => a.Id == address.Id);
var savedAlias = savedSdn.Aliases.First(a => a.Id == alias.Id);
var savedComment = savedSdn.Comments.First(a => a.Id == comment.Id);
savedAddress.Should().BeEquivalentTo(address);
savedAlias.Should().BeEquivalentTo(alias);
savedComment.Should().BeEquivalentTo(comment);
}
[Fact]
public async Task DisposeAsync_WhenCalled_ItShouldDisposeContext()
{
await _uow.DisposeAsync();
var action = () => _context.SaveChangesAsync();
await action.Should().ThrowAsync<ObjectDisposedException>();
}
}
@@ -1,38 +1,25 @@
namespace SanctionsSearch.Worker.Tests.Integration;
public class RepositoryTest : IAsyncLifetime
public class RepositoryTest : DatabaseTest, IAsyncLifetime
{
private readonly AppDbContext _appDbContext;
private readonly SdnFaker _faker = new();
private readonly Sdn _sdn;
protected readonly Mock<TimeProvider> _timeProvider = new();
protected readonly DbContext _context;
protected int SdnId => _sdn.Id;
protected readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders());
public RepositoryTest()
{
var options = new DbOptions { DatabaseName = $"{Guid.NewGuid()}.db" };
var context = new AppDbContext(options, _timeProvider.Object);
_appDbContext = context;
_context = context;
_sdn = _faker.Generate();
_sdn.Id = SdnFaker.ReservedId;
}
public async Task InitializeAsync()
public override async Task InitializeAsync()
{
await _context.Database.MigrateAsync();
await base.InitializeAsync();
await _context.Set<Sdn>().AddAsync(_sdn);
}
public async Task DisposeAsync()
public override async Task DisposeAsync()
{
await _appDbContext.DisposeAsync();
SqliteConnection.ClearAllPools();
File.Delete(_appDbContext.DatabasePath);
await base.DisposeAsync();
}
}
@@ -14,7 +14,7 @@ public class SdnRepositoryTests : RepositoryTest
public async Task Upsert_WithNewEntity_ShouldAddEntityToDatabase()
{
var now = DateTimeOffset.UtcNow;
_timeProvider.Setup(x => x.GetUtcNow()).Returns(now);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(now);
var entity = _faker.Generate();
@@ -34,7 +34,7 @@ public class SdnRepositoryTests : RepositoryTest
var createdTimeStamp = DateTimeOffset.UtcNow;
var updatedTimeStamp = createdTimeStamp.AddSeconds(2);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(createdTimeStamp);
var entity = _faker.Generate();
@@ -49,7 +49,7 @@ public class SdnRepositoryTests : RepositoryTest
createdEntity!.CreatedAt.Should().Be(createdTimeStamp.DateTime);
createdEntity.UpdatedAt.Should().Be(createdTimeStamp.DateTime);
_timeProvider.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
_timeProviderMock.Setup(x => x.GetUtcNow()).Returns(updatedTimeStamp);
// Update the entity
entity.Name = "Updated";
@@ -0,0 +1,37 @@
namespace SanctionsSearch.Worker.Tests.Unit;
public class EfUnitOfWorkTests
{
private readonly Mock<DbContext> _context = new();
private readonly Mock<ILoggerFactory> _loggerFactory = new();
private readonly EfUnitOfWork _unitOfWork;
public EfUnitOfWorkTests()
{
_unitOfWork = new(_context.Object, _loggerFactory.Object);
}
[Fact]
public void Sdns_WhenCalled_ShouldReturnSdnRepository()
{
_unitOfWork.Sdns.Should().BeOfType<SdnRepository>();
}
[Fact]
public void Addresses_WhenCalled_ShouldReturnAddressRepository()
{
_unitOfWork.Addresses.Should().BeOfType<AddressRepository>();
}
[Fact]
public void Aliases_WhenCalled_ShouldReturnAliasRepository()
{
_unitOfWork.Aliases.Should().BeOfType<AliasRepository>();
}
[Fact]
public void Comments_WhenCalled_ShouldReturnCommentRepository()
{
_unitOfWork.Comments.Should().BeOfType<CommentRepository>();
}
}
@@ -0,0 +1,226 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SanctionsSearch.Worker.Persistence;
#nullable disable
namespace SanctionsSearch.Worker.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240822235325_add_addresses_and_comments_to_sdn")]
partial class add_addresses_and_comments_to_sdn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.Entity("SanctionsSearch.Worker.Models.Address", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CityProvincePostal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Remarks")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SdnId")
.HasColumnType("INTEGER");
b.Property<string>("StreetAddress")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SdnId");
b.ToTable("Addresses");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Alias", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Remarks")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SdnId")
.HasColumnType("INTEGER");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SdnId");
b.ToTable("Aliases");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Comment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Remarks")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SdnId")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("SdnId");
b.ToTable("Comments");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Sdn", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CallSign")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("GrossRegisteredTonnage")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Program")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Remarks")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Tonnage")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("VesselFlag")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("VesselOwner")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("VesselType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Sdns");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Address", b =>
{
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
.WithMany("Addresses")
.HasForeignKey("SdnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Sdn");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Alias", b =>
{
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
.WithMany("Aliases")
.HasForeignKey("SdnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Sdn");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Comment", b =>
{
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
.WithMany("Comments")
.HasForeignKey("SdnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Sdn");
});
modelBuilder.Entity("SanctionsSearch.Worker.Models.Sdn", b =>
{
b.Navigation("Addresses");
b.Navigation("Aliases");
b.Navigation("Comments");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SanctionsSearch.Worker.Migrations
{
/// <inheritdoc />
public partial class add_addresses_and_comments_to_sdn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -179,7 +179,7 @@ namespace SanctionsSearch.Worker.Migrations
modelBuilder.Entity("SanctionsSearch.Worker.Models.Address", b =>
{
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
.WithMany()
.WithMany("Addresses")
.HasForeignKey("SdnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -201,7 +201,7 @@ namespace SanctionsSearch.Worker.Migrations
modelBuilder.Entity("SanctionsSearch.Worker.Models.Comment", b =>
{
b.HasOne("SanctionsSearch.Worker.Models.Sdn", "Sdn")
.WithMany()
.WithMany("Comments")
.HasForeignKey("SdnId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -211,7 +211,11 @@ namespace SanctionsSearch.Worker.Migrations
modelBuilder.Entity("SanctionsSearch.Worker.Models.Sdn", b =>
{
b.Navigation("Addresses");
b.Navigation("Aliases");
b.Navigation("Comments");
});
#pragma warning restore 612, 618
}
+2
View File
@@ -14,5 +14,7 @@ class Sdn : Entity
public string VesselOwner { get; set; } = string.Empty;
public string Remarks { get; set; } = string.Empty;
public virtual ICollection<Address> Addresses { get; set; } = [];
public virtual ICollection<Alias> Aliases { get; set; } = [];
public virtual ICollection<Comment> Comments { get; set; } = [];
}
@@ -1,8 +1,8 @@
namespace SanctionsSearch.Worker.Persistence;
class EfUnitOfWork(AppDbContext context, ILoggerFactory loggerFactory) : IUnitOfWork, IAsyncDisposable
class EfUnitOfWork(DbContext context, ILoggerFactory loggerFactory) : IUnitOfWork, IAsyncDisposable
{
private readonly AppDbContext _context = context;
private readonly DbContext _context = context;
public ISdnRepository Sdns { get; } = new SdnRepository(context, loggerFactory.CreateLogger<SdnRepository>());
public IAddressRepository Addresses { get; } = new AddressRepository(context, loggerFactory.CreateLogger<AddressRepository>());
public IAliasRepository Aliases { get; } = new AliasRepository(context, loggerFactory.CreateLogger<AliasRepository>());
+2 -2
View File
@@ -63,14 +63,14 @@ class Program
builder.Services.AddScoped(rs => rs.GetRequiredService<IOptionsSnapshot<DbOptions>>().Value);
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<IOfacFileService, OfacFileService>();
// builder.Services.AddScoped<IOfacFileService, OfacFileService>();
builder.Services.AddScoped<ISdnRepository, SdnRepository>();
builder.Services.AddScoped<IAddressRepository, AddressRepository>();
builder.Services.AddScoped<IAliasRepository, AliasRepository>();
builder.Services.AddScoped<ICommentRepository, CommentRepository>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddDbContext<DbContext, AppDbContext>();
builder.Services.AddHostedService<Worker>();