当知道它们 运行 并行时,使用 xUnit 进行集成测试的策略是什么?
What strategy to use with xUnit for integration tests when knowing they run in parallel?
我将 dotnet core 与 xUnit 一起用于单元测试和集成测试。我以这种方式遵循 Given-Then-When 哲学的所有测试都有一个基本摘要 class:
namespace ToolBelt.TestSupport
{
public abstract class Given_WhenAsync_Then_Test
: IDisposable
{
protected Given_WhenAsync_Then_Test()
{
Task.Run(async () => { await SetupAsync();}).GetAwaiter().GetResult();
}
private async Task SetupAsync()
{
Given();
await WhenAsync();
}
protected abstract void Given();
protected abstract Task WhenAsync();
public void Dispose()
{
Cleanup();
}
protected virtual void Cleanup()
{
}
}
}
作为总结,对于每个事实(then),构造函数(given)和动作(when)被再次执行。这对于实现幂等测试非常有趣,因为每个事实都应该 运行nable 孤立(给定的应该是幂等的)。这非常适合单元测试。
但是对于集成测试,有时我会在这样的场景中发现问题:
我有一个要测试的 mongoDb 存储库实现。我有测试来验证我可以在上面写,还有其他测试来验证我可以从中读取。但是由于所有这些测试 运行 并行进行,我必须非常注意如何设置 Given
以及如何以及何时清理上下文。
测试class答:
- 给定:我向数据库写入一个文档
- 时间:我读了文档
- 那么:结果就是预期的文档
测试class B:
- 鉴于:回购可用
- 时间:我写文档
- 然后:它写入无一例外
现在假设两个测试class是并行的运行,有时会出现以下问题:
- 测试 A 执行并写入 ID 为 1 的文档。同时,测试 B 尝试在其
when
中写入 ID 为 1 的文档,但失败了,因为同一数据库中已有文档使用相同的 ID。
- 测试 B 执行,它有一个 teardown/cleanup 在测试结束时删除文档。与此同时,测试 A 正要读取一份预期存在的文档...但它失败了,因为该文档已被删除(来自测试 B)
问题是:是否有可能 运行 并行集成测试并实现幂等性 Given
而不会遇到问题,因为一个测试会弄乱另一个测试的数据测试?
我想到了一些想法,但我没有经验,所以我正在寻找意见和解决方案。
- 解决方案 A:确保每个测试 class 使用其他测试无法访问的数据。例如,通过为测试数据提供不同的 ID。这确实可以解决问题,但它迫使开发人员了解其他测试正在使用哪些 ID。
- 解决方案 B:使用某种程序集 Given 和 Teardown 为每个测试准备场景。同样,我们将依赖比测试 class 本身更大的东西,它似乎违反了我想遵循的 GivenThenWhen 哲学。
xUnit 有可能在测试之间有不同的共享上下文,但我也看不出它如何适合我的模板:https://xunit.github.io/docs/shared-context.
您如何使用 xUnit 处理这些集成测试场景?塔
更新 1:这是我如何使用 GTW 理念和 xUnit 创建测试的示例。这些事实有时会失败,因为它们无法插入具有已存在 Id 的文档(因为使用具有相同 id 的文档的其他测试 classes 同时 运行ning 并且没有清理还)
public static class GetAllTests
{
public class Given_A_Valid_Filter_When_Getting_All
: Given_WhenAsync_Then_Test
{
private ReadRepository<FakeDocument> _sut;
private Exception _exception;
private Expression<Func<FakeDocument, bool>> _filter;
private IEnumerable<FakeDocument> _result;
private IEnumerable<FakeDocument> _expectedDocuments;
protected override void Given()
{
_filter = x => true;
var cursorServiceMock = new Mock<ICursorService<FakeDocument>>();
var all = Enumerable.Empty<FakeDocument>().ToList();
cursorServiceMock
.Setup(x => x.GetList(It.IsAny<IAsyncCursor<FakeDocument>>()))
.ReturnsAsync(all);
var cursorService = cursorServiceMock.Object;
var documentsMock = new Mock<IMongoCollection<FakeDocument>>();
documentsMock
.Setup(x => x.FindAsync(It.IsAny<Expression<Func<FakeDocument, bool>>>(),
It.IsAny<FindOptions<FakeDocument, FakeDocument>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(default(IAsyncCursor<FakeDocument>));
var documents = documentsMock.Object;
_sut = new ReadRepository<FakeDocument>(documents, cursorService);
_expectedDocuments = all;
}
protected override async Task WhenAsync()
{
try
{
_result = await _sut.GetAll(_filter);
}
catch (Exception exception)
{
_exception = exception;
}
}
[Fact]
public void Then_It_Should_Execute_Without_Exceptions()
{
_exception.Should().BeNull();
}
[Fact]
public void Then_It_Should_Return_The_Expected_Documents()
{
_result.Should().AllBeEquivalentTo(_expectedDocuments);
}
}
public class Given_A_Null_Filter_When_Getting_All
: Given_WhenAsync_Then_Test
{
private ReadRepository<FakeDocument> _sut;
private ArgumentNullException _exception;
private Expression<Func<FakeDocument, bool>> _filter;
protected override void Given()
{
_filter = default;
var cursorService = Mock.Of<ICursorService<FakeDocument>>();
var documents = Mock.Of<IMongoCollection<FakeDocument>>();
_sut = new ReadRepository<FakeDocument>(documents, cursorService);
}
protected override async Task WhenAsync()
{
try
{
await _sut.GetAll(_filter);
}
catch (ArgumentNullException exception)
{
_exception = exception;
}
}
[Fact]
public void Then_It_Should_Throw_A_ArgumentNullException()
{
_exception.Should().NotBeNull();
}
}
}
更新 2: 如果我制作随机 ID,有时我也会遇到问题,因为从数据库中检索的预期文档包含的项目多于测试预期的项目(因为,同样,其他测试 运行ning 并行地在数据库中写入了更多文档)。
我不久前发现 xUnit 内置了对 IAsyncLifetime
异步场景的支持。
所以现在,根据 my open source library
我有以下 GivenWhenThen 模板
using System.Threading.Tasks;
using Xunit;
namespace Sasw.TestSupport.XUnit
{
public abstract class Given_When_Then_Test_Async
: IAsyncLifetime
{
public async Task InitializeAsync()
{
await Given();
await When();
}
public async Task DisposeAsync()
{
await Cleanup();
}
protected virtual Task Cleanup()
{
return Task.CompletedTask;
}
protected abstract Task Given();
protected abstract Task When();
}
}
按照并行执行方法,我通常在测试的前提条件下创建唯一的数据库、流等,这样我根本就没有任何逻辑共享资源。每个测试使用相同的服务器、不同的流、数据库或集合。
如果出于某种原因,我需要禁用并行 xUnit 测试执行,我做的第一件事就是重新考虑我的设计方法,也许这是一种气味。如果在 运行 并行时某些测试仍然失败,因为有一些我无法控制的共享资源,我通过在我的测试项目上添加一个文件夹 Properties
来禁用并行 xUnit 行为使用一个文件 AssemblyInfo
,我在其中指示 xUnit 在那个 project/assembly 上,测试应该 运行 按顺序进行。
using Xunit;
#if DEBUG
[assembly: CollectionBehavior(DisableTestParallelization = false)]
#else
// Maybe in DEBUG I want to run in parallel and
// in RELEASE mode I want to run sequentially, so I can have this.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
#endif
关于在我的一个视频教程中使用上述方法的一些测试方法的更多信息(抱歉,是西班牙语):https://www.youtube.com/watch?v=dyVEayGwU3I
我将 dotnet core 与 xUnit 一起用于单元测试和集成测试。我以这种方式遵循 Given-Then-When 哲学的所有测试都有一个基本摘要 class:
namespace ToolBelt.TestSupport
{
public abstract class Given_WhenAsync_Then_Test
: IDisposable
{
protected Given_WhenAsync_Then_Test()
{
Task.Run(async () => { await SetupAsync();}).GetAwaiter().GetResult();
}
private async Task SetupAsync()
{
Given();
await WhenAsync();
}
protected abstract void Given();
protected abstract Task WhenAsync();
public void Dispose()
{
Cleanup();
}
protected virtual void Cleanup()
{
}
}
}
作为总结,对于每个事实(then),构造函数(given)和动作(when)被再次执行。这对于实现幂等测试非常有趣,因为每个事实都应该 运行nable 孤立(给定的应该是幂等的)。这非常适合单元测试。
但是对于集成测试,有时我会在这样的场景中发现问题:
我有一个要测试的 mongoDb 存储库实现。我有测试来验证我可以在上面写,还有其他测试来验证我可以从中读取。但是由于所有这些测试 运行 并行进行,我必须非常注意如何设置 Given
以及如何以及何时清理上下文。
测试class答:
- 给定:我向数据库写入一个文档
- 时间:我读了文档
- 那么:结果就是预期的文档
测试class B:
- 鉴于:回购可用
- 时间:我写文档
- 然后:它写入无一例外
现在假设两个测试class是并行的运行,有时会出现以下问题:
- 测试 A 执行并写入 ID 为 1 的文档。同时,测试 B 尝试在其
when
中写入 ID 为 1 的文档,但失败了,因为同一数据库中已有文档使用相同的 ID。 - 测试 B 执行,它有一个 teardown/cleanup 在测试结束时删除文档。与此同时,测试 A 正要读取一份预期存在的文档...但它失败了,因为该文档已被删除(来自测试 B)
问题是:是否有可能 运行 并行集成测试并实现幂等性 Given
而不会遇到问题,因为一个测试会弄乱另一个测试的数据测试?
我想到了一些想法,但我没有经验,所以我正在寻找意见和解决方案。
- 解决方案 A:确保每个测试 class 使用其他测试无法访问的数据。例如,通过为测试数据提供不同的 ID。这确实可以解决问题,但它迫使开发人员了解其他测试正在使用哪些 ID。
- 解决方案 B:使用某种程序集 Given 和 Teardown 为每个测试准备场景。同样,我们将依赖比测试 class 本身更大的东西,它似乎违反了我想遵循的 GivenThenWhen 哲学。
xUnit 有可能在测试之间有不同的共享上下文,但我也看不出它如何适合我的模板:https://xunit.github.io/docs/shared-context.
您如何使用 xUnit 处理这些集成测试场景?塔
更新 1:这是我如何使用 GTW 理念和 xUnit 创建测试的示例。这些事实有时会失败,因为它们无法插入具有已存在 Id 的文档(因为使用具有相同 id 的文档的其他测试 classes 同时 运行ning 并且没有清理还)
public static class GetAllTests
{
public class Given_A_Valid_Filter_When_Getting_All
: Given_WhenAsync_Then_Test
{
private ReadRepository<FakeDocument> _sut;
private Exception _exception;
private Expression<Func<FakeDocument, bool>> _filter;
private IEnumerable<FakeDocument> _result;
private IEnumerable<FakeDocument> _expectedDocuments;
protected override void Given()
{
_filter = x => true;
var cursorServiceMock = new Mock<ICursorService<FakeDocument>>();
var all = Enumerable.Empty<FakeDocument>().ToList();
cursorServiceMock
.Setup(x => x.GetList(It.IsAny<IAsyncCursor<FakeDocument>>()))
.ReturnsAsync(all);
var cursorService = cursorServiceMock.Object;
var documentsMock = new Mock<IMongoCollection<FakeDocument>>();
documentsMock
.Setup(x => x.FindAsync(It.IsAny<Expression<Func<FakeDocument, bool>>>(),
It.IsAny<FindOptions<FakeDocument, FakeDocument>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(default(IAsyncCursor<FakeDocument>));
var documents = documentsMock.Object;
_sut = new ReadRepository<FakeDocument>(documents, cursorService);
_expectedDocuments = all;
}
protected override async Task WhenAsync()
{
try
{
_result = await _sut.GetAll(_filter);
}
catch (Exception exception)
{
_exception = exception;
}
}
[Fact]
public void Then_It_Should_Execute_Without_Exceptions()
{
_exception.Should().BeNull();
}
[Fact]
public void Then_It_Should_Return_The_Expected_Documents()
{
_result.Should().AllBeEquivalentTo(_expectedDocuments);
}
}
public class Given_A_Null_Filter_When_Getting_All
: Given_WhenAsync_Then_Test
{
private ReadRepository<FakeDocument> _sut;
private ArgumentNullException _exception;
private Expression<Func<FakeDocument, bool>> _filter;
protected override void Given()
{
_filter = default;
var cursorService = Mock.Of<ICursorService<FakeDocument>>();
var documents = Mock.Of<IMongoCollection<FakeDocument>>();
_sut = new ReadRepository<FakeDocument>(documents, cursorService);
}
protected override async Task WhenAsync()
{
try
{
await _sut.GetAll(_filter);
}
catch (ArgumentNullException exception)
{
_exception = exception;
}
}
[Fact]
public void Then_It_Should_Throw_A_ArgumentNullException()
{
_exception.Should().NotBeNull();
}
}
}
更新 2: 如果我制作随机 ID,有时我也会遇到问题,因为从数据库中检索的预期文档包含的项目多于测试预期的项目(因为,同样,其他测试 运行ning 并行地在数据库中写入了更多文档)。
我不久前发现 xUnit 内置了对 IAsyncLifetime
异步场景的支持。
所以现在,根据 my open source library
我有以下 GivenWhenThen 模板
using System.Threading.Tasks;
using Xunit;
namespace Sasw.TestSupport.XUnit
{
public abstract class Given_When_Then_Test_Async
: IAsyncLifetime
{
public async Task InitializeAsync()
{
await Given();
await When();
}
public async Task DisposeAsync()
{
await Cleanup();
}
protected virtual Task Cleanup()
{
return Task.CompletedTask;
}
protected abstract Task Given();
protected abstract Task When();
}
}
按照并行执行方法,我通常在测试的前提条件下创建唯一的数据库、流等,这样我根本就没有任何逻辑共享资源。每个测试使用相同的服务器、不同的流、数据库或集合。
如果出于某种原因,我需要禁用并行 xUnit 测试执行,我做的第一件事就是重新考虑我的设计方法,也许这是一种气味。如果在 运行 并行时某些测试仍然失败,因为有一些我无法控制的共享资源,我通过在我的测试项目上添加一个文件夹 Properties
来禁用并行 xUnit 行为使用一个文件 AssemblyInfo
,我在其中指示 xUnit 在那个 project/assembly 上,测试应该 运行 按顺序进行。
using Xunit;
#if DEBUG
[assembly: CollectionBehavior(DisableTestParallelization = false)]
#else
// Maybe in DEBUG I want to run in parallel and
// in RELEASE mode I want to run sequentially, so I can have this.
[assembly: CollectionBehavior(DisableTestParallelization = true)]
#endif
关于在我的一个视频教程中使用上述方法的一些测试方法的更多信息(抱歉,是西班牙语):https://www.youtube.com/watch?v=dyVEayGwU3I