0

I have implemented Integration Tests for my .Net Core API using xUnit, shared Class Fixtures, and dependency injection so I can use WebApplicationFactory.

public class DatabaseFixture : WebApplicationFactory<Startup> {
    public DatabaseFixture(WebApplicationFactory<Startup>) {
        //Initialize Stuff
    }
}

public class SaveTests : IClassFixture<DatabaseFixture> {
    private DatabaseFixture databaseFixture;

    public SaveTests(DatabaseFixture databaseFixture)
    {
        this.applicationFixture = applicationFixture;
    }

    //Tests.....
}

public class SaveTests2 : IClassFixture<DatabaseFixture> {
    private DatabaseFixture databaseFixture;

    public SaveTests2(DatabaseFixture databaseFixture)
    {
        this.applicationFixture = applicationFixture;
    }

    //Tests.....
}

This has worked well, but, the Test Data is initialized for each test file.

I am looking to update my tests to use a Collection Fixture instead of a Class Fixture however I have been unable to implement a Collection Fixture while injecting WebApplicationFactory.

[CollectionDefinition("SaveTestCollection")]
public class SaveTestCollectionFixture : ICollectionFixture<DatabaseFixture>
{
}

public class DatabaseFixture : WebApplicationFactory<Startup> {
    public DatabaseFixture(WebApplicationFactory<Startup>) {
        //Initialize Stuff
    }
}

[Collection("SaveTestCollection")]
public class SaveTests
{
    DatabaseFixture fixture;

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

The error I get is:

System.AggregateException : One or more errors occurred. (Collection fixture type 'DatabaseFixture' had one or more unresolved constructor arguments: WebApplicationFactory) (The following constructor parameters did not have matching fixture data: DatabaseFixture fixture)
---- Collection fixture type 'DatabaseFixture' had one or more unresolved constructor arguments: WebApplicationFactory`1 factory
---- The following constructor parameters did not have matching fixture data: DatabaseFixture fixture

I understand that the Dependency Injection is failing, but I am not sure how to fix it.

So my question is, am I able to include the WebApplicationFactory via Dependency Injection, or do I need to change how I am using WebApplicationFactory to work with Collection Fixtures?

Any help is greatly appreciated

3 Answers 3

1

A working demo you could follow:

[CollectionDefinition("SaveTestCollection")]
public class SaveTestCollectionFixture : ICollectionFixture<DatabaseFixture>
{
}

public class DatabaseFixture : WebApplicationFactory<Startup>
{
    
}

[Collection("SaveTestCollection")]
public class SaveTests
{
    private readonly DatabaseFixture _fixture;

    public SaveTests(DatabaseFixture fixture)
    {
        _fixture = fixture;
    }
    [Fact]
    public async Task Test()
    {
        var client = _fixture.CreateClient();
        var response = await client.GetAsync(url);

    }

}
Sign up to request clarification or add additional context in comments.

2 Comments

Correct me if im wrong but this does not utilize the WebApplicationFactory at all? It appears as though the constructor has just been removed
Of course you use, just no need use the constructor, DatabaseFixture itself is extends WebApplicationFactory, you just use it which is enough.
1

Ended up implementing the following solution, which is documented on the XUnit Github page:

  • Continue using IClassFixture
  • Only fully execute the fixture constructor the first time in, and keep a static reference to the fully initialized fixture
  • Any additional instances of the fixture constructor exit early, and do not re-generate the test data
  • All Test files reference the static fixture instead of the fixture passed into the test constructor
  • The fixture still needs to be passed into the test constructor so the fixture constructor is fully executed when it is needed

Main Collection to provide the WebApplicationFactory

[CollectionDefinition("Tests")]
public class FixtureCollection : ICollectionFixture<WebApplicationFactory<Startup>>
{

}
  • Fixture uses the Injected WebApplicationFactory to initalize the test data.
  • There is now a static property that keeps a reference to an instance of the fixture
  • If the static Instance has not been defined, run fixture initialization, and set the static instance to reference this instance of the fixture
  • If the static Instance has been defined, do not run fixture initialization
  • This ensures that the Database data is only generated once, no matter how many times the fixture is used
public class SaveTestDatabaseFixture : WebApplicationFactory<Startup>{

    public static SaveTestDatabaseFixture Instance;

    public TestData first_test_data { get; set; }

    public SaveTestDatabaseFixture(WebApplicationFactory<Startup> factory)
    {
          if (Instance != null)
          {
              return;
          }

          var context = factory.GetContext();
          ......
          first_test_data = new TestData();
          context.TestData.Add(first_test_data);
          context.SaveChanges();

          Instance = this;
    }
}

All tests use the static instance of the Database Fixture.

[Collection("Tests")]
public class TestDataTests : IClassFixture<SaveTestDatabaseFixture> {
    private readonly SaveTestDatabaseFixture databaseFixture;

    public TestDataTests (SaveTestDatabaseFixture _)
    {
        this.databaseFixture = SaveTestDatabaseFixture.Instance;
    }

    [Fact]
    public async Task Test1()
    {
        var x = databaseFixture.TestData.Id;
        //Run Test
    }
}

[Collection("Tests")]
public class TestData2Tests : IClassFixture<SaveTestDatabaseFixture> {
    private readonly SaveTestDatabaseFixture databaseFixture;

    public TestData2Tests (SaveTestDatabaseFixture _)
    {
        this.databaseFixture = SaveTestDatabaseFixture.Instance;
    }

    [Fact]
    public async Task Test2()
    {
        var x = databaseFixture.TestData2.Id;
        //Run Test
    }
}

As I have quite a few separate test files, this change reduced the amount of data being saved to the Test Database by 90% (2000 rows down to 200 rows). It also reduced the execution time of the test suite (including Database setup time) from from 53 seconds to 20 seconds.

Comments

0

You do want the data initialization to run for each test file. Your initial approach is correct. Otherwise, you risk creating a shared database for tests, which is something you should avoid as it can break your tests depending on test execution order.

An example - you have an integration test that fetches details of a user account (that is already seeded) named "John Doe". You then have another test that deletes a user account named "John Doe". Assuming you have a shared database which is seeded only once for all tests, these tests will work only if executed in specific order, that is first fetching, then deleting. If you reverse the order, it will fail on fetching, since deleting test removed the resource which fetching test was trying to obtain.

Of course if your seeding process takes ages you may want to optimize it e.g. prepare a bare minimum seed, or different seeds for different type of tests, because sometimes you don't need a whole database seeded just to check if a record exists in a table. There is always some way to make it just a little bit faster, and usually it is worth it since the more integration tests you have the more noticeable the difference will be in total execution time.

2 Comments

For my use case I specifically want to have a shared database. If I am looking to update or delete something, I have a specific entity saved to the database for that purpose. I want to be able to organise my tests in separate groups, and use collection fixtures to generate the test data for each group once, and have each group use the same set of data
Well, then it sounds like you didn't present the whole picture. If you have a specific entity that somehow makes the risk I mentioned in my answer negligible, then maybe you can try a shared database approach. Although I learned from my own experience that it can bite.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.