【问题标题】:What strategy to use with xUnit for integration tests when knowing they run in parallel?当知道它们并行运行时,使用 xUnit 进行集成测试的策略是什么?
【发布时间】:2019-08-13 08:00:44
【问题描述】:

我将 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),构造函数(给定)和动作(when)都被再次执行。这对于实现幂等测试非常有趣,因为每个事实都应该可以独立运行(给定的应该是幂等的)。这非常适合单元测试。

但是对于集成测试,有时我会在这样的场景中发现问题:

我有一个想要测试的 mongoDb 存储库实现。我有测试来验证我可以在上面写,还有其他测试来验证我可以从中读取。但是由于所有这些测试都是并行运行的,我必须非常注意如何设置 Given 以及如何以及何时清理上下文。

测试A类:

  1. 鉴于:我将文档写入数据库
  2. 时间:我阅读了文档
  3. 那么:结果就是预期的文档

测试B类:

  1. 鉴于:回购可用
  2. 什么时候:我写一个文件
  3. 然后:它毫无例外地写入

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

  • 测试 A 执行并写入 ID 为 1 的文档。同时测试 B 尝试在其 when 中写入 ID 为 1 的文档,但由于同一数据库中已存在具有相同 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,有时我也会遇到问题,因为从数据库中检索的预期文档包含的项目比测试预期的要多(因为,再次,其他并行运行的测试在数据库中写入了更多文档)。

【问题讨论】:

    标签: c# concurrency integration-testing xunit shared-data


    【解决方案1】:

    我不久前发现 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 和一个文件 AssemblyInfo 来禁用并行 xUnit 行为我指示 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

    【讨论】:

      猜你喜欢
      • 2019-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-29
      • 1970-01-01
      • 2015-11-20
      • 1970-01-01
      相关资源
      最近更新 更多