Какую стратегию использовать с xUnit для интеграционных тестов, зная, что они работают параллельно? - PullRequest
1 голос
/ 22 марта 2019

Я использую ядро ​​dotnet с xUnit для своих модульных тестов, а также для интеграционных тестов.У меня есть базовый абстрактный класс для всех моих тестов, который следует философии «дано тогда и когда» следующим образом:

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()
        {
        }
    }
}

В качестве резюме, для каждого факта (тогда), конструктора (данного) и действия(когда) выполняются снова.Это очень интересно для достижения идемпотентных тестов, потому что каждый факт должен быть работоспособен изолированно (данные должны быть идемпотентными).Это отлично подходит для модульных тестов.

Но для интеграционных тестов иногда я нахожу проблемы в таких сценариях, как это:

У меня есть реализация репозитория mongoDb, которую я хочу протестировать.У меня есть тесты, чтобы проверить, что я могу писать на нем, и другие, чтобы убедиться, что я могу читать с него.Но так как все эти тесты выполняются параллельно, я должен помнить, как настроить Given и как и когда очищать контекст.

Тестовый класс A:

  1. Дано: Я записываю в базу данных документ
  2. Когда: Я читаю документ
  3. Затем: результат - ожидаемый документ

Тестовый класс B:

  1. Дано: Репо доступно
  2. Когда: я пишу документ
  3. Затем: он пишет без исключений

Теперь представьте, что оба тестовых классапри параллельном запуске иногда возникают следующие проблемы:

  • Тест A выполняет и записывает документ с Id 1. В то же время Тест B пытается записать документ с Id 1 в его when ион терпит неудачу, потому что в той же базе данных уже есть документ с тем же идентификатором.
  • Испытание B выполняется, и у него есть разборка / очистка, которая удаляет документ в конце его теста.В то же время тест А собирался прочитать документ, который должен быть там ... и он не прошел, потому что документ был удален (из теста Б)

Вопрос в следующем: Можно ли даже запустить интеграционные тесты параллельно и достичь идемпотентности Given, не сталкиваясь с проблемами, потому что один тест портится с данными другого теста?

Я подумал о нескольких идеях, ноУ меня нет опыта работы с ним, поэтому я ищу мнения и решения.

  • Решение A: Обеспечение того, чтобы каждый тестовый класс использовал данные, к которым нет других тестовых доступов.Например, предоставляя разные идентификаторы для тестовых данных.Это действительно может решить проблему, но заставляет разработчиков знать, какие идентификаторы используются в других тестах.
  • Решение B: Предоставить некоторую сборку данных и Teardown, которая подготавливает сценарий для каждого теста.Опять же, мы бы положились на нечто большее, чем сам класс тестирования, и, похоже, это нарушает принцип GivenThenWhen, которому я хочу следовать.

xUnit имеет возможность различного общего контекста между тестами, но я непосмотрите, как это согласуется с моим шаблоном: https://xunit.github.io/docs/shared-context.

Как вы справляетесь с этими сценариями интеграционного тестирования с xUnit?Та


ОБНОВЛЕНИЕ 1 : Вот пример того, как я создаю свои тесты, используя философию GTW и xUnit.Эти факты иногда терпят неудачу, потому что они не могут вставить документ с идентификатором, который уже существует (потому что другие тестовые классы, которые используют документ с тем же идентификатором, работают в то же время и еще не очистили)

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: Если я создаю случайные идентификаторы, иногда я также сталкиваюсь с проблемами, потому что ожидаемые документы, которые будут получены из БД, содержат больше элементов, чем те, которые ожидает тест (потому что, опять же, другиепараллельно выполняющиеся тесты записали больше документов в базу данных).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...