Search code examples
c#.netxunitxunit.net

Managing multiple levels of collection fixtures in xUnit / porting from NUnit


We are currently migrating NUnit tests over to xUnit. In our previous tests, we had multiple levels of test bases:

public class TestBase {
    protected ICacheManagerFactory _cacheManagerFactory;
    [OneTimeSetup]
    public void OneTimeSetup()
    {
        //Setup code
    }
}

public class ControllerTestBase : TestBase {
    [OneTimeSetup]
    public void OneTimeSetup()
    {
        //Setup code
        _cacheManagerFactory.SomeSetup();
    }
}

We want to replicate that functionality in xUnit such that the original TestBase setup isn't ran multiple times. We are trying to use collections within collections like so:

public sealed class CollectionFixtureBase {
    public ICacheManagerFactory CacheManagerFactory;
    public CollectionFixtureBase
    {
        //Setup code
    }
}

[Collection("CollectionFixtureBase")]
public sealed class ControllerCollectionFixture {
    public CollectionFixtureBase CollectionBase;
    public ControllerCollectionFixture(CollectionFixtureBase collectionBase)
    {
        CollectionBase = collectionBase
        CollectionBase.CacheManagerFactory.SomeSetup();
    }
}

(I've omitted the collection fixture definitions for brevity)

However, when we run this code we are getting dependency injection errors:

System.AggregateException : One or more errors occurred. Collection fixture type 'Instanda.UnitTests.New.Collections.ControllerCollectionFixture' had one or more unresolved constructor arguments: CollectionFixtureBase collectionFixture The following constructor parameters did not have matching fixture data: ControllerCollectionFixture collectionFixture

Are we going about this the wrong way? We could inherit from the base classes but I'm under the impression this causes the base code to run multiple times? Is there another way to get this functionality?


Solution

  • When trying to understand CollectionFixture as a newcomer (especially one experienced in NUnit, JUnit and other systems that tend to lead to class hierarchies in ones Test Classes), I'd recommend a good number of passes over https://xunit.net/docs/shared-context ; the model is quite different and subtle. OK, now you're back from there: TL;DR

    1. the main thing xUnit Collection Fixtures achieve is to ensure only one test at a time gets to use the shared thing

    2. xUnit manages them:

      a. makes them (once)

      b. passes them to your test class via the IUse* just before each test runs (remember the key diff between NUnit and xUnit is that there's a Fresh Fixture every time

    3. disposes them when no more classes/tests need them

    4. that's it - no class hierarchy etc

    While Collection Fixtures are the most powerful construct that's hard to write manually, and relatively common for e.g. wrapping databases, don't forget Class Fixtures - they're the work horses for typical OneTimeSetup like in the OP

    When doing your high level break down of how to manage shared fixtures across Test Classes, the main thing you want to do is let as much stuff run concurrently and independently as possible (xUnit runs multiple Test Classes concurrently by default; Collection Fixtures are the key way to constrain this)

    When migrating between the typical class hierarchy you get with the NUnit patterns and OneTimeSetup/Setup/Teardown attributes, general high level advice:

    • try to split helper logic up into individual Fixtures that manage one aspect in a clear manner; often in NUnit your fixtures will tend to 'blend'
    • most stuff that you do in base classes with NUnit maps to Class Fixtures in xUnit (which xUnit makes and supplies to the Test Class before each test)
    • stuff there can absolutely be only one of typically map to Collection Fixtures
    • advanced/rare: for any other helpers that can be safely shared, normally use a Class Fixture with some static aspects initialised by a static ctor or some other guard to manage the concurrent initialisation/access of the state safely
    • its definitely not outlawed, but should become a pretty rare case that you end up with base classes for tests (you put stuff in the constructors and IDisposable.Dispose to replace Setup and Teardown methods)