我将 dotnet core 与 xUnit 一起用于我的单元测试和集成测试。我以这种方式遵循 Given-Then-When 哲学的所有测试都有一个基本抽象类:
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) 都会再次执行。这对于实现幂等测试非常有趣,因为每个事实都应该可以独立运行(给定的应该是幂等的)。这对于单元测试非常有用。
但是对于集成测试,有时我会在这样的场景中发现问题:
我有一个要测试的 mongoDb 存储库实现。我有测试来验证我可以在上面写,还有其他测试来验证我可以从中读取。但是由于所有这些测试都是并行运行的,所以我必须非常注意如何设置Given
以及如何以及何时清理上下文。
测试A类:
- 给定:我将文档写入数据库
- 时间:我阅读了文件
- 然后:结果就是预期的文档
测试B类:
- 给定:回购可用
- 时间:我写一份文件
- 然后:它毫无例外地写入
现在假设两个测试类并行运行,有时会出现以下问题:
- 测试 A 执行并写入 ID 为 1 的文档。同时测试 B 尝试写入 ID 为 1 的文档,
when
但由于同一数据库中已有具有相同 ID 的文档而失败。 - 测试 B 执行,它有一个拆卸/清理,在测试结束时删除文档。与此同时,测试 A 正要读取一个预期存在的文档......但它失败了,因为该文档已被删除(来自测试 B)
问题是:是否有可能并行运行集成测试并实现幂等Given
而不会因为一个测试与另一个测试的数据混淆而遇到问题?
我想到了一些想法,但我没有经验,所以我正在寻找意见和解决方案。
- 解决方案 A:确保每个测试类都使用其他测试无法访问的数据。例如,通过为测试数据提供不同的 ID。这确实可以解决问题,但它迫使开发人员了解其他测试正在使用哪些 Id。
- 解决方案 B:有某种组件 Given 和 Teardown,为每个测试准备场景。同样,我们将依赖比测试类本身更大的东西,它似乎违反了我想要遵循的 GivenThenWhen 哲学。
xUnit 有可能在测试之间使用不同的共享上下文,但我也看不出这与我的模板有何匹配:https ://xunit.github.io/docs/shared-context 。
您如何使用 xUnit 处理这些集成测试场景?塔
更新 1:这是我如何使用 GTW 哲学和 xUnit 创建测试的示例。这些事实有时会失败,因为它们无法插入具有已经存在的 Id 的文档(因为使用具有相同 id 的文档的其他测试类同时运行并且尚未清理)
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,有时我也会遇到问题,因为要从数据库中检索的预期文档包含的项目比测试预期的要多(因为,同样,并行运行的其他测试在数据库)。