【问题标题】:Run single test against multiple configurations in Visual Studio在 Visual Studio 中针对多个配置运行单个测试
【发布时间】:2019-12-06 06:39:00
【问题描述】:

我们使用 xUnitMicrosoft.AspNetCore.TestHost.TestServer 设置了集成测试,以针对在 ASP.NET Core 2.2 上运行的 Web API 运行测试。

我们的 Web API 是一个单一的代码库,可以根据一些配置或应用程序设置差异(例如 countrycurrency 等)单独部署多次。

下图试图解释我们的部署设置:

我们希望确保我们的集成测试针对所有部署运行。

对于这两种部署,XX` API 端点、请求和响应绝对相同。因此,我们希望避免在每次部署的集成测试中重复自己。

这是解释我们当前测试设置的示例代码:

TestStartup.cs

public class TestStartup : IStartup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
       var configuration = new ConfigurationBuilder()
           .SetBasePath(Directory.GetCurrentDirectory())
           .AddJsonFile("appsettings.json", false)
           .AddEnvironmentVariables()
           .Build();

        services.AddMvc()
            .SetCompatibilityVersion(version: CompatibilityVersion.Version_2_2);

        // Code to add required services based on configuration


        return services.BuildServiceProvider();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();

        // Code to configure test Startup
    }
}

TestServerFixture.cs

public class TestServerFixture
{

    public TestServerFixture()
    {
        var builder = new WebHostBuilder().ConfigureServices(services =>
        {
            services.AddSingleton<IStartup>(new TestStartup());
        });

        var server = new TestServer(builder);
        Client = server.CreateClient();
    }

    public HttpClient Client { get; private set; }
}

MyTest.cs

public class MyTest : IClassFixture<TestServerFixture>
{
    private readonly TestServerFixture _fixture;

    public MyTest(TestServerFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void ItShouldExecuteTwice_AgainstTwoSeparateConfigurations()
    {
        //...
    }
}

现在,我希望在 MyTest 类中针对两个不同的配置/应用设置或换句话说针对 Visual Studio 中的两个不同的测试部署多次运行 ItShouldExecuteTwice_AgainstTwoSeparateConfigurations 测试。

  • 我知道,我应该能够使用构建配置(如DEBUG_SETTING1DEBUG_SETTING2)和预处理器指令(#if DEBUG_SETTING1)的组合来实现这一点。

  • 另一种选择可能是拥有一个包含常用方法的基本测试帮助程序项目,并为每个部署创建一个单独的集成项目。

有没有更好更优雅的方法来实现这一点?

【问题讨论】:

  • 通过配置,您的意思是 1) 构建配置,如发布与调试? 2)或运行时的配置文件 3)或预处理器宏? #1 和 #2 只是编译时,你需要不同的程序集。此外,#3 不受欢迎,您应该首先考虑编写多个测试或重构代码以避免需要。
  • @TanveerBadar 我已经用更多信息更新了这个问题。希望它更清楚。我也很惊讶为什么我投了反对票,这个问题不是很好吗? downvoter 可以解释如何改进这个问题吗?
  • 我没有否决你的问题,但你永远不认识人。这取决于遇到它的人的心情。在我上面的评论中还发现了一个错字,它应该是“#1 and #3 are compile time”。
  • 我明白这一点。感谢您的澄清。我只是不知道我可以做些什么来解释我的问题。
  • 您的部署管道设置如何?为什么不通过触发它作为部署脚本的一部分来运行具有不同设置的测试呢?我相信在 CI/CD 级别,您可以决定需要进行哪些部署。然后为每个部署运行集成测试也很有意义。因此,只需在您的部署设置中运行一个新任务,该任务将为给定部署设置环境变量并运行测试。对每个部署重复此操作。这样,当有不同设置的新部署时,您无需更改测试代码。

标签: c# asp.net-core .net-core integration-testing xunit


【解决方案1】:

重构测试启动以允许根据测试需要对其进行修改

例如

public class TestStartup : IStartup {
    private readonly string settings;

    public TestStartup(string settings) {
        this.settings = settings;
    }

    public void ConfigureServices(IServiceCollection services) {
       var configuration = new ConfigurationBuilder()
           .SetBasePath(Directory.GetCurrentDirectory())
           .AddJsonFile(settings, false) //<--just an example
           .AddEnvironmentVariables()
           .Build();

        services.AddMvc()
            .SetCompatibilityVersion(version: CompatibilityVersion.Version_2_2);

        //...Code to add required services based on configuration

    }

    public void Configure(IApplicationBuilder app) {
        app.UseMvc();

        //...Code to configure test Startup
    }
}

并让该模式通过夹具过滤

public class TestServerFixture {
    static readonly Dictionary<string, TestServer> cache = 
        new Dictionary<string, TestServer>();

    public TestServerFixture() {
        //...
    }

    public HttpClient GetClient(string settings) {
        TestServer server = null;
        if(!cache.TryGetValue(settings, out server)) {
            var startup = new TestStartup(settings); //<---
            var builder = new WebHostBuilder()
                .ConfigureServices(services => {
                    services.AddSingleton<IStartup>(startup);
                });
            server = new TestServer(builder);
            cache.Add(settings, server);
        }
        return server.CreateClient();
    }
}

最终是测试本身

public class MyTest : IClassFixture<TestServerFixture> {
    private readonly TestServerFixture fixture;

    public MyTest(TestServerFixture fixture) {
        this.fixture = fixture;
    }

    [Theory]
    [InlineData("settings1.json")]
    [InlineData("settings2.json")]
    public async Task Should_Execute_Using_Configurations(string settings) {
        var client = fixture.CreateClient(settings);

        //...use client

    }
}

【讨论】:

  • 嗨@Nkosi,这样我需要为每个客户端/服务器安排、执行和断言两次,这正是我试图避免的。
  • 嗨@Nkosi,您更新的答案将不起作用,因为服务器级别的设置不同。测试服务器已经在 Fixture 中设置好了。在 FactTheory 中提供这个设置会很晚..
  • @AnkitVijay 不会。当客户端被请求时,测试服务器由夹具设置。请注意,它没有在构造函数中完成。如果是这样,我会同意你的说法。
  • 我认为我看到的这种方法的一个副作用是,TestServer 必须为我在单个类中的每个测试创建。这可能会对性能产生一些影响。但我应该能够通过为每个设置/部署创建测试服务器的静态实例来缓解它
  • @AnkitVijay 可以通过本地缓存轻松修复
【解决方案2】:

@Nkosi 的帖子非常适合我们的场景和我提出的问题。这是一种简单、干净且易于理解的方法,具有最大的可重用性。答案给满分。

但是,我无法继续采用这种方法有几个原因:

  • 在建议的方法中,我们不能只对一个特定的setting 运行测试。它对我们未来很重要的原因是,可能两个不同的团队维护他们的具体实施和部署。使用Theory,在所有测试中只运行一个setting 变得有点困难。

  • 我们很可能需要针对每个设置/部署使用两个单独的构建和部署管道。

  • 虽然 API 端点 RequestResponse 现在完全一样,但我们不知道随着我们的开发继续这种情况。

由于上述原因,我们还考虑了以下两种方法:

方法 1

有一个共同的class 库,它具有共同的FixtureTests 作为abstract

  • 项目 Common.IntegrationTests

TestStartup.cs

public abstract class TestStartup : IStartup
{
    public abstract IServiceProvider ConfigureServices(IServiceCollection services);

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvc();

        // Code to configure test Startup
    }
}

TestServerFixture.cs

public abstract class TestServerFixture
{

    protected TestServerFixture(IStartup startup)
    {
        var builder = new WebHostBuilder().ConfigureServices(services =>
        {
            services.AddSingleton<IStartup>(startup);
        });

        var server = new TestServer(builder);
        Client = server.CreateClient();
    }

    public HttpClient Client { get; private set; }
}

MyTest.cs

public abstract class MyTest
{
    private readonly TestServerFixture _fixture;

    protected MyTest(TestServerFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void ItShouldExecuteTwice_AgainstTwoSeparateConfigurations()
    {
        //...
    }
}
  • 项目Setting1.IntegrationTests(参考Common.IntegrationTests

TestStartup.cs

public class TestStartup : Common.IntegrationTests.TestStartup
{
    public override IServiceProvider ConfigureServices(IServiceCollection services)
    {
       var configuration = new ConfigurationBuilder()
           .SetBasePath(Directory.GetCurrentDirectory())
           .AddJsonFile("appsettings.json", false) // appsettings for Setting1
           .AddEnvironmentVariables()
           .Build();

        services.AddMvc()
            .SetCompatibilityVersion(version: CompatibilityVersion.Version_2_2);

        // Code to add required services based on configuration


        return services.BuildServiceProvider();
    }
}

TestServerFixture.cs

public class TestServerFixture : Fixtures.TestServerFixture
{
    public TestServerFixture() : base(new TestStartup())
    {
    }
}

MyTests.cs

public class MyTests : Common.IntegrationTests.MyTests, IClassFixture<TestServerFixture>
{
    public MyTests(TestServerFixture fixture) : base(fixture)
    {
    }
}
  • 项目Setting2.IntegrationTests(参考Common.IntegrationTests

Setting1.IntegrationTests

类似的结构

这种方法很好地平衡了可重用性和独立运行/修改测试的灵活性。但是,我仍然不是 100% 相信这种方法,因为它意味着对于每个常见的 Test 类,我们需要有一个实现,除了调用 base constructor 之外我们什么都不做。

方法 2

在第二种方法中,我们进一步采用了方法 1,并尝试通过 Shared Project 解决我们在方法 1 中遇到的问题。来自文档:

共享项目可让您编写由 不同应用项目的数量。代码作为一部分编译 每个引用项目,并且可以包括编译器指令 帮助将特定于平台的功能合并到共享代码中 基地。

Shared Project 为我们提供了两全其美的体验,没有 link 文件的丑陋和不必要的类 inheritanceabstraction。我们的新设置如下:

编辑: 我为此写了一篇博文,详细讨论了我们的用例和解决方案。这是链接:

https://ankitvijay.net/2020/01/04/running-an-asp-net-core-application-against-multiple-db-providers-part-2/

【讨论】:

    猜你喜欢
    • 2016-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-19
    • 2018-07-23
    相关资源
    最近更新 更多