Запустіть код один раз до і після ВСІХ тестів у xUnit.net


80

TL; DR - Я шукаю еквівалент xUnit MSTest AssemblyInitialize(він же ОДНА функція, яка мені подобається).

Зокрема, я шукаю його, тому що у мене є кілька тестів диму на селен, які я хотів би мати можливість проводити без інших залежностей. У мене є пристрій, який запустить для мене IisExpress і вб’є його при утилізації. Але це робить перед кожним тестом, що надзвичайно зменшує час роботи.

Я хотів би запустити цей код один раз на початку тестування та утилізувати його (завершуючи процес) наприкінці. Як я міг займатися цим?

Навіть якщо я можу отримати програмний доступ до чогось на кшталт "скільки тестів виконується в даний час", я можу щось зрозуміти.


Ви маєте на увазі xUnit як "загальну групу специфічних для мови інструментальних засобів тестування, таких як JUnit, NUnit тощо". або xUnit як "xUnit.net, інструмент тестування модуля .Net"?
Andy Tinkham

3
На основі цієї таблиці xunit.codeplex.com/... я не думаю, що існує еквівалент. Обхідним шляхом було б перенести ініціалізацію збірки в єдиний і викликати її з кожного з ваших конструкторів.
Аллен

@allen - Це схоже на те, що я роблю, але це дає мені ініціалізатор збірки без відключення збірки. Ось чому я запитував про кількість тестів.
Джордж Мауер

Відповіді:


59

Станом на листопад 2015 р. XUnit 2 вийшов, тому існує канонічний спосіб обміну функціями між тестами. Це задокументовано тут .

В основному вам потрібно буде створити клас, який виконує пристрій:

    public class DatabaseFixture : IDisposable
    {
        public DatabaseFixture()
        {
            Db = new SqlConnection("MyConnectionString");

            // ... initialize data in the test database ...
        }

        public void Dispose()
        {
            // ... clean up test data from the database ...
        }

        public SqlConnection Db { get; private set; }
    }

Фіктивний клас із CollectionDefinitionатрибутом. Цей клас дозволяє Xunit створювати тестову колекцію і використовуватиме дане кріплення для всіх тестових класів колекції.

    [CollectionDefinition("Database collection")]
    public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
    {
        // This class has no code, and is never created. Its purpose is simply
        // to be the place to apply [CollectionDefinition] and all the
        // ICollectionFixture<> interfaces.
    }

Потім вам потрібно додати назву колекції до всіх ваших тестових класів. Тестові класи можуть отримати кріплення через конструктор.

    [Collection("Database collection")]
    public class DatabaseTestClass1
    {
        DatabaseFixture fixture;

        public DatabaseTestClass1(DatabaseFixture fixture)
        {
            this.fixture = fixture;
        }
    }

Це трохи детальніше, ніж MsTests, AssemblyInitializeоскільки вам потрібно оголосити на кожному тестовому класі, до якої колекції тестів він належить, але це також більш модульно (і з MsTests вам все одно потрібно поставити TestClass на свої класи)

Примітка: зразки взяті з документації .


10
Коли я читаю це: "... // Цей клас не має коду і ніколи не створюється ...", тоді я дійсно віддаю перевагу реалізації Microsofts з AssemblyInitialize. Набагато елегантніше.
Елізабет

Я згоден, що це трохи дивно
gwenzek

8
@Elisabeth Я щойно використав це і додав [CollectionDefinition("Database collection")]атрибут та ICollectionFixture<DatabaseFixture>інтерфейс до DatabaseFixtureкласу, і все працює. Він видаляє порожній клас і здається мені чистішим!
Крістіан Доулерс

11
Атрибут [Collection] не дозволяє паралельно запускати тести. Чи існує інший підхід до глобальної ініціалізації / знесення в xUnit, який дозволяє запускати тести паралельно?
IT потрапив в WebDAV

1
Я знаю, що це старе, але як у вас є кілька колекцій в одному тестовому класі? Я хочу, щоб він запускав код налаштування бази даних, а також мій код зіставлення.
rball

25

Створіть статичне поле та застосуйте фіналізатор.

Ви можете використовувати той факт, що xUnit створює AppDomain для запуску тестової збірки та вивантажує її після її закінчення. Вивантаження домену програми призведе до запуску фіналізатора.

Я використовую цей метод для запуску та зупинки IISExpress.

public sealed class ExampleFixture
{
    public static ExampleFixture Current = new ExampleFixture();

    private ExampleFixture()
    {
        // Run at start
    }

    ~ExampleFixture()
    {
        Dispose();
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        // Run at end
    }        
}

Редагувати: отримати доступ до приладу за допомогою ExampleFixture.Currentваших тестів.


Цікаво - домен додатків негайно утилізується або це займає деякий час? Іншими словами, що робити, якщо я провів два тестові пробіги попереду?
Джордж Мауер

AppDomain вивантажується відразу після завершення останнього тесту у вашій збірці. Якщо у вас є 100 тестів, що виконуються в одній тестовій збірці, вона буде вивантажена після завершення 100-го тесту.
Джаред Келлс

1
Я використовую цей метод, щоб запустити IISExpress на початку моїх тестів і зупинити його після їх завершення. Це чудово працює в ReSharper і MSBuild на Teamcity.
Jared Kells

1
@GeorgeMauer та Jared, можливо, AppDomain Unloadподія може бути кориснішою? (Звичайно, під час відключення всі ставки вимкнені, але це може бути трохи надійніше з пам’яті)
Рубен Бартелінк

2
Це просто анекдотично, але з моменту розміщення цієї відповіді мій сервер збірки працював місяць і зробив 300 збірок за допомогою цього методу, і він працює нормально.
Джаред Келлс,

21

Для виконання коду при ініціалізації збірки можна це зробити (перевірено за допомогою xUnit 2.3.1)

using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: Xunit.TestFramework("MyNamespace.MyClassName", "MyAssemblyName")]

namespace MyNamespace
{   
   public class MyClassName : XunitTestFramework
   {
      public MyClassName(IMessageSink messageSink)
        :base(messageSink)
      {
        // Place initialization code here
      }

      public new void Dispose()
      {
        // Place tear down code here
        base.Dispose();
      }
   }
}

Див. Також https://github.com/xunit/samples.xunit/tree/master/AssemblyFixtureExample


Привіт, це виглядає цікаво: чи можемо ми отримати більше інформації?
Джоната

1
@JonathaANTOINE Подумайте, що вам слід створити нове запитання stackoverflow, замість того, щоб починати з відкритого питання з 10000 можливих відповідей.
Рольф Крістенсен

гаразд, я зрозумів, як це зробити, і в мене є вхід в блозі :)
Джоната

Викликається конструктор, але dispose не викликається після тесту. (xUnit 2.4.1)
TonyQ

16

Сьогодні це неможливо зробити в рамках. Ця функція запланована для версії 2.0.

Для того, щоб зробити цю роботу до версії 2.0, вам знадобиться виконати значну реструктуризацію фреймворку або написати власні бігуни, які розпізнають ваші власні спеціальні атрибути.


Дякую, Бред, поки я тут, ти маєш уявлення про моє останнє запитання про VS test runner? visualstudiogallery.msdn.microsoft.com/…
Джордж Мауер,

1
Привіт, Бред - ознайомтесь із відповіддю @JaredKells нижче, чи бачите ви якісь проблеми з таким підходом?
Джордж Мауер

1
Відповідно до .NET framework, немає гарантії, коли буде запущений фіналізатор, чи буде він працювати взагалі. Якщо вам це зручно, то, мабуть, його пропозиція чудова. :)
Бред Вілсон,

5
Отже, тепер, коли випущено 2.0, рішення, яке НЕ передбачає необхідності мати загальний базовий клас для всіх тестів (або іншим чином викликати якийсь загальний код), це реалізувати власний підклас XunitTestFramework, виконати ініціалізацію в конструкторі і фіналізатор, а потім позначте збірку TestFrameworkAttribute. Альтернативою фіналізатору, якщо вам це незручно, є додавання об’єкта до захищеної властивості DispositionTracker базового класу TestFramework, який виконує очищення.
Avi Cherry

1
@AviCherry, це зараз має бути відповіддю на вихідне запитання. У мене
чудово вийшло

3

Я використовую AssemblyFixture ( NuGet ).

Що він робить, це забезпечує IAssemblyFixture<T>інтерфейс, який замінює будь-яке місце, IClassFixture<T>де ви хочете, щоб час життя об’єкта був тестовим вузлом.

Приклад:

public class Singleton { }

public class TestClass1 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass1(Singleton singleton)
  {
    _Singleton = singleton;
  }

  [Fact]
  public void Test1()
  {
     //use singleton  
  }
}

public class TestClass2 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass2(Singleton singleton)
  {
    //same singleton instance of TestClass1
    _Singleton = singleton;
  }

  [Fact]
  public void Test2()
  {
     //use singleton  
  }
}

Виправте мене, якщо я помиляюся, але варто сказати, що це xUnit 2+
Джордж Мауер

Це не працює, якщо ви не додасте [assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")]до одного з класів проекту. Довідка
меншеет лама

2

Мене дуже дратувало те, що я не мав можливості виконувати щось наприкінці всіх тестів xUnit. Деякі варіанти тут не такі чудові, оскільки вони передбачають зміну всіх ваших тестів або розміщення їх під однією колекцією (тобто вони виконуються синхронно). Але відповідь Рольфа Крістенсена дала мені необхідну інформацію, щоб дістатися до цього коду. Це трохи довго, але вам потрібно лише додати його у свій тестовий проект, інші зміни коду не потрібні:

using Siderite.Tests;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: TestFramework(
    SideriteTestFramework.TypeName,
    SideriteTestFramework.AssemblyName)]

namespace Siderite.Tests
{
    public class SideriteTestFramework : ITestFramework
    {
        public const string TypeName = "Siderite.Tests.SideriteTestFramework";
        public const string AssemblyName = "Siderite.Tests";
        private readonly XunitTestFramework _innerFramework;

        public SideriteTestFramework(IMessageSink messageSink)
        {
            _innerFramework = new XunitTestFramework(messageSink);
        }

        public ISourceInformationProvider SourceInformationProvider
        {
            set
            {
                _innerFramework.SourceInformationProvider = value;
            }
        }

        public void Dispose()
        {
            _innerFramework.Dispose();
        }

        public ITestFrameworkDiscoverer GetDiscoverer(IAssemblyInfo assembly)
        {
            return _innerFramework.GetDiscoverer(assembly);
        }

        public ITestFrameworkExecutor GetExecutor(AssemblyName assemblyName)
        {
            var executor = _innerFramework.GetExecutor(assemblyName);
            return new SideriteTestExecutor(executor);
        }

        private class SideriteTestExecutor : ITestFrameworkExecutor
        {
            private readonly ITestFrameworkExecutor _executor;
            private IEnumerable<ITestCase> _testCases;

            public SideriteTestExecutor(ITestFrameworkExecutor executor)
            {
                this._executor = executor;
            }

            public ITestCase Deserialize(string value)
            {
                return _executor.Deserialize(value);
            }

            public void Dispose()
            {
                _executor.Dispose();
            }

            public void RunAll(IMessageSink executionMessageSink, ITestFrameworkDiscoveryOptions discoveryOptions, ITestFrameworkExecutionOptions executionOptions)
            {
                _executor.RunAll(executionMessageSink, discoveryOptions, executionOptions);
            }

            public void RunTests(IEnumerable<ITestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
            {
                _testCases = testCases;
                _executor.RunTests(testCases, new SpySink(executionMessageSink, this), executionOptions);
            }

            internal void Finished(TestAssemblyFinished executionFinished)
            {
                // do something with the run test cases in _testcases and the number of failed and skipped tests in executionFinished
            }
        }


        private class SpySink : IMessageSink
        {
            private readonly IMessageSink _executionMessageSink;
            private readonly SideriteTestExecutor _testExecutor;

            public SpySink(IMessageSink executionMessageSink, SideriteTestExecutor testExecutor)
            {
                this._executionMessageSink = executionMessageSink;
                _testExecutor = testExecutor;
            }

            public bool OnMessage(IMessageSinkMessage message)
            {
                var result = _executionMessageSink.OnMessage(message);
                if (message is TestAssemblyFinished executionFinished)
                {
                    _testExecutor.Finished(executionFinished);
                }
                return result;
            }
        }
    }
}

Основні моменти:

  • складання: TestFramework доручає xUnit використовувати ваш фреймворк, який є проксі-сервером для типового
  • SideriteTestFramework також обертає виконавця у власний клас, який потім обгортає раковину повідомлень
  • врешті-решт, виконується метод Finished, зі списком запущених тестів та результатом повідомлення xUnit

Тут можна було б зробити більше роботи. Якщо ви хочете виконувати речі, не дбаючи про запущені тести, ви можете успадкувати від XunitTestFramework і просто обернути раковину повідомлень.


1

Ви можете використовувати інтерфейс IUseFixture, щоб це сталося. Також весь ваш тест повинен успадкувати клас TestBase. Ви також можете використовувати OneTimeFixture безпосередньо з тесту.

public class TestBase : IUseFixture<OneTimeFixture<ApplicationFixture>>
{
    protected ApplicationFixture Application;

    public void SetFixture(OneTimeFixture<ApplicationFixture> data)
    {
        this.Application = data.Fixture;
    }
}

public class ApplicationFixture : IDisposable
{
    public ApplicationFixture()
    {
        // This code run only one time
    }

    public void Dispose()
    {
        // Here is run only one time too
    }
}

public class OneTimeFixture<TFixture> where TFixture : new()
{
    // This value does not share between each generic type
    private static readonly TFixture sharedFixture;

    static OneTimeFixture()
    {
        // Constructor will call one time for each generic type
        sharedFixture = new TFixture();
        var disposable = sharedFixture as IDisposable;
        if (disposable != null)
        {
            AppDomain.CurrentDomain.DomainUnload += (sender, args) => disposable.Dispose();
        }
    }

    public OneTimeFixture()
    {
        this.Fixture = sharedFixture;
    }

    public TFixture Fixture { get; private set; }
}

EDIT: Вирішіть проблему, яку створюють нові кріплення для кожного тестового класу.


Це і неправильна, і погана порада. Методи ApplicationFixtureзапускатимуться до і після кожного тесту, а не лише один раз. Також поганою порадою є завжди успадковувати з того самого TestBase, що використовує одне посилання для успадкування, і ви більше не можете використовувати його для обміну загальними методами між групою пов’язаних класів. Насправді, саме тому IUseFixtureбуло винайдено, щоб не доводиться покладатися на спадщину. Нарешті, ви зауважите, що творець xUnit вже має прийняту відповідь на питання, що до версії 2.0 це неможливо зробити належним чином.
Джордж Мауер

Ну насправді він не запускався перед кожним тестом. Коли ви використовуєте IUseFixture, пристрій створить лише один раз для кожного типу тестового класу. Отже, якщо ви помістите свій код у конструктор Fixture, він буде виконуватися лише один раз. Погано, що він виконується один раз для кожного типу тестового класу, і я це теж знаю. Я думав, що це створить один екземпляр для кожної сесії тесту, але це не так. Я просто модифікую зразок коду, щоб вирішити цю проблему. Використовуйте статичну змінну для зберігання екземпляра Fixture, щоб переконатись, що він створить лише один раз і подію AppDomain Unload для утилізації Fixture.
Khoa Le

0

Чи надає ваш інструмент побудови таку функцію?

У світі Java, використовуючи Maven як інструмент побудови, ми використовуємо відповідні фази життєвого циклу побудови . Наприклад, у вашому випадку (приймальні тести з інструментами, подібними до селену), ви можете ефективно використовувати фази pre-integration-testта і post-integration-testдля запуску / зупинки веб-додатка до / після свого integration-test.

Я майже впевнений, що такий самий механізм можна встановити у вашому середовищі.


З повноцінною системою збірки, звичайно, можливо все. Я можу налаштувати це за допомогою psake або бурчати досить легко. Проблема полягає в тому, що інтегровані Visual Studio запуску тестів не використовують системи збірки просто для запуску своїх тестів, з того, що я бачив у їхніх кодових базах, вони викликаються IDE безпосередньо і самі запускають будь-які dll безпосередньо.
Джордж Мауер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.