diff --git a/Frank.Testing.EntityFrameworkCore/DbContextBuilder.cs b/Frank.Testing.EntityFrameworkCore/DbContextBuilder.cs new file mode 100644 index 0000000..8bfc90d --- /dev/null +++ b/Frank.Testing.EntityFrameworkCore/DbContextBuilder.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Frank.Testing.EntityFrameworkCore; + +/// +/// Builder class for constructing an instance of DbContext. +/// +/// The type of DbContext. +public class DbContextBuilder where T : DbContext +{ + private readonly IServiceCollection _serviceCollection = new ServiceCollection(); + + private Action? _configuredOptions; + + private ILoggerFactory? _loggerFactory; + private string _sqliteConnectionString = "Data Source=:memory:"; + + public DbContextBuilder WithLoggerProvider(ILoggerProvider loggerProvider) + { + _loggerFactory = LoggerFactory.Create(builder => builder.ClearProviders().AddProvider(loggerProvider)); + return this; + } + + public DbContextBuilder WithSqliteConnectionString(string sqliteConnectionString) + { + _sqliteConnectionString = sqliteConnectionString; + return this; + } + + public DbContextBuilder WithService(Action configureService) + { + configureService(_serviceCollection); + return this; + } + + public DbContextBuilder WithOptions(Action> configureOptions) + { + _configuredOptions = configureOptions as Action; + return this; + } + + public T Build() + { + _serviceCollection.AddDbContext(OptionsAction); + var context = _serviceCollection.BuildServiceProvider().GetRequiredService(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + return context; + } + + private void OptionsAction(IServiceProvider arg1, DbContextOptionsBuilder arg2) + { + _configuredOptions?.Invoke(arg2); + if (_loggerFactory != null) + arg2.UseLoggerFactory(_loggerFactory); + + arg2.EnableSensitiveDataLogging(); + arg2.EnableDetailedErrors(); + arg2.UseSqlite(_sqliteConnectionString); + } +} \ No newline at end of file diff --git a/Frank.Testing.EntityFrameworkCore/Frank.Testing.EntityFrameworkCore.csproj b/Frank.Testing.EntityFrameworkCore/Frank.Testing.EntityFrameworkCore.csproj new file mode 100644 index 0000000..f8ea016 --- /dev/null +++ b/Frank.Testing.EntityFrameworkCore/Frank.Testing.EntityFrameworkCore.csproj @@ -0,0 +1,11 @@ + + + + xUnit.net helpers and extensions for writing automated tests + xunit test testing TDD BDD + + + + + + diff --git a/Frank.Testing.Logging/Frank.Testing.Logging.csproj b/Frank.Testing.Logging/Frank.Testing.Logging.csproj index 17ca64a..12636ec 100644 --- a/Frank.Testing.Logging/Frank.Testing.Logging.csproj +++ b/Frank.Testing.Logging/Frank.Testing.Logging.csproj @@ -10,7 +10,7 @@ - + diff --git a/Frank.Testing.Logging/LoggingBuilderExtensions.cs b/Frank.Testing.Logging/LoggingBuilderExtensions.cs index cec129d..60f1b28 100644 --- a/Frank.Testing.Logging/LoggingBuilderExtensions.cs +++ b/Frank.Testing.Logging/LoggingBuilderExtensions.cs @@ -27,7 +27,7 @@ public static ILoggingBuilder AddPulseFlowTestLoggingProvider(this ILoggingBuild options.LogLevel = logLevel; }); builder.Services.AddPulseFlow(flowBuilder => flowBuilder.AddFlow()); - builder.Services.AddSingleton(provider => new TestLoggerProvider(provider.GetRequiredService())); + builder.Services.AddSingleton(provider => new TestLoggerProvider(provider.GetRequiredService(), provider.GetService() ?? new TestLoggerSettings())); return builder; } } \ No newline at end of file diff --git a/Frank.Testing.Logging/PulseFlowTestLogger.cs b/Frank.Testing.Logging/PulseFlowTestLogger.cs index 54c62de..8d9fd1c 100644 --- a/Frank.Testing.Logging/PulseFlowTestLogger.cs +++ b/Frank.Testing.Logging/PulseFlowTestLogger.cs @@ -6,14 +6,14 @@ namespace Frank.Testing.Logging; -public class PulseFlowTestLogger(IConduit conduit, string categoryName) : ILogger +public class PulseFlowTestLogger(IConduit conduit, string categoryName, TestLoggerSettings settings) : ILogger { public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => conduit.SendAsync(new LogPulse(logLevel, eventId, exception, categoryName, formatter(state, exception))).GetAwaiter().GetResult(); - public bool IsEnabled(LogLevel logLevel) => true; + public bool IsEnabled(LogLevel logLevel) => logLevel >= settings.LogLevel; public IDisposable? BeginScope(TState state) where TState : notnull => new PulseFlowLoggerScope(state); } -public class PulseFlowTestLogger(IConduit conduit) : PulseFlowTestLogger(conduit, typeof(T).GetDisplayName()), ILogger; \ No newline at end of file +public class PulseFlowTestLogger(IConduit conduit, TestLoggerSettings settings) : PulseFlowTestLogger(conduit, typeof(T).GetDisplayName(), settings), ILogger; \ No newline at end of file diff --git a/Frank.Testing.Logging/TestLoggerProvider.cs b/Frank.Testing.Logging/TestLoggerProvider.cs index 013a8e1..97ffa89 100644 --- a/Frank.Testing.Logging/TestLoggerProvider.cs +++ b/Frank.Testing.Logging/TestLoggerProvider.cs @@ -6,7 +6,7 @@ namespace Frank.Testing.Logging; -public class TestLoggerProvider(IConduit conduit) : ILoggerProvider +public class TestLoggerProvider(IConduit conduit, TestLoggerSettings settings) : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(); @@ -15,7 +15,7 @@ public ILogger CreateLogger(string categoryName) if (_loggers.TryGetValue(categoryName, out var logger)) return logger; - var newLogger = new PulseFlowTestLogger(conduit, categoryName); + var newLogger = new PulseFlowTestLogger(conduit, categoryName, settings); return _loggers.GetOrAdd(categoryName, newLogger); } diff --git a/Frank.Testing.Logging/TestLoggingOutputFlow.cs b/Frank.Testing.Logging/TestLoggingOutputFlow.cs index cd3a1a9..9c40151 100644 --- a/Frank.Testing.Logging/TestLoggingOutputFlow.cs +++ b/Frank.Testing.Logging/TestLoggingOutputFlow.cs @@ -14,7 +14,7 @@ public async Task HandleAsync(IPulse pulse, CancellationToken cancellationToken) { if (pulse is LogPulse logPulse) { - outputHelper.WriteLine(logPulse.Message); + outputHelper.WriteLine(logPulse.ToString()); await Task.CompletedTask; } } diff --git a/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj b/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj index a438db1..4c1ade4 100644 --- a/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj +++ b/Frank.Testing.TestOutputExtensions/Frank.Testing.TestOutputExtensions.csproj @@ -8,7 +8,7 @@ - + diff --git a/Frank.Testing.Tests/EntityFramworkCoreTests/DbContextBuilderTests.cs b/Frank.Testing.Tests/EntityFramworkCoreTests/DbContextBuilderTests.cs new file mode 100644 index 0000000..b48f233 --- /dev/null +++ b/Frank.Testing.Tests/EntityFramworkCoreTests/DbContextBuilderTests.cs @@ -0,0 +1,111 @@ +using Frank.PulseFlow; +using Frank.PulseFlow.Logging; +using Frank.Testing.EntityFrameworkCore; +using Frank.Testing.Logging; +using Frank.Testing.Tests.TestingInfrastructure; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +using Xunit.Abstractions; + +namespace Frank.Testing.Tests.EntityFramworkCoreTests; + +public class DbContextBuilderTests(ITestOutputHelper outputHelper) +{ + [Fact] + public void Build_WithLoggerProvider_UsesLoggerProvider() + { + var conduit = new TestConduit(); + var loggerProvider = new TestLoggerProvider(conduit, new TestLoggerSettings()); + var dbContext = new DbContextBuilder() + .WithLoggerProvider(loggerProvider) + .Build(); + dbContext.Database.EnsureCreated(); + dbContext.Database.ExecuteSqlRaw("SELECT 1"); + dbContext.Dispose(); + + outputHelper.WriteJson(conduit.Logs); + // var log = conduit.Logs.Single(); + // Assert.Equal("SELECT 1", log.Message); + } + + [Fact] + public void Build_WithService_UsesService() + { + var dbContext = new DbContextBuilder() + .WithService(services => services.AddSingleton()) + .Build(); + Assert.NotNull(dbContext.GetService()); + } + + [Fact] + public void Build_WithOptions_UsesOptions() + { + var dbContext = new DbContextBuilder() + .WithOptions(options => options.UseSqlite("Data Source=:memory:")) + .Build(); + dbContext.Database.EnsureCreated(); + dbContext.Database.ExecuteSqlRaw("SELECT 1"); + dbContext.Dispose(); + } + + [Fact] + public void Build_WithLoggerProviderAndService_UsesLoggerProviderAndService() + { + var conduit = new TestConduit(); + var loggerProvider = new TestLoggerProvider(conduit, new TestLoggerSettings()); + var dbContext = new DbContextBuilder() + .WithLoggerProvider(loggerProvider) + .WithSqliteConnectionString("Data Source=MyTestDatabase.db") + .WithService(services => services.AddSingleton()) + .Build(); + dbContext.Database.EnsureCreated(); + dbContext.Persons.Add(new TestPerson() { Name = "Frank" }); + dbContext.SaveChanges(); + dbContext.Dispose(); + } + + public class TestService : ITestService + { + public async Task DoSomethingAsync() + { + await Task.CompletedTask; + } + } + + public interface ITestService + { + Task DoSomethingAsync(); + } + + [Fact] + public void Build_WithLoggerProviderAndOptions_UsesLoggerProviderAndOptions() + { + var conduit = new TestConduit(); + var loggerProvider = new TestLoggerProvider(conduit, new TestLoggerSettings()); + var dbContext = new DbContextBuilder() + .WithSqliteConnectionString("Data Source=MyTestDatabase.db") + .WithLoggerProvider(loggerProvider) + .Build(); + dbContext.Database.EnsureCreated(); + dbContext.Persons.Add(new TestPerson() { Name = "Frank" }); + dbContext.SaveChanges(); + dbContext.Database.ExecuteSqlRaw("SELECT 1"); + dbContext.Dispose(); + + outputHelper.WriteJson(conduit.Logs); + } + + public class TestConduit : IConduit + { + public async Task SendAsync(IPulse message) + { + await Task.CompletedTask; + Logs.Add(message as LogPulse ?? throw new InvalidOperationException()); + } + + public List Logs { get; } = new(); + } +} diff --git a/Frank.Testing.Tests/Frank.Testing.Tests.csproj b/Frank.Testing.Tests/Frank.Testing.Tests.csproj index d779898..75f4672 100644 --- a/Frank.Testing.Tests/Frank.Testing.Tests.csproj +++ b/Frank.Testing.Tests/Frank.Testing.Tests.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -27,6 +27,7 @@ + diff --git a/Frank.Testing.Tests/TestLogging/TestLoggingTests.cs b/Frank.Testing.Tests/TestLogging/TestLoggingTests.cs index c17d3f2..63a668b 100644 --- a/Frank.Testing.Tests/TestLogging/TestLoggingTests.cs +++ b/Frank.Testing.Tests/TestLogging/TestLoggingTests.cs @@ -13,7 +13,7 @@ public class TestLoggingTests(ITestOutputHelper outputHelper) [Fact] public async Task Test1() { - var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1)); var host = CreateHostBuilder().Build(); await host.RunAsync(cancellationTokenSource.Token); @@ -40,7 +40,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (!stoppingToken.IsCancellationRequested) { logger.LogInformation("Hello from MyService"); - await Task.Delay(1000, stoppingToken); + await Task.Delay(100, stoppingToken); } } } diff --git a/Frank.Testing.Tests/TestingInfrastructure/TestAddress.cs b/Frank.Testing.Tests/TestingInfrastructure/TestAddress.cs index fef89b5..f3b8007 100644 --- a/Frank.Testing.Tests/TestingInfrastructure/TestAddress.cs +++ b/Frank.Testing.Tests/TestingInfrastructure/TestAddress.cs @@ -2,6 +2,8 @@ public class TestAddress { + public Guid Id { get; set; } + public string City { get; set; } public int ZipCode { get; set; } diff --git a/Frank.Testing.Tests/TestingInfrastructure/TestDbContext.cs b/Frank.Testing.Tests/TestingInfrastructure/TestDbContext.cs new file mode 100644 index 0000000..8d8b9b5 --- /dev/null +++ b/Frank.Testing.Tests/TestingInfrastructure/TestDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Frank.Testing.Tests.TestingInfrastructure; + +public class TestDbContext : DbContext +{ + public TestDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Persons { get; set; } + + public DbSet Addresses { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(e => e.Id); + modelBuilder.Entity().HasKey(e => e.Id); + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/Frank.Testing.Tests/TestingInfrastructure/TestPerson.cs b/Frank.Testing.Tests/TestingInfrastructure/TestPerson.cs index d16f6f5..c6bb8e8 100644 --- a/Frank.Testing.Tests/TestingInfrastructure/TestPerson.cs +++ b/Frank.Testing.Tests/TestingInfrastructure/TestPerson.cs @@ -2,6 +2,7 @@ public class TestPerson { + public Guid Id { get; set; } public string Name { get; set; } public int Age { get; set; } diff --git a/Frank.Testing.sln b/Frank.Testing.sln index 0e5aa9c..b0ace23 100644 --- a/Frank.Testing.sln +++ b/Frank.Testing.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.TestOutputExt EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.ApiTesting", "Frank.Testing.ApiTesting\Frank.Testing.ApiTesting.csproj", "{3B30D75C-5B95-4E87-B862-7E8DC49038DF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frank.Testing.EntityFrameworkCore", "Frank.Testing.EntityFrameworkCore\Frank.Testing.EntityFrameworkCore.csproj", "{A64BD8FC-364F-40E5-A276-F1DEA9D98F67}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,5 +49,9 @@ Global {3B30D75C-5B95-4E87-B862-7E8DC49038DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B30D75C-5B95-4E87-B862-7E8DC49038DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {3B30D75C-5B95-4E87-B862-7E8DC49038DF}.Release|Any CPU.Build.0 = Release|Any CPU + {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A64BD8FC-364F-40E5-A276-F1DEA9D98F67}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal