2

我将 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类:

  1. 给定:我将文档写入数据库
  2. 时间:我阅读了文件
  3. 然后:结果就是预期的文档

测试B类:

  1. 给定:回购可用
  2. 时间:我写一份文件
  3. 然后:它毫无例外地写入

现在假设两个测试类并行运行,有时会出现以下问题:

  • 测试 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,有时我也会遇到问题,因为要从数据库中检索的预期文档包含的项目比测试预期的要多(因为,同样,并行运行的其他测试在数据库)。

4

1 回答 1

1

我很久以前发现 xUnit 已经内置了对异步场景的支持IAsyncLifetime

所以现在,根据我的开源库

我有以下 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 测试执行,我要做的第一件事就是重新考虑我的设计方法,也许这是一种气味。如果在并行运行时某些测试仍然失败,因为有一些我无法控制的共享资源,我通过在我的测试项目中添加一个文件夹来禁用并行 xUnit 行为,该文件夹Properties包含一个AssemblyInfo我指示 xUnit的文件项目/程序集,测试应该按顺序运行。

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

于 2021-06-23T15:37:05.207 回答