在Visual Studio中针对多个配置运行单个测试

7
我们使用xUnitMicrosoft.AspNetCore.TestHost.TestServer来设置集成测试,并针对在ASP.NET Core 2.2上运行的Web API运行测试。
我们的Web API是单一代码库,根据某些配置或应用程序设置差异(如国家货币等)将分别部署多次。
下面的图表试图解释我们的部署设置: enter image description here 我们希望确保我们的集成测试针对所有部署运行。
对于部署和,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()
    {
        //...
    }
}

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

  • 我知道,可以通过构建配置的组合(如DEBUG_SETTING1DEBUG_SETTING2)和预处理指令(#if DEBUG_SETTING1)来实现这一点。

  • 另一个选项是创建一个基本的测试助手项目,包含常见方法,以及为每个部署创建单独的集成项目。

是否有更好、更优雅的方法来实现这个目标呢?


通过配置,您是指1)构建配置,例如Release vs Debug吗?2)还是运行时的配置文件3)或预处理器宏?#1和#2仅在编译时存在,您需要不同的程序集。此外,#3不被赞同,您应该考虑编写多个测试或重构代码以避免首先需要。 - Tanveer Badar
@TanveerBadar 我已经更新了问题的更多信息,希望这样会更清楚。我也很惊讶为什么会被踩,难道问题表述不清楚吗?踩的人可以解释一下如何改进这个问题吗? - Ankit Vijay
1
我没有给你的问题投反对票,但你永远不知道别人的心情。这取决于遇到它的人的心情。另外,在我的上面的评论中发现了一个错别字,应该是“#1和#3是编译时”。 - Tanveer Badar
我明白了,感谢您的澄清。我只是不知道我该如何做得更好来解释我的问题。 - Ankit Vijay
你的部署流程是怎样设置的?为什么不通过触发部署脚本中的不同设置来运行测试呢?我认为在 CI/CD 层面,你可以决定需要进行哪些部署。然后,针对每个部署进行集成测试也是有意义的。因此,在部署设置中简单地运行一个新任务,该任务将为给定的部署设置环境变量并运行测试即可。对于每个部署重复执行这个步骤。这样,当有新的不同设置的部署时,您就不需要更改测试代码了。 - tubakaya
我看到你提到在 "Visual Studio" 中运行。我猜你也想能够在本地针对不同的端点运行测试。在这种情况下,您可以为每个环境创建appsettings.{environment}.json文件。然后使用不同的构建配置来设置您正在运行的环境。TestStartup中的代码将加载正确的appsettings文件。在部署管道中,您可以按照我之前的评论所述进行操作。请告诉我这是否有帮助。 - tubakaya
2个回答

5

重构测试启动程序,以便根据需要修改其测试方式。

例如:

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,这样我需要为每个客户端/服务器安排、执行和断言两次,这正是我试图避免的。 - Ankit Vijay
嗨@Nkosi,你更新的答案行不通,因为服务器级别的设置不同。测试服务器已经在Fixture中设置好了.. 在FactTheory中提供这个设置会很晚.. - Ankit Vijay
@AnkitVijay 不会的。当客户端请求时,测试服务器是由fixture设置的。请注意,这不是在构造函数中完成的。如果是的话,我就会同意你的说法。 - Nkosi
@AnkitVijay 这可以通过本地缓存轻松解决。 - Nkosi
嗨@Nkosi,你的答案非常好并且有效。然而,我们采取了稍微不同的方法,我将发布我的答案和方法背后的原因。话虽如此,我已将您的回复标记为答案。 - Ankit Vijay
显示剩余2条评论

3

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

但是,我不能使用这种方法的原因有几个:

  • 在建议的方法中,我们无法仅运行一个特定的setting进行测试。原因在于,将来可能会有两个不同的团队维护其各自的实现和部署。使用Theory,对于所有测试仅运行一个setting会稍微有些困难。

  • 很可能我们需要为每个设置/部署使用两个单独的构建和部署流水线。

  • 虽然API端点,RequestResponse今天完全相同,但我们不知道在开发过程中是否会继续保持这种情况。

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

方法1

拥有一个通用的class库,其中包含公共的FixtureTests作为abstract

Approach 1: Project Structure

  • 项目 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相似的结构

这种方法提供了重复使用性和灵活性之间的良好平衡,使测试能够独立运行/修改。但是,我仍然对这种方法不完全满意,因为这意味着对于每个普通的Test类,我们都需要有一个实现,在其中除了调用base constructor之外,我们什么都没做。

方法2

在第二种方法中,我们进一步采用了方法1,并尝试使用Shared Project解决我们在方法1中遇到的问题。从文档中可以看出:

共享项目让您编写由许多不同应用程序项目引用的公共代码。该代码作为每个引用项目的一部分进行编译,可以包括编译器指令以帮助将平台特定功能纳入共享代码库。

共享项目为我们提供了最佳的双赢方案,而没有link文件和不必要的类inheritanceabstraction的丑陋样式。我们的新设置如下:

方法2:项目结构

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接