xUnit.net의 모든 테스트 전후에 한 번 코드 실행


80

TL; DR-MSTest의 xUnit에 해당하는 기능을 찾고 있습니다 AssemblyInitialize(내가 좋아하는 하나의 기능).

특히 다른 종속성없이 실행할 수있는 Selenium 연기 테스트가 있기 때문에 찾고 있습니다. 나는 나를 위해 IisExpress를 시작하고 처분 할 때 죽일 Fixture가 있습니다. 그러나 모든 테스트 전에 이것을 수행하면 런타임이 엄청나게 부풀어집니다.

테스트를 시작할 때이 코드를 한 번 트리거하고 마지막에 폐기 (프로세스 종료)하고 싶습니다. 어떻게하면 되나요?

"현재 실행중인 테스트 수"와 같은 것에 프로그래밍 방식으로 액세스 할 수 있어도 뭔가 알아낼 수 있습니다.


xUnit을 "JUnit, NUnit 등과 같은 언어 별 단위 테스트 도구의 일반적인 그룹"으로 의미합니까? 또는 "xUnit.net, .Net 단위 테스트 도구"로 xUnit?
— Andy Tinkham

3
이 테이블을 기반으로 xunit.codeplex.com/… 이에 상응하는 것이 있다고 생각하지 않습니다. 해결 방법은 어셈블리 초기화를 싱글 톤으로 이동하고 각 생성자에서 호출하는 것입니다.
— allen

@allen-내가하는 일과 비슷하지만 어셈블리 분해없이 어셈블리 이니셜 라이저를 제공합니다. 이것이 내가 테스트 횟수에 대해 묻는 이유입니다.
— George Mauer

답변:


59

2015 년 11 월부터 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;
        }
    }

AssemblyInitialize어떤 테스트 컬렉션이 속하는지 각 테스트 클래스에 선언해야하기 때문에 MsTests보다 조금 더 장황 하지만 모듈화도 더 가능합니다.

참고 : 샘플은 설명서 에서 가져 왔습니다 .


10
내가 이것을 읽을 때 : "... //이 클래스에는 코드가 없으며 생성되지 않았습니다 ..."그러면 Microsoft의 AssemblyInitialize 구현을 선호합니다. 훨씬 더 우아합니다.
— Elisabeth

나는 조금 이상한 동의
— gwenzek

8
@Elisabeth 방금 이것을 사용하고 [CollectionDefinition("Database collection")]속성과 ICollectionFixture<DatabaseFixture>인터페이스를 DatabaseFixture클래스에 추가했으며 모든 것이 작동합니다. 빈 클래스를 제거하고 나에게 더 깨끗해 보입니다!
— Christian Droulers

11
[Collection] 속성은 테스트가 병렬로 실행되는 것을 방지합니다. 테스트를 병렬로 실행할 수있는 xUnit의 전역 초기화 / 해체에 대한 다른 접근 방식이 있습니까?
— IT Hit 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이 즉시 폐기됩니까 아니면 시간이 걸리나요? 즉, 두 번의 테스트가 연속적으로 실행되면 어떻게됩니까?
— George Mauer 2013

어셈블리의 마지막 테스트 실행이 완료된 직후 AppDomain이 언로드됩니다. 동일한 테스트 어셈블리에서 연속적으로 실행되는 100 개의 테스트가있는 경우 100 번째 테스트가 완료된 후 언로드됩니다.
— Jared Kells 2013

1
이 방법을 사용하여 테스트를 시작할 때 IISExpress를 시작하고 모든 테스트가 완료된 후 중지합니다. ReSharper 및 Teamcity의 MSBuild에서 잘 작동합니다.
— Jared Kells 2013

1
@GeorgeMauer와 Jared, 아마도 AppDomain Unload이벤트가 더 유용할까요? (물론 셧다운 중에는 모든 베팅이 해제되지만 메모리에서 조금 더 안정적 일 수 있습니다.)
— Ruben Bartelink

2
이것은 일화 일화이지만이 답변을 게시 한 이후로 내 빌드 서버는 한 달 동안 실행 되었으며이 방법을 사용하여 300 개의 빌드를 수행했으며 정상적으로 작동합니다.
— Jared Kells 2013 년

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 도 참조하십시오.


안녕하세요, 흥미로워 보입니다. 더 많은 정보를 얻을 수 있습니까?
— Jonatha ANTOINE 2019-08-12

1
@JonathaANTOINE 가능한 답변이 10000 개있는 열린 질문으로 시작하는 대신 새로운 스택 오버플로 질문을 만들어야한다고 생각합니다.
— Rolf Kristensen

좋아, 나는 이것을하는 방법을 알아 냈고 나는 들어오는 블로그 포스트가있다 :)
— Jonatha ANTOINE aug

생성자가 호출되지만 dispose는 테스트 케이스 후에 호출되지 않습니다. (xUnit 2.4.1)
— TonyQ

16

오늘날 프레임 워크에서는 불가능합니다. 이것은 2.0에 계획된 기능입니다.

2.0 이전에이 작업을 수행하려면 프레임 워크에서 중요한 재 아키텍처를 수행하거나 고유 한 특성을 인식하는 자체 러너를 작성해야합니다.


감사합니다 브래드, 내가 여기있는 한 VS 테스트 러너에 대한 가장 최근의 질문에 대한 아이디어가 있습니까? visualstudiogallery.msdn.microsoft.com/…
— George Mauer

1
안녕 Brad-아래 @JaredKells의 답변을 확인하십시오. 그 접근 방식에 문제가 있습니까?
— George Mauer 2013

1
.NET 프레임 워크에 따르면 종료자가 언제 실행 될지 또는 전혀 실행 될지에 대한 보장이 없습니다. 그게 마음에 든다면 그의 제안은 괜찮은 것 같아요. :)
— Brad Wilson

5
이제 2.0이 출시되었으므로 모든 테스트에 대해 공통 기본 클래스를 가질 필요가없는 솔루션 (또는 일부 공통 코드 호출)은 XunitTestFramework의 자체 하위 클래스를 구현하고 생성자에서 초기화를 수행하는 것입니다. 종료 자 및 TestFrameworkAttribute로 어셈블리를 표시합니다. 종료 자 대신 사용할 수있는 방법은 정리를 수행하는 기본 클래스 TestFramework의 보호 된 DisposalTracker 속성에 개체를 추가하는 것입니다.
— 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  
  }
}

내가 틀렸다 그러나이 있음을 언급 그 가치가 2 + xUnit의 경우 정정 해줘
— 조지 마우어

[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")]프로젝트의 클래스 중 하나에 추가하지 않으면 작동하지 않습니다 . 참조
— manjeet lama 201

2

모든 xUnit 테스트가 끝날 때 실행할 수있는 옵션이 없어서 상당히 짜증이났습니다. 여기에있는 일부 옵션은 모든 테스트를 변경하거나 하나의 컬렉션에 넣는 (동 기적으로 실행됨을 의미) 포함하기 때문에 그다지 좋지 않습니다. 그러나 Rolf Kristensen의 대답은이 코드를 얻는 데 필요한 정보를 제공했습니다. 약간 길지만 테스트 프로젝트에 추가하기 만하면되고 다른 코드 변경은 필요하지 않습니다.

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; }
}

편집 : 새 조명기가 각 테스트 클래스에 대해 생성하는 문제를 수정합니다.


이것은 잘못된 조언이자 나쁜 조언입니다. 의 메소드는 ApplicationFixture단 한 번이 아니라 각 테스트 전후에 실행됩니다. TestBase하나의 상속 링크를 사용 하는 것과 동일하게 항상 상속하는 것도 좋지 않으며 더 이상 관련 클래스 그룹간에 공통 메서드를 공유하는 데 사용할 수 없습니다. 사실, 그것이 바로 IUseFixture상속에 의존 할 필요가없는 발명 된 이유 입니다. 마지막으로, xUnit의 제작자는 2.0이 출시 될 때까지 제대로 수행 할 수 없다는 질문에 대한 대답을 이미 받아 들였습니다.
— George Mauer

실제로 각 테스트 전에 실행되지 않습니다. IUseFixture를 사용할 때 Fixture는 각 유형의 테스트 클래스에 대해 한 번만 생성합니다. 따라서 코드를 Fixture 생성자에 넣으면 한 번만 실행됩니다. 나쁜 점은 각 유형의 테스트 클래스에 대해 한 번 실행된다는 것입니다. 나는 그것이 각 테스트 세션에 대해 하나의 인스턴스를 만들 것이라고 생각했지만 그렇지 않습니다. 이 문제를 해결하기 위해 샘플 코드를 수정합니다. 정적 변수를 사용하여 Fixture 인스턴스를 저장하여 한 번만 생성되도록하고 AppDomain Unload 이벤트를 사용하여 Fixture를 처리합니다.
— Khoa Le

0

빌드 도구가 이러한 기능을 제공합니까?

Java 세계에서 Maven 을 빌드 도구로 사용할 때 빌드 수명주기 의 적절한 단계를 사용합니다 . 예를 들어 귀하의 경우 (Selenium과 유사한 도구로 수용 테스트) pre-integration-test및 post-integration-test단계를 잘 사용 하여 웹 응용 프로그램 을 시작 / 중지 할 수 integration-test있습니다.

귀하의 환경에서 동일한 메커니즘을 설정할 수 있다고 확신합니다.


완전한 빌드 시스템으로 모든 것이 가능합니다. 나는 이것을 psake 또는 grunt로 상당히 쉽게 설정할 수 있습니다. 문제는 Visual Studio 통합 테스트 실행기가 단순히 테스트를 실행하기 위해 빌드 시스템을 사용하지 않는다는 것입니다. 내가 본 코드베이스에서 IDE에서 직접 호출하고 dll을 직접 실행합니다.
— George Mauer
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.