Выполнять модульные тесты последовательно (а не параллельно)

107

Я пытаюсь выполнить модульное тестирование написанного мной механизма управления хостом WCF. Механизм в основном создает экземпляры ServiceHost на лету на основе конфигурации. Это позволяет нам динамически перенастраивать, какие службы доступны, без необходимости отключать их все и перезапускать их всякий раз, когда добавляется новая служба или удаляется старая.

Однако я столкнулся с трудностями при модульном тестировании этого механизма управления хостом из-за того, как работает ServiceHost. Если ServiceHost уже был создан, открыт и еще не закрыт для конкретной конечной точки, другой ServiceHost для той же конечной точки не может быть создан, что приведет к исключению. Из-за того, что современные платформы модульного тестирования распараллеливают выполнение тестов, у меня нет эффективного способа модульного тестирования этого фрагмента кода.

Я использовал xUnit.NET, надеясь, что благодаря его расширяемости я смогу найти способ заставить его запускать тесты последовательно. Однако мне не повезло. Я надеюсь, что кто-то здесь, на SO, столкнулся с подобной проблемой и знает, как заставить модульные тесты запускаться последовательно.

ПРИМЕЧАНИЕ. ServiceHost - это класс WCF, написанный Microsoft. У меня нет возможности изменить его поведение. Размещение каждой конечной точки службы только один раз также является правильным поведением ... однако это не особенно способствует модульному тестированию.

Jrista
источник
Разве это конкретное поведение ServiceHost не было бы тем, чем вы могли бы заняться?
Роберт Харви,
ServiceHost написан Microsoft. Я не могу это контролировать. И технически говоря, это допустимое поведение ... у вас никогда не должно быть более одного ServiceHost на конечную точку.
jrista
1
У меня была аналогичная проблема при попытке запустить несколько TestServerв докере. Поэтому мне пришлось сериализовать интеграционные тесты.
h-

Ответы:

128

Каждый тестовый класс представляет собой уникальный набор тестов, и тесты в нем будут выполняться последовательно, поэтому, если вы поместите все свои тесты в одну и ту же коллекцию, он будет запускаться последовательно.

Для этого в xUnit можно внести следующие изменения:

Следующее будет выполняться параллельно:

namespace IntegrationTests
{
    public class Class1
    {
        [Fact]
        public void Test1()
        {
            Console.WriteLine("Test1 called");
        }

        [Fact]
        public void Test2()
        {
            Console.WriteLine("Test2 called");
        }
    }

    public class Class2
    {
        [Fact]
        public void Test3()
        {
            Console.WriteLine("Test3 called");
        }

        [Fact]
        public void Test4()
        {
            Console.WriteLine("Test4 called");
        }
    }
}

Чтобы сделать его последовательным, вам просто нужно поместить оба тестовых класса в одну коллекцию:

namespace IntegrationTests
{
    [Collection("Sequential")]
    public class Class1
    {
        [Fact]
        public void Test1()
        {
            Console.WriteLine("Test1 called");
        }

        [Fact]
        public void Test2()
        {
            Console.WriteLine("Test2 called");
        }
    }

    [Collection("Sequential")]
    public class Class2
    {
        [Fact]
        public void Test3()
        {
            Console.WriteLine("Test3 called");
        }

        [Fact]
        public void Test4()
        {
            Console.WriteLine("Test4 called");
        }
    }
}

Для получения дополнительной информации вы можете перейти по этой ссылке

Абхинав Саксена
источник
27
Думаю, недооцененный ответ. Кажется, работает, и мне нравится степень детализации, поскольку у меня есть параллелизуемые и непараллелизуемые тесты в одной сборке.
Иганд 02
1
Это правильный способ сделать это, см. Документацию Xunit.
Хокон К. Олафсен,
2
Это должен быть принятый ответ, потому что обычно некоторые тесты могут выполняться параллельно (в моем случае все модульные тесты), но некоторые из них случайным образом терпят неудачу при параллельном запуске (в моем случае те, которые используют веб-клиент / сервер в памяти), поэтому один из них при желании можно оптимизировать выполнение тестов.
Алексей
2
Это не сработало для меня в основном проекте .net, где я выполняю интеграционные тесты с базой данных sqlite. Тесты по-прежнему проводились параллельно. Однако принятый ответ сработал.
user1796440
Большое спасибо за этот ответ! Это необходимо, поскольку у меня есть приемочные тесты в разных классах, которые наследуются от одной и той же TestBase, и параллелизм не очень хорошо работал с EF Core.
Кианит
108

Важно: этот ответ относится к .NET Framework. Для ядра dotnet см. Ответ Дмитрия относительно xunit.runner.json.

Все хорошие модульные тесты должны быть на 100% изолированы. Использование общего состояния (например, в зависимости от staticсвойства, изменяемого каждым тестом) считается плохой практикой.

Сказав это, на ваш вопрос о последовательном запуске тестов xUnit есть ответ! Я столкнулся с точно такой же проблемой, потому что моя система использует статический локатор сервисов (что далеко не идеально).

По умолчанию xUnit 2.x запускает все тесты параллельно. Это можно изменить для каждой сборки, указав CollectionBehaviorв файле AssemblyInfo.cs в тестовом проекте.

Для разделения сборки используйте:

using Xunit;
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]

или вообще без распараллеливания:

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Последний, вероятно, тот, который вам нужен. Дополнительную информацию о распараллеливании и настройке можно найти в документации xUnit .

Покорять
источник
5
Для меня были общие ресурсы между методами внутри каждого класса. Запуск теста из одного класса, а затем одного из другого нарушит тесты обоих. Я смог решить, используя [assembly: CollectionBehavior(CollectionBehavior.CollectionPerClass, DisableTestParallelization = true)]. Благодаря тебе, @Squiggle, я могу запустить все свои тесты и пойти выпить кофе! :)
Алиельсон Пиффер
2
Ответ Абхинава Саксены более детален для .NET Core.
Yennefer 06
74

Для проектов .NET Core создайте xunit.runner.jsonс помощью:

{
  "parallelizeAssembly": false,
  "parallelizeTestCollections": false
}

Кроме того, вы csprojдолжны содержать

<ItemGroup>
  <None Update="xunit.runner.json"> 
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

Для старых проектов .Net Core ваш project.jsonдолжен содержать

"buildOptions": {
  "copyToOutput": {
    "include": [ "xunit.runner.json" ]
  }
}
Дмитрий Поломошнов
источник
2
Я предполагаю, что последний эквивалент ядра csproj dotnet будет <ItemGroup><None Include="xunit.runner.json" CopyToOutputDirectory="Always" /></ItemGroup>или похож?
Squiggle
3
Это сработало для меня в csproj:<ItemGroup> <None Update="xunit.runner.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>
skynyrd
Работает ли отключение распараллеливания с xUnit Theories?
Джон Заброски
Это единственное, что у меня сработало, пробовал бегать, dotnet test --no-build -c Release -- xunit.parallelizeTestCollections=falseно у меня не получилось.
harvzor
18

Для проектов .NET Core вы можете настроить xUnit с помощью xunit.runner.jsonфайла, как описано в https://xunit.github.io/docs/configuring-with-json.html .

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

Установите это значение, trueесли сборка желает запускать тесты внутри этой сборки параллельно друг другу. ... Установите это значение, falseчтобы отключить распараллеливание в этой тестовой сборке.

Тип схемы JSON: логический
Значение по умолчанию:true

Так xunit.runner.jsonвыглядит минимал для этой цели

{
    "parallelizeTestCollections": false
}

Как указано в документации, не забудьте включить этот файл в свою сборку:

  • Для параметра Копировать в выходной каталог значение Копировать, если оно более новое, в свойствах файла в Visual Studio, или
  • Добавление

    <Content Include=".\xunit.runner.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    

    в ваш .csprojфайл, или

  • Добавление

    "buildOptions": {
      "copyToOutput": {
        "include": [ "xunit.runner.json" ]
      }
    }
    

    в ваш project.jsonфайл

в зависимости от типа вашего проекта.

Наконец, в дополнение к вышесказанному, если вы используете Visual Studio, убедитесь, что вы случайно не нажали кнопку « Выполнить тесты в параллельном режиме» , что приведет к параллельному запуску тестов, даже если вы отключили параллелизацию в xunit.runner.json. Дизайнеры пользовательского интерфейса Microsoft хитро сделали эту кнопку без надписи, чтобы ее было трудно заметить и на расстоянии примерно сантиметра от кнопки «Выполнить все» в обозревателе тестов, просто чтобы максимизировать вероятность того, что вы нажмете ее по ошибке и не поймете, почему ваши тесты внезапно выходят из строя:

Снимок экрана с кнопкой в ​​кружке

Марк Эмери
источник
@JohnZabroski Я не понимаю вашего предложенного редактирования . Причем тут ReSharper? Я думаю, что, вероятно, он был установлен, когда я писал ответ выше, но разве здесь не все зависит от того, используете вы его или нет? Какое отношение имеет страница, на которую вы ссылаетесь при редактировании , с указанием xunit.runner.jsonфайла? И какое отношение указание xunit.runner.jsonимеет к последовательному запуску тестов?
Марк Эмери
Я пытаюсь запустить тесты последовательно, и сначала подумал, что проблема связана с ReSharper (поскольку в ReSharper НЕТ кнопки «Выполнить тесты параллельно», как в Visual Studio Test Explorer). Однако, похоже, когда я использую [Теорию], мои тесты не изолированы. Это странно, потому что все, что я прочитал, предполагает, что класс - это наименьшая параллелизируемая единица.
Джон Заброски
10

Это старый вопрос, но я хотел написать решение для людей, ищущих новых, как я :)

Примечание. Я использую этот метод в тестах интеграции Dot Net Core WebUI с xunit версии 2.4.1.

Создайте пустой класс с именем NonParallelCollectionDefinitionClass и затем присвойте этому классу атрибут CollectionDefinition, как показано ниже. (Важная часть - параметр DisableParallelization = true.)

using Xunit;

namespace WebUI.IntegrationTests.Common
{
    [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)]
    public class NonParallelCollectionDefinitionClass
    {
    }
}

После этого добавьте атрибут Collection в класс, который вы не хотите, чтобы он запускался параллельно, как показано ниже. (Важная часть - это имя коллекции. Оно должно совпадать с именем, используемым в CollectionDefinition)

namespace WebUI.IntegrationTests.Controllers.Users
{
    [Collection("Non-Parallel Collection")]
    public class ChangePassword : IClassFixture<CustomWebApplicationFactory<Startup>>
    ...

Когда мы это делаем, сначала запускаются другие параллельные тесты. После этого запускаются другие тесты с атрибутом Коллекция («Непараллельная коллекция»).

Гюндюз Эмре ОЗЕР
источник
6

вы можете использовать плейлист

щелкните правой кнопкой мыши метод тестирования -> Добавить в список воспроизведения -> Новый список воспроизведения

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

введите описание изображения здесь

HB MAAM
источник
5

Я не знаю подробностей, но похоже, что вы пытаетесь провести интеграционное тестирование, а не модульное тестирование . Если бы вы могли изолировать зависимость от ServiceHost, это, вероятно, упростило бы (и ускорило) ваше тестирование. Так (например) вы можете самостоятельно протестировать следующее:

  • Класс чтения конфигурации
  • ServiceHost factory (возможно, в качестве интеграционного теста)
  • Класс двигателя, который принимает IServiceHostFactoryиIConfiguration

Инструменты, которые помогут включить изоляционные (имитирующие) фреймворки и (необязательно) фреймворки контейнеров IoC. Увидеть:

TrueWill
источник
Я не пытаюсь проводить интеграционное тестирование. Мне действительно нужно проводить модульное тестирование. Я досконально разбираюсь в терминах и практиках TDD / BDD (IoC, DI, Mocking и т. Д.), Поэтому такие обычные вещи, как создание фабрик и использование интерфейсов, - это не то, что мне нужно (это уже сделано, За исключением случая самого ServiceHost.) ServiceHost не является зависимостью, которую можно изолировать, поскольку она не может быть должным образом имитация (как и многие пространства имен .NET System). Мне действительно нужен способ запускать модульные тесты последовательно.
jrista
1
@jrista - никакого пренебрежения к вашим навыкам не предполагалось. Я не разработчик WCF, но сможет ли движок вернуть оболочку вокруг ServiceHost с интерфейсом в оболочке? Или, может быть, завод по индивидуальному заказу для ServiceHosts?
TrueWill
Механизм хостинга не возвращает никаких ServiceHosts. На самом деле он ничего не возвращает, он просто управляет созданием, открытием и закрытием ServiceHosts изнутри. Я мог бы обернуть все основные типы WCF, но это ОЧЕНЬ много работы, на выполнение которой я не был уполномочен. Кроме того, как оказалось, проблема не вызвана параллельным выполнением и все равно будет возникать при нормальной работе. Я начал еще один вопрос здесь о проблеме, и, надеюсь, я получу ответ.
jrista
@TrueWill: Кстати, я совсем не беспокоился о том, что вы пренебрегаете моими навыками ... Я просто не хотел получать много заурядных ответов, охватывающих все общие вопросы о модульном тестировании. Мне нужен был быстрый ответ по очень конкретной проблеме. Извините, если я немного грубоват, это не было моим намерением. Просто у меня очень мало времени, чтобы заставить эту штуку работать.
jrista
3

Возможно, вы сможете использовать Advanced Unit Testing . Это позволяет вам определить последовательность, в которой вы запускаете тест . Поэтому вам, возможно, придется создать новый файл cs для размещения этих тестов.

Вот как можно изменить методы тестирования, чтобы они работали в нужной вам последовательности.

[Test]
[Sequence(16)]
[Requires("POConstructor")]
[Requires("WorkOrderConstructor")]
public void ClosePO()
{
  po.Close();

  // one charge slip should be added to both work orders

  Assertion.Assert(wo1.ChargeSlipCount==1,
    "First work order: ChargeSlipCount not 1.");
  Assertion.Assert(wo2.ChargeSlipCount==1,
    "Second work order: ChargeSlipCount not 1.");
  ...
}

Дайте мне знать, работает ли это.

Гравитон
источник
Отличная статья. Я на самом деле сделал закладку на CP. Спасибо за ссылку, но, как оказалось, проблема гораздо глубже, поскольку участники тестирования не запускают тесты параллельно.
jrista
2
Подождите, сначала вы говорите, что не хотите, чтобы тест запускался параллельно, а затем вы говорите, что проблема в том, что участники тестирования не запускают тесты параллельно ... так что это что?
Graviton
Ссылка, которую вы предоставили, больше не работает. И можно ли это сделать с xunit?
Аллен Ван
1

Для меня в приложении .Net Core Console, когда я хотел запускать тестовые методы (не классы) синхронно, единственное решение, которое сработало, было описано в этом блоге: xUnit: Control the Test Execution Order

user3057544
источник
0

Я добавил атрибут [Collection ("Sequential")] в базовый класс:

    namespace IntegrationTests
    {
      [Collection("Sequential")]
      public class SequentialTest : IDisposable
      ...


      public class TestClass1 : SequentialTest
      {
      ...
      }

      public class TestClass2 : SequentialTest
      {
      ...
      }
    }
Матиас Ромеро
источник
0

Ни один из предложенных ответов пока не помог мне. У меня есть приложение ядра dotnet с XUnit 2.4.1. Я добился желаемого поведения с помощью обходного пути, установив вместо этого блокировку в каждом модульном тесте. В моем случае меня не волновал порядок выполнения, просто тесты были последовательными.

public class TestClass
{
    [Fact]
    void Test1()
    {
        lock (this)
        {
            //Test Code
        }
    }

    [Fact]
    void Test2()
    {
        lock (this)
        {
            //Test Code
        }
    }
}
Мэтт Хейс
источник