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