Add test infrastructure and repository tests

Introduced `CfgProfileDto` for testing and added a `FakeDataGenerator` utility for generating test data. Updated `ECMJobRunner.Tests.csproj` to support both `.NET Framework 4.8` and `.NET 8.0`, enabling nullable reference types and adding dependencies for testing frameworks, DI, and AutoMapper.

Added `TestFixture` to set up dependency injection and in-memory databases for tests. Created `TestMappingProfile` for AutoMapper configurations. Implemented `CfgProfileRepositoryTests` to validate repository methods, including `GetByIdAsync`, `GetAllAsync`, `AddAsync`, and `FindAsync`.

Enhanced test maintainability with `FluentAssertions` and realistic data generation using `Bogus`.
This commit is contained in:
2026-07-09 16:09:19 +02:00
parent bd6c1c1309
commit ee3af3e462
6 changed files with 428 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
using Bogus;
using ECMJobRunner.Domain.Entities;
namespace ECMJobRunner.Tests.Infrastructure;
/// <summary>
/// Fake data generator using Bogus library
/// </summary>
public static class FakeDataGenerator
{
private static int _profileIdCounter = 1;
private static int _sqlJobIdCounter = 1;
private static int _historyIdCounter = 1;
/// <summary>
/// Reset ID counters (call at the beginning of each test)
/// </summary>
public static void ResetIdCounters()
{
_profileIdCounter = 1;
_sqlJobIdCounter = 1;
_historyIdCounter = 1;
}
/// <summary>
/// Generate fake CfgProfile
/// </summary>
public static Faker<CfgProfile> CfgProfileFaker()
{
return new Faker<CfgProfile>()
.RuleFor(p => p.Id, f => _profileIdCounter++)
.RuleFor(p => p.Active, f => f.Random.Bool())
.RuleFor(p => p.ProfileName, f => f.Commerce.ProductName())
.RuleFor(p => p.TypeId, f => f.Random.Byte(0, 3))
.RuleFor(p => p.Schedule, f => $"{f.Random.Int(0, 59)} {f.Random.Int(0, 23)} * * *")
.RuleFor(p => p.Comment, f => f.Lorem.Sentence())
.RuleFor(p => p.AddedWho, f => f.Internet.UserName())
.RuleFor(p => p.AddedWhen, f => f.Date.Past())
.RuleFor(p => p.ChangedWho, f => f.Internet.UserName())
.RuleFor(p => p.ChangedWhen, f => f.Date.Recent());
}
/// <summary>
/// Generate fake ProfileSqlJob
/// </summary>
public static Faker<ProfileSqlJob> ProfileSqlJobFaker(long? profileId = null)
{
return new Faker<ProfileSqlJob>()
.RuleFor(j => j.Id, f => _sqlJobIdCounter++)
.RuleFor(j => j.ProfileId, f => profileId ?? f.Random.Long(1, 100))
.RuleFor(j => j.Active, f => f.Random.Bool())
.RuleFor(j => j.Sequence, f => f.Random.Short(1, 100))
.RuleFor(j => j.Name, f => f.Hacker.Verb())
.RuleFor(j => j.SqlCheckQuery, f => $"SELECT COUNT(*) FROM {f.Database.Type()}")
.RuleFor(j => j.SqlMainQuery, f => $"SELECT * FROM {f.Database.Type()}")
.RuleFor(j => j.ApiCommand, f => f.Internet.Url())
.RuleFor(j => j.Comment, f => f.Lorem.Sentence())
.RuleFor(j => j.AddedWho, f => f.Internet.UserName())
.RuleFor(j => j.AddedWhen, f => f.Date.Past())
.RuleFor(j => j.ChangedWho, f => f.Internet.UserName())
.RuleFor(j => j.ChangedWhen, f => f.Date.Recent());
}
/// <summary>
/// Generate fake ProfileHistory
/// </summary>
public static Faker<ProfileHistory> ProfileHistoryFaker(long? profileId = null)
{
return new Faker<ProfileHistory>()
.RuleFor(h => h.Id, f => _historyIdCounter++)
.RuleFor(h => h.ProfileId, f => profileId ?? f.Random.Long(1, 100))
.RuleFor(h => h.ResultId, f => f.Random.Byte(0, 2))
.RuleFor(h => h.ResultText, f => f.Lorem.Paragraph())
.RuleFor(h => h.AddedWho, f => f.Internet.UserName())
.RuleFor(h => h.AddedWhen, f => f.Date.Past())
.RuleFor(h => h.ChangedWho, f => f.Internet.UserName())
.RuleFor(h => h.ChangedWhen, f => f.Date.Recent());
}
}

View File

@@ -0,0 +1,81 @@
using AutoMapper;
using ECMJobRunner.Domain.Interfaces;
using ECMJobRunner.Infrastructure;
using ECMJobRunner.Infrastructure.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ECMJobRunner.Tests.Infrastructure;
/// <summary>
/// Base test fixture with Dependency Injection and In-Memory Database
/// Supports both .NET Framework 4.8 (EF6 with Effort) and .NET 8 (EF Core InMemory)
/// </summary>
public class TestFixture : IDisposable
{
private readonly IHost _host;
private IServiceScope? _scope;
public IServiceProvider Services => _scope?.ServiceProvider ?? _host.Services;
public JobRunnerDbContext DbContext => Services.GetRequiredService<JobRunnerDbContext>();
public IMapper Mapper => Services.GetRequiredService<IMapper>();
public ICfgProfileRepository ProfileRepository => Services.GetRequiredService<ICfgProfileRepository>();
public IProfileSqlJobRepository SqlJobRepository => Services.GetRequiredService<IProfileSqlJobRepository>();
public IProfileHistoryRepository HistoryRepository => Services.GetRequiredService<IProfileHistoryRepository>();
public TestFixture()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Use AddInfrastructureInMemory extension for both frameworks
services.AddInfrastructureInMemory();
// Register test-specific AutoMapper profiles
services.AddAutoMapper(typeof(TestFixture).Assembly);
})
.Build();
// Create a scope for scoped services
_scope = _host.Services.CreateScope();
#if !NET48
// Ensure database is created (EF Core only)
DbContext.Database.EnsureCreated();
#endif
}
/// <summary>
/// Create a new service scope (useful for testing scoped lifetime)
/// </summary>
public IServiceScope CreateScope()
{
return _host.Services.CreateScope();
}
/// <summary>
/// Reset the current scope (creates a new DbContext)
/// </summary>
public void ResetScope()
{
_scope?.Dispose();
_scope = _host.Services.CreateScope();
}
/// <summary>
/// Clear all data from database
/// </summary>
public async Task ClearDatabaseAsync()
{
DbContext.ProfileHistories.RemoveRange(DbContext.ProfileHistories);
DbContext.ProfileSqlJobs.RemoveRange(DbContext.ProfileSqlJobs);
DbContext.CfgProfiles.RemoveRange(DbContext.CfgProfiles);
await DbContext.SaveChangesAsync();
}
public void Dispose()
{
_scope?.Dispose();
_host?.Dispose();
}
}