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:
20
ECMJobRunner.Tests/DTOs/CfgProfileDto.cs
Normal file
20
ECMJobRunner.Tests/DTOs/CfgProfileDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace ECMJobRunner.Tests.DTOs
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO for CfgProfile entity used in tests
|
||||
/// </summary>
|
||||
public class CfgProfileDto
|
||||
{
|
||||
public bool Active { get; set; }
|
||||
public string ProfileName { get; set; } = string.Empty;
|
||||
public byte TypeId { get; set; }
|
||||
public string? Schedule { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public string AddedWho { get; set; } = string.Empty;
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public string ChangedWho { get; set; } = string.Empty;
|
||||
public DateTime ChangedWhen { get; set; }
|
||||
}
|
||||
}
|
||||
71
ECMJobRunner.Tests/ECMJobRunner.Tests.csproj
Normal file
71
ECMJobRunner.Tests/ECMJobRunner.Tests.csproj
Normal file
@@ -0,0 +1,71 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net480;net8.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
|
||||
<!-- Suppress AutoMapper vulnerability warning for .NET Framework 4.8 -->
|
||||
<NoWarn>$(NoWarn);NU1903</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Test Framework - Common for both frameworks -->
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Fluent Assertions for Better Test Readability -->
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
|
||||
<!-- Mocking Framework -->
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
|
||||
<!-- Fake Data Generation (common for both frameworks) -->
|
||||
<PackageReference Include="Bogus" Version="35.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- .NET Framework 4.8 specific packages -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net480'">
|
||||
<!-- Dependency Injection & Hosting for .NET Framework 4.8 -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
|
||||
<!-- AutoMapper for .NET Framework 4.8 (matching Infrastructure) -->
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- .NET 8.0 specific packages -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<!-- Dependency Injection & Hosting for .NET 8 -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
|
||||
<!-- AutoMapper for .NET 8 (matching Infrastructure) -->
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Project References -->
|
||||
<ProjectReference Include="..\ECMJobRunner.Domain\ECMJobRunner.Domain.csproj" />
|
||||
<ProjectReference Include="..\ECMJobRunner.Infrastructure\ECMJobRunner.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
79
ECMJobRunner.Tests/Infrastructure/FakeDataGenerator.cs
Normal file
79
ECMJobRunner.Tests/Infrastructure/FakeDataGenerator.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
81
ECMJobRunner.Tests/Infrastructure/TestFixture.cs
Normal file
81
ECMJobRunner.Tests/Infrastructure/TestFixture.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
24
ECMJobRunner.Tests/Mapping/TestMappingProfile.cs
Normal file
24
ECMJobRunner.Tests/Mapping/TestMappingProfile.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Tests.DTOs;
|
||||
|
||||
namespace ECMJobRunner.Tests.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// AutoMapper profile for test DTOs
|
||||
/// </summary>
|
||||
public class TestMappingProfile : Profile
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor configuring test DTO mappings
|
||||
/// </summary>
|
||||
public TestMappingProfile()
|
||||
{
|
||||
// CfgProfileDto -> CfgProfile
|
||||
CreateMap<CfgProfileDto, CfgProfile>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.SqlJobs, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.ProfileHistories, opt => opt.Ignore());
|
||||
}
|
||||
}
|
||||
}
|
||||
153
ECMJobRunner.Tests/Repositories/CfgProfileRepositoryTests.cs
Normal file
153
ECMJobRunner.Tests/Repositories/CfgProfileRepositoryTests.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Tests.DTOs;
|
||||
using ECMJobRunner.Tests.Infrastructure;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace ECMJobRunner.Tests.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for CfgProfileRepository
|
||||
/// </summary>
|
||||
public class CfgProfileRepositoryTests : IClassFixture<TestFixture>
|
||||
{
|
||||
private readonly TestFixture _fixture;
|
||||
|
||||
public CfgProfileRepositoryTests(TestFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
FakeDataGenerator.ResetIdCounters();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ExistingProfile_ReturnsProfile()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.ClearDatabaseAsync();
|
||||
var dto = new CfgProfileDto
|
||||
{
|
||||
Active = true,
|
||||
ProfileName = "Test Profile for GetById",
|
||||
TypeId = (byte)1,
|
||||
Schedule = "0 0 * * *",
|
||||
Comment = "Test",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
ChangedWho = "TestUser",
|
||||
ChangedWhen = DateTime.Now
|
||||
};
|
||||
var addedProfile = await _fixture.ProfileRepository.AddAsync(dto);
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ProfileRepository.GetByIdAsync(addedProfile.Id);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(addedProfile.Id);
|
||||
result.ProfileName.Should().Be("Test Profile for GetById");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAllAsync_ReturnsAllProfiles()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.ClearDatabaseAsync();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var dto = new CfgProfileDto
|
||||
{
|
||||
Active = true,
|
||||
ProfileName = $"Profile {i}",
|
||||
TypeId = (byte)1,
|
||||
Schedule = "0 0 * * *",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
ChangedWho = "TestUser",
|
||||
ChangedWhen = DateTime.Now
|
||||
};
|
||||
await _fixture.ProfileRepository.AddAsync(dto);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ProfileRepository.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAsync_NewProfile_AddsToDatabase()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.ClearDatabaseAsync();
|
||||
var dto = new CfgProfileDto
|
||||
{
|
||||
Active = true,
|
||||
ProfileName = "Test Profile",
|
||||
TypeId = (byte)1,
|
||||
Schedule = "0 0 * * *",
|
||||
Comment = "Test comment",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
ChangedWho = "TestUser",
|
||||
ChangedWhen = DateTime.Now
|
||||
};
|
||||
|
||||
// Act
|
||||
var addedProfile = await _fixture.ProfileRepository.AddAsync(dto);
|
||||
|
||||
// Assert
|
||||
var result = await _fixture.ProfileRepository.GetByIdAsync(addedProfile.Id);
|
||||
result.Should().NotBeNull();
|
||||
result!.ProfileName.Should().Be("Test Profile");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindAsync_WithPredicate_ReturnsMatchingProfiles()
|
||||
{
|
||||
// Arrange
|
||||
await _fixture.ClearDatabaseAsync();
|
||||
|
||||
// Add active profiles
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var dto = new CfgProfileDto
|
||||
{
|
||||
Active = true,
|
||||
ProfileName = $"Active Profile {i}",
|
||||
TypeId = (byte)1,
|
||||
Schedule = "0 0 * * *",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
ChangedWho = "TestUser",
|
||||
ChangedWhen = DateTime.Now
|
||||
};
|
||||
await _fixture.ProfileRepository.AddAsync(dto);
|
||||
}
|
||||
|
||||
// Add inactive profiles
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var dto = new CfgProfileDto
|
||||
{
|
||||
Active = false,
|
||||
ProfileName = $"Inactive Profile {i}",
|
||||
TypeId = (byte)1,
|
||||
Schedule = "0 0 * * *",
|
||||
AddedWho = "TestUser",
|
||||
AddedWhen = DateTime.Now,
|
||||
ChangedWho = "TestUser",
|
||||
ChangedWhen = DateTime.Now
|
||||
};
|
||||
await _fixture.ProfileRepository.AddAsync(dto);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await _fixture.ProfileRepository.FindAsync(p => p.Active);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
result.Should().OnlyContain(p => p.Active);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user