运行 针对 Visual Studio 中的多个配置的单一测试
Run single test against multiple configurations in Visual Studio
我们使用 xUnit
和 Microsoft.AspNetCore.TestHost.TestServer
设置了集成测试,以 运行 测试 Web API 运行ning on ASP.NET核心 2.2.
我们的 Web API 是一个单一的代码库,可以根据某些配置或应用程序设置差异单独部署多次,例如 country、货币,等等
下图试图解释我们的部署设置:
我们希望确保我们的集成测试 运行 针对所有部署。
对于两个部署,X 和 X` 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()
{
//...
}
}
现在,我希望 运行 ItShouldExecuteTwice_AgainstTwoSeparateConfigurations
在 class MyTest
中针对两种不同的配置/应用程序设置或换句话说针对两种不同的配置/应用程序设置进行多次测试Visual Studio.
中的不同测试部署
我知道,我应该能够使用构建配置(如 DEBUG_SETTING1
、DEBUG_SETTING2
)和预处理器指令(#if DEBUG_SETTING1
)的组合来实现这一点.
另一种选择可能是拥有一个包含常用方法的基础测试帮助程序项目和一个用于每个部署的单独集成项目。
有没有更好更优雅的方法来实现这个?
重构测试启动以允许根据测试需要对其进行修改
例如
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 的 post 非常适合我们的场景和我提出的问题。这是一种简单、干净且易于理解的方法,具有最大的可重用性。满分答案。
但是,有几个原因导致我无法继续采用该方法:
在建议的方法中,我们不能 运行 只测试一个特定的 setting
。这对我们来说很重要,因为在未来,可以 两个不同的团队维护他们的特定实施和部署。使用 Theory
,要 运行 对所有测试只有一个 setting
变得有点困难。
我们很可能需要为每个设置/部署使用两个单独的构建和部署管道。
虽然 API 端点、Request
和 Response
今天完全相同,但我们不知道它是否会继续如此我们的开发继续进行。
基于以上原因我们还考虑了以下两种方案:
方法一
有一个通用的 class
库,它具有通用的 Fixture
和 Tests
作为 abstract
class
- 项目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
class 我们需要有一个实现,除了调用 base
我们什么都不做constructor
.
方法二
在第二种方法中,我们进一步采用了方法 1,并尝试使用 Shared Project 解决方法 1 中的问题。来自文档:
Shared Projects let you write common code that is referenced by a
number of different application projects. The code is compiled as part
of each referencing project and can include compiler directives to
help incorporate platform-specific functionality into the shared code
base.
Shared Project 为我们提供了两全其美的体验,没有丑陋的 link
文件和不必要的 class inheritance
或 abstraction
。我们的新设置如下:
编辑:
我为此写了一篇博客 post,其中详细讨论了我们的用例和解决方案。这是 link:
我们使用 xUnit
和 Microsoft.AspNetCore.TestHost.TestServer
设置了集成测试,以 运行 测试 Web API 运行ning on ASP.NET核心 2.2.
我们的 Web API 是一个单一的代码库,可以根据某些配置或应用程序设置差异单独部署多次,例如 country、货币,等等
下图试图解释我们的部署设置:
我们希望确保我们的集成测试 运行 针对所有部署。
对于两个部署,X 和 X` 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()
{
//...
}
}
现在,我希望 运行 ItShouldExecuteTwice_AgainstTwoSeparateConfigurations
在 class MyTest
中针对两种不同的配置/应用程序设置或换句话说针对两种不同的配置/应用程序设置进行多次测试Visual Studio.
我知道,我应该能够使用构建配置(如
DEBUG_SETTING1
、DEBUG_SETTING2
)和预处理器指令(#if DEBUG_SETTING1
)的组合来实现这一点.另一种选择可能是拥有一个包含常用方法的基础测试帮助程序项目和一个用于每个部署的单独集成项目。
有没有更好更优雅的方法来实现这个?
重构测试启动以允许根据测试需要对其进行修改
例如
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 的 post 非常适合我们的场景和我提出的问题。这是一种简单、干净且易于理解的方法,具有最大的可重用性。满分答案。
但是,有几个原因导致我无法继续采用该方法:
在建议的方法中,我们不能 运行 只测试一个特定的
setting
。这对我们来说很重要,因为在未来,可以 两个不同的团队维护他们的特定实施和部署。使用Theory
,要 运行 对所有测试只有一个setting
变得有点困难。我们很可能需要为每个设置/部署使用两个单独的构建和部署管道。
虽然 API 端点、
Request
和Response
今天完全相同,但我们不知道它是否会继续如此我们的开发继续进行。
基于以上原因我们还考虑了以下两种方案:
方法一
有一个通用的 class
库,它具有通用的 Fixture
和 Tests
作为 abstract
class
- 项目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
class 我们需要有一个实现,除了调用 base
我们什么都不做constructor
.
方法二
在第二种方法中,我们进一步采用了方法 1,并尝试使用 Shared Project 解决方法 1 中的问题。来自文档:
Shared Projects let you write common code that is referenced by a number of different application projects. The code is compiled as part of each referencing project and can include compiler directives to help incorporate platform-specific functionality into the shared code base.
Shared Project 为我们提供了两全其美的体验,没有丑陋的 link
文件和不必要的 class inheritance
或 abstraction
。我们的新设置如下:
编辑: 我为此写了一篇博客 post,其中详细讨论了我们的用例和解决方案。这是 link: