Интеграционные тесты для ASP.NET Core 2.1 Web API всегда выполняются в режиме «Разработка» - PullRequest
0 голосов
/ 16 марта 2019

Я хотел узнать, как создать API, включающий ASP.NET Core API 2.1 + Dapper + DbUp + Azure Key Vault и размещенный внутри Azure App Service (Web App), а также управлять им внутри Azure DevOps project with CI/CD.

Я использую 2 среды (Разработка и Производство), и в Azure App Service (Web App), где я это развернул, я установил ASPNETCORE_ENVIRONMENT со значением, установленным для Производство

В Azure DevOps я выбрал ASP.NET Core Template для своего конвейера сборки (я использую конструктор, а не YAML), и я столкнулся с проблемой при запуске «тестов».

The error in build pipeline

Тесты, которые я реализовал, являются "интеграционными тестами", и они всегда выполняются в среде Development. Чтобы решить эту проблему, я реализовал пользовательский WebApplicationFactory, как указано в ссылках ниже,

Сможете ли вы помочь мне решить проблему, пожалуйста?

Ниже приведены фрагменты кода в моем приложении.

  • appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "DatabaseConfig": {
    "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BlogDb;Integrated Security=True;Connect Timeout=30;"
  }
}
  • appsettings.Production.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "AzureKeyVaultUrl": "THE AKV URL",
  "DatabaseConfig": {
    "ConnectionString": "BlogConnectionString"
  }
}
  • Мои настройки базы данных находятся в классе DatabaseConfig, и в режиме разработки NON он будет использовать AKV для получения данных строки подключения.

То есть в Program.cs

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, builder) =>
        {
            if (!context.HostingEnvironment.IsDevelopment())
            {
                var akvUrl = builder.Build()[AzureKeyVaultUrl];
                var tokenProvider = new AzureServiceTokenProvider();
                var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));

                builder.AddAzureKeyVault(akvUrl, keyVaultClient, new DefaultKeyVaultSecretManager());
            }
        })
        .UseStartup<Startup>();
}

В Startup.cs

public void ConfigureDevelopmentServices(IServiceCollection services)
{
    RegisterDevDepdendencies(services);
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void ConfigureProductionServices(IServiceCollection services)
{
    RegisterDependencies(services);
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

private void RegisterDevDepdendencies(IServiceCollection services)
{
    //
    // Register configurations
    //
    services.Configure<DatabaseConfig>(Configuration.GetSection("DatabaseConfig"));
    services.AddSingleton(provider =>
    {
        var dbConfig = provider.GetRequiredService<IOptions<DatabaseConfig>>().Value;
        return dbConfig;
    });
    //
    // Register repositories
    //
    services.AddTransient<IPostsRepository, PostsRepository>();
    //
    // Register filters
    //
    services.AddTransient<IStartupFilter, DevDatabaseInitFilter>();
}

private void RegisterDependencies(IServiceCollection services)
{
    //
    // Register configurations
    //
    services.Configure<DatabaseConfig>(Configuration.GetSection("DatabaseConfig"));
    services.AddSingleton(provider =>
    {
        var dbConfig = provider.GetRequiredService<IOptions<DatabaseConfig>>().Value;
        var connectionString = Configuration[dbConfig.ConnectionString];
        return new DatabaseConfig {ConnectionString = connectionString};
    });
    //
    // Register repositories
    //
    services.AddTransient<IPostsRepository, PostsRepository>();
    //
    // Register filters
    //
    services.AddTransient<IStartupFilter, DatabaseInitFilter>();
}

Реализации фильтра запуска

Фильтры запуска должны выполнять миграцию с использованием DbUp. Я запускаю это как задачу запуска (это было вдохновлено после прочтения потрясающей статьи от Andrew Lock ). Ниже приведены их реализации.

public class DevDatabaseInitFilter : IStartupFilter
{
    private readonly string _connectionString;

    public DevDatabaseInitFilter(DatabaseConfig config)
    {
        _connectionString = config?.ConnectionString;
        if (string.IsNullOrEmpty(_connectionString))
        {
            throw new ArgumentNullException(nameof(config));
        }
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        EnsureDatabase.For.SqlDatabase(_connectionString);

        var dbUpgradeEngineBuilder = DeployChanges.To
            .SqlDatabase(_connectionString)
            .WithScriptsEmbeddedInAssembly(typeof(DatabaseInitFilter).Assembly)
            .WithTransaction()
            .LogToAutodetectedLog();

        var dbUpgradeEngine = dbUpgradeEngineBuilder.Build();

        if (dbUpgradeEngine.IsUpgradeRequired())
        {
            var upgradeOperation = dbUpgradeEngine.PerformUpgrade();
            if (!upgradeOperation.Successful)
            {
                throw new Exception("Database upgrade unsuccessful", upgradeOperation.Error);
            }
        }

        return next;
    }
}

public class DatabaseInitFilter : IStartupFilter
{
    private readonly string _connectionString;

    public DatabaseInitFilter(DatabaseConfig config)
    {   
        _connectionString = config?.ConnectionString;

        if (string.IsNullOrEmpty(_connectionString))
        {
            throw new ArgumentNullException(nameof(config));
        }
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        EnsureDatabase.For.SqlDatabase(_connectionString);

        var dbUpgradeEngineBuilder = DeployChanges.To
            .SqlDatabase(_connectionString)
            .WithScriptsEmbeddedInAssembly(typeof(DatabaseInitFilter).Assembly)
            .WithTransaction()
            .LogToAutodetectedLog();

        var dbUpgradeEngine = dbUpgradeEngineBuilder.Build();

        if (dbUpgradeEngine.IsUpgradeRequired())
        {
            var upgradeOperation = dbUpgradeEngine.PerformUpgrade();
            if (!upgradeOperation.Successful)
            {
                throw new Exception("Database upgrade unsuccessful", upgradeOperation.Error);
            }
        }

        return next;
    }
}

И, наконец, связанные с тестом классы

public class TestWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
    private const string AspNetCoreEnvironment = "ASPNETCORE_ENVIRONMENT";

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        var env = Environment.GetEnvironmentVariable(AspNetCoreEnvironment);
        if (!string.IsNullOrEmpty(env))
        {
            builder.UseEnvironment(env);
        }
    }
}

public class PostsControllerTests : IClassFixture<TestWebApplicationFactory<Startup>>
{
    private HttpClient _client;

    public PostsControllerTests(TestWebApplicationFactory<Startup> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task Must_Have_Posts()
    {

        //
        // Arrange and Act
        //
        var httpResponse = await _client.GetAsync(@"/api/posts").ConfigureAwait(false);
        //
        // Assert
        //
        httpResponse.EnsureSuccessStatusCode();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...