Before I begin with my recommendations it's probably worth me defining what I mean by "integration tests" as there is some ambiguity with this term. Given that I'm interested in running these tests as part of the development/build process and the purposes is to prove out that "this particular microservice behaves correctly against these external dependency contracts" - then I'm testing the intergration of the "units" of code within this system, not testing the integration of this system with the external dependencies. I find that writing these type of "integration tests" using SpecFlow is a great way to de-couple your behaviour from your code structure, making TDD more realistic and also meaning after a re-factor (which often results in re-factoring the unit tests) you can confirm there are no breaking changes. It also allows you to involve your QA/BA in the process by quantifying in plain English what scenario's you are catering for and how the system behaves for each. It's worth noting that SpecFlow can also be used to automate your "full integration tests", however that's a little more complex to setup as usually involved spinning up SQL servers, Kafka instances, mocked external APIs etc. and also is too slow to run those types of tests on build, whereas the integration tests I will demonstrate below you can quickly run on build like any other unit test. See the footnote on TDDF for a way to use the same set of tests with real dependencies too! To create a SpecFlow project for testing an API add an NUnit test project and install the SpecFlow.NUnit.Runners & Microsoft.AspNetCore.Mvc.Testing NuGet packages into that test project, add a reference to the Api project and then begin creating your tests. My recommendations to consider are below:
  • Create a "WebTestFixture" that inherits from "WebApplicationFactory<Startup>"
    • Where "Startup" is your API Statup class
    • Take constructor params to capture shared class instances from BoDi (the SpecFlow DI container) - e.g. your mocks
    • Override the "ConfigureWebHost" method and use "builder.ConfigureTestServices" to replace any "real" dependencies with mocks defined in the test project
    • Also register any other class instances that you want to share between BoDi and the .NET DI container
  • Create a folder structure that allows you consider the following genres of classes:
    • Infrastructure - e.g. SpecFlow hooks, Value Retrievers, Transformations etc. (basically the custom SpecFlow pipework)
    • TestDataProviders - with a subfolder for each high level dependency you are mocking (e.g. what would be a class library in the real implementation)
      • EachDataProvider - containing:
        • Mock Factory - create a class that will build your default mock for each interface (I prefer to use Moq)
        • StepDefinitions - All the SpecFlow step definitions for interacting with these mocks
      • Mocks Root Class - for easy injection of all your mocks into WebTestFixture and your step definitions
    • FolderPerController - the "tests" live in here so assuming your controllers align with a sensible functional grouping it makes sense to mirror that structure
      • Interactions - create a class which interacts with this controller via the "WebTestFixture.CreateClient()" HttpClient
      • Features - create a SpecFlow feature file per endpoint of the controller - in here create the scenario's this endpoint supports
      • Context - any classes that represent the data context of the controller itself (such as the data you will post, or the response from the API)
      • StepDefinitions - All the SpecFlow step definitions for interacting with this API controller and the assertions of the features

    This structure works well for me as it allows me to keep a separation of code specific to a controller or endpoint (making it easier to see what is involved with which moving part) from each other, but also allows code re-use of steps which are for contriving data in your mocked repositories, with a clear separation again which would match the structure of your class libraries of your project. And of course, once you have defined the features/steps/data required to interact with all mocks and all controllers/endpoints - you can create a high level folder of features that interact across multiple of these, if you have such scenarios to assert. For a strategy of preparing your test data that can be used both in-memory and against a "real" datastore, see TDDF