< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Information
Class: ZhEaIsNsAaBn.IntegrationTestingBases.SqlServerTestFixture<T1, T2>
Assembly: ZhEaIsNsAaBn.IntegrationTestingBases
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/TestPackages/ZhEaIsNsAaBn.IntegrationTestingBases/Fixtures/SqlServerTestFixture.cs
Line coverage
87%
Covered lines: 51
Uncovered lines: 7
Coverable lines: 58
Total lines: 234
Line coverage: 87.9%
Branch coverage
75%
Covered branches: 9
Total branches: 12
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_TablesToIgnore()100%210%
get_SchemasToInclude()100%11100%
get_DatabaseName()100%210%
get_ConnectionStringName()100%210%
get_SqlServerImage()100%11100%
GetAdditionalConfiguration()100%210%
GetDbContext()50%22100%
GetOpenedDbConnectionAsync()100%1157.14%
InitializeRespawnerAsync()100%11100%
ResetDatabaseAsync()50%22100%
RunBeforeAnyTestsAsync()100%11100%
RunAfterAllTestsAsync()87.5%88100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/TestPackages/ZhEaIsNsAaBn.IntegrationTestingBases/Fixtures/SqlServerTestFixture.cs

#LineLine coverage
 1#region Usings
 2using System.Data.Common;
 3using System.Diagnostics.CodeAnalysis;
 4using Microsoft.Data.SqlClient;
 5using Respawn;
 6using Respawn.Graph;
 7using Testcontainers.MsSql;
 8#endregion
 9
 10namespace ZhEaIsNsAaBn.IntegrationTestingBases;
 11
 12/// <summary>
 13/// Base NUnit set-up fixture that starts SQL Server in Docker (Testcontainers), hosts the app under test with
 14/// <see cref="CustomWebApplicationFactory{TProgram}"/>, and resets the database between tests with Respawn.
 15/// Derive one class per test project and implement <see cref="InitializeDatabaseAsync"/>.
 16/// </summary>
 17/// <typeparam name="TProgram">The <c>Program</c> class of the web application under test.</typeparam>
 18/// <typeparam name="TDbContext">The app's <see cref="DbContext"/>. It must be registered with <c>AddDbContextFactory</c
 19[ SetUpFixture ]
 20[ SuppressMessage( "Design", "CA1001:Types that own disposable fields should be disposable",
 21                   Justification = "Disposed in the [OneTimeTearDown] method RunAfterAllTestsAsync, which NUnit always r
 22public abstract class SqlServerTestFixture< TProgram, TDbContext >
 23    where TProgram : class
 24    where TDbContext : DbContext
 25{
 26    private const string IndexRebuildScript = """
 27                                              DECLARE @TableName NVARCHAR(517)
 28                                              DECLARE @sql NVARCHAR(1000)
 29                                              DECLARE TableCursor CURSOR FOR
 30                                                  SELECT QUOTENAME(OBJECT_SCHEMA_NAME([object_id])) + '.' + QUOTENAME(na
 31                                              OPEN TableCursor
 32                                              FETCH NEXT FROM TableCursor INTO @TableName
 33                                              WHILE @@FETCH_STATUS = 0
 34                                              BEGIN
 35                                                  SET @sql = 'ALTER INDEX ALL ON ' + @TableName + ' REBUILD WITH (FILLFA
 36                                                  EXEC (@sql)
 37                                                  FETCH NEXT FROM TableCursor INTO @TableName
 38                                              END
 39                                              CLOSE TableCursor
 40                                              DEALLOCATE TableCursor
 41                                              """;
 42
 643    private string _connectionString = string.Empty;
 44
 45
 46    /// <summary>
 47    /// The fixture NUnit started for this <typeparamref name="TProgram"/> / <typeparamref name="TDbContext"/> pair, or 
 48    /// when none is running. Set at the end of <see cref="RunBeforeAnyTestsAsync"/> and cleared in <see cref="RunAfterA
 49    /// <see cref="IntegrationTestBase{TProgram, TDbContext}"/> uses it when no fixture was set explicitly.
 50    /// </summary>
 51    [ SuppressMessage( "Design", "CA1000:Do not declare static members on generic types",
 52                       Justification = "One registry per app/context pair is intended; callers reach it through Integrat
 53    public static SqlServerTestFixture<TProgram, TDbContext>? Current { get; private set; }
 54
 55    /// <summary>The HTTP client for the app under test. Set by <see cref="RunBeforeAnyTestsAsync"/>.</summary>
 56    protected internal HttpClient? Client { get; protected set; }
 57
 58    /// <summary>The factory hosting the app under test. Set by <see cref="RunBeforeAnyTestsAsync"/>.</summary>
 59    protected WebApplicationFactory<TProgram>? Factory { get; set; }
 60
 61    /// <summary>The SQL Server container. Set by <see cref="RunBeforeAnyTestsAsync"/>.</summary>
 62    protected MsSqlContainer? SqlContainer { get; set; }
 63
 64    /// <summary>The Respawn instance that clears data. Set by <see cref="InitializeRespawnerAsync"/>.</summary>
 65    protected Respawner? DatabaseRespawner { get; set; }
 66
 67    /// <summary>Tables kept during a reset, in addition to <c>__EFMigrationsHistory</c>. Default: none.</summary>
 068    protected virtual Table[] TablesToIgnore => [];
 69
 70    /// <summary>Schemas cleared during a reset. Default: <c>dbo</c>.</summary>
 371    protected virtual string[] SchemasToInclude => [ "dbo" ];
 72
 73    /// <summary>The database name inside the container. Default: <c>TestDatabase</c>.</summary>
 074    protected virtual string DatabaseName => "TestDatabase";
 75
 76    /// <summary>
 77    /// The connection-string name the app under test reads (<c>GetConnectionString( name )</c>). The container's connec
 78    /// string is set at <c>ConnectionStrings:{name}</c>. Default: <c>DefaultConnection</c>.
 79    /// </summary>
 080    protected virtual string ConnectionStringName => CustomWebApplicationFactory<TProgram>.DefaultConnectionStringName;
 81
 82    /// <summary>The SQL Server Docker image. Default: <c>mcr.microsoft.com/mssql/server:2022-latest</c>.</summary>
 383    protected virtual string SqlServerImage => "mcr.microsoft.com/mssql/server:2022-latest";
 84
 85    /// <summary>Extra configuration values for the app under test (key = configuration path). Default: none.</summary>
 86    /// <returns>The values to add to the app's configuration.</returns>
 087    protected virtual Dictionary<string, string> GetAdditionalConfiguration() => [];
 88
 89    /// <summary>Prepares the database once per run: migrations, seed data and so on.</summary>
 90    /// <param name="scope">A service scope of the app under test.</param>
 91    /// <param name="cancellationToken">Token to cancel the operation.</param>
 92    /// <returns>A task that completes when the database is ready.</returns>
 93    protected abstract Task InitializeDatabaseAsync( IServiceScope scope, CancellationToken cancellationToken = default 
 94
 95    /// <summary>Creates a new <typeparamref name="TDbContext"/> from the app's <see cref="IDbContextFactory{TContext}"/
 96    /// <returns>A new context connected to the test database.</returns>
 97    /// <exception cref="InvalidOperationException"><see cref="RunBeforeAnyTestsAsync"/> has not run yet.</exception>
 98    protected internal TDbContext GetDbContext()
 99    {
 27100        if ( Factory is null ) { throw new InvalidOperationException( "Factory is not initialized. Ensure RunBeforeAnyTe
 101
 102
 27103        using var scope = Factory.Services.CreateScope();
 104
 27105        return scope.ServiceProvider
 27106                    .GetRequiredService<IDbContextFactory<TDbContext>>()
 27107                    .CreateDbContext();
 27108    }
 109
 110    /// <summary>Opens a new connection to the test database. The caller disposes it.</summary>
 111    /// <param name="cancellationToken">Token to cancel the operation.</param>
 112    /// <returns>An open connection.</returns>
 113    protected async Task<DbConnection> GetOpenedDbConnectionAsync( CancellationToken cancellationToken = default )
 114    {
 18115        var connection = new SqlConnection( _connectionString );
 116
 117        try
 118        {
 18119            await connection.OpenAsync( cancellationToken ).ConfigureAwait( false );
 120
 18121            return connection;
 122        }
 0123        catch
 124        {
 0125            await connection.DisposeAsync().ConfigureAwait( false );
 126
 0127            throw;
 128        }
 18129    }
 130
 131    /// <summary>Creates the <see cref="DatabaseRespawner"/>, ignoring <c>__EFMigrationsHistory</c> and <see cref="Table
 132    /// <param name="cancellationToken">Token to cancel the operation.</param>
 133    /// <returns>A task that completes when the respawner is ready.</returns>
 134    protected virtual async Task InitializeRespawnerAsync( CancellationToken cancellationToken = default )
 135    {
 3136        var connection = await GetOpenedDbConnectionAsync( cancellationToken ).ConfigureAwait( false );
 137
 3138        await using ( connection.ConfigureAwait( false ) )
 139        {
 3140            Table[] tablesToIgnore = [ "__EFMigrationsHistory", .. TablesToIgnore ];
 141
 142
 3143            DatabaseRespawner = await Respawner.CreateAsync( connection, new RespawnerOptions
 3144                                                                         {
 3145                                                                             SchemasToInclude = SchemasToInclude,
 3146                                                                             TablesToIgnore   = tablesToIgnore,
 3147                                                                             DbAdapter        = DbAdapter.SqlServer,
 3148                                                                             WithReseed       = true
 3149                                                                         } )
 3150                                               .ConfigureAwait( false );
 151        }
 3152    }
 153
 154    /// <summary>Deletes all data (except ignored tables), reseeds identities and rebuilds indexes.</summary>
 155    /// <param name="cancellationToken">Token to cancel the operation.</param>
 156    /// <returns>A task that completes when the database is clean.</returns>
 157    /// <exception cref="InvalidOperationException"><see cref="InitializeRespawnerAsync"/> has not run yet.</exception>
 158    public virtual async Task ResetDatabaseAsync( CancellationToken cancellationToken = default )
 159    {
 15160        if ( DatabaseRespawner is null ) { throw new InvalidOperationException( "Database respawner is not initialized."
 161
 162
 15163        var connection = await GetOpenedDbConnectionAsync( cancellationToken ).ConfigureAwait( false );
 164
 15165        await using ( connection.ConfigureAwait( false ) )
 166        {
 15167            await DatabaseRespawner.ResetAsync( connection ).ConfigureAwait( false );
 168        }
 169
 170
 15171        var context = GetDbContext();
 172
 15173        await using ( context.ConfigureAwait( false ) )
 174        {
 15175            await context.Database.ExecuteSqlRawAsync( IndexRebuildScript, cancellationToken ).ConfigureAwait( false );
 176        }
 15177    }
 178
 179    /// <summary>
 180    /// Starts the container, creates <see cref="Factory"/> and <see cref="Client"/>, then runs
 181    /// <see cref="InitializeDatabaseAsync"/> and <see cref="InitializeRespawnerAsync"/>.
 182    /// </summary>
 183    /// <returns>A task that completes when the environment is ready.</returns>
 184    [ OneTimeSetUp ]
 185    public virtual async Task RunBeforeAnyTestsAsync()
 186    {
 3187        SqlContainer = new MsSqlBuilder( SqlServerImage ).Build();
 188
 3189        await SqlContainer.StartAsync().ConfigureAwait( false );
 190
 191
 3192        _connectionString = new SqlConnectionStringBuilder( SqlContainer.GetConnectionString() ) { InitialCatalog = Data
 3193        Factory           = new CustomWebApplicationFactory<TProgram>( _connectionString, "Testing", GetAdditionalConfig
 3194        Client            = Factory.CreateClient();
 195
 196
 3197        var scope = Factory.Services.CreateAsyncScope();
 198
 3199        await using ( scope.ConfigureAwait( false ) )
 200        {
 3201            await InitializeDatabaseAsync( scope, CancellationToken.None ).ConfigureAwait( false );
 202        }
 203
 204
 3205        await InitializeRespawnerAsync().ConfigureAwait( false );
 206
 207
 3208        Current = this;
 3209    }
 210
 211    /// <summary>
 212    /// Clears <see cref="Current"/> (if it is this fixture), disposes <see cref="Client"/>, stops and disposes the cont
 213    /// and disposes <see cref="Factory"/>.
 214    /// </summary>
 215    /// <returns>A task that completes when everything is released.</returns>
 216    [ OneTimeTearDown ]
 217    public virtual async Task RunAfterAllTestsAsync()
 218    {
 6219        if ( ReferenceEquals( Current, this ) ) { Current = null; }
 220
 221
 3222        Client?.Dispose();
 223
 224
 3225        if ( SqlContainer is not null )
 226        {
 3227            await SqlContainer.StopAsync().ConfigureAwait( false );
 3228            await SqlContainer.DisposeAsync().ConfigureAwait( false );
 229        }
 230
 231
 6232        if ( Factory is not null ) { await Factory.DisposeAsync().ConfigureAwait( false ); }
 3233    }
 234}