XUnit DI through overridden Startup file (.net core)
Asked Answered
S

2

9

I have build a WebAPI and apart from my tests running on Postman I would like to implement some Integration/Unit tests.

Now my business logic is very thin, most of the time its more of CRUD actions, therefore I wanted to start with testing my Controllers.

I have a basic setup. Repository pattern (interfaces), Services (business logic) and Controllers. The flow goes Controller (DI Service) -> Service (DI Repo) -> Repo Action!

So what I did was override my Startup file to change into a in memory database and the rest should be fine (I would assume) Services are added, repos are added and now I am pointing into a in memory DB which is fine for my basic testing.

namespace API.UnitTests
{    
    public class TestStartup : Startup
    {
        public TestStartup(IHostingEnvironment env)
            : base(env)
        {

        }

        public void ConfigureTestServices(IServiceCollection services)
        {
            base.ConfigureServices(services);
            //services.Replace<IService, IMockedService>();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            base.Configure(app, env, loggerFactory);
        }

        public override void SetUpDataBase(IServiceCollection services)
        {
            var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" };
            var connectionString = connectionStringBuilder.ToString();
            var connection = new SqliteConnection(connectionString);

            services
                .AddEntityFrameworkSqlite()
                .AddDbContext<ApplicationDbContext>(
                    options => options.UseSqlite(connection)
                );
        }
    }
}

I wrote my first test, but the DatasourceService is not there:

The following constructor parameters did not have matching fixture data: DatasourceService datasourceService

namespace API.UnitTests
{
    public class DatasourceControllerTest
    {
        private readonly DatasourceService _datasourceService; 

        public DatasourceControllerTest(DatasourceService datasourceService)
        { 
            _datasourceService = datasourceService;            
        }

        [Xunit.Theory,
        InlineData(1)]
        public void GetAll(int companyFk) {
            Assert.NotEmpty(_datasourceService.GetAll(companyFk));
        }
    }
}

What am I missing?

Sorcim answered 27/12, 2016 at 16:59 Comment(9)
iirc you can't use dependency injection on test classes. You can only let xunit inject special fixtures via constructor (xunit.github.io/docs/shared-context.html see class and collection fixtures). For integrationtests you need to obtain an instance of IServiceProvider and have it resolve your service. For controller testing you have to use TestServer class => learn.microsoft.com/en-us/aspnet/core/testing/integration-testingNatter
Also you may not want to inherit from Startup.cs and instead have a separate class. with the bootstrapping code. overriding doesn't work well with certain configurations (i.e. when you need to execute code after A but before B). Registering two time the same interface may result in exceptions when you call GetRequiredService, because more than one is registered (unless you consequently use services.TryAddXxx<Interface, Implementation>()Natter
'If the test classes need access to the fixture instance, add it as a constructor argument, and it will be provided automatically.' so Fixtures can use DI but you can't DI from startup? well thats a shame. Well of course registering twice etc are to be expected. But still if I could DI through startup in Tests then I would have my test up and running in seconds.Sorcim
Not really, it's no real IoC container. xunit just scans the test unity library for the given class and checks if it implements the given class. But only if the class implements the marker interface IClassFixture<T> or ICollectionFixture<T> and then the only parameter allowed in the constructor is of type TNatter
What I mean is you'd need something like var _datasourceService = provider.GetRequriedService<DatasourceService >(); Assert.NotEmpty(_datasourceService.GetAll(companyFk));Natter
Well not DI per definition but it does a similar job but only for specific classes. Apart from that, now that I want to test my Controller, I will make the HomeControllerTest and in the constructor create a mock Service which requires a Mock Repo? or a Mock Service that doesn't need a repo?Sorcim
If you want make a unit test yes, but you'll have to mock the http context too and that's a pain. If you want integration test, look at the documentation here learn.microsoft.com/en-us/aspnet/core/testing/…. You can use TestServer class to start the web application and with _server.CreateClient() get a HttpClient configured for it, then just call the URL of the service and inspect the results (json, xml, results of in memory database). I'll try to put up a simple example as answerNatter
from what i saw ' _server = new TestServer(new WebHostBuilder().UseStartup<Startup>());' uses Startup is that the Startup.cs file? if so can I use my custom TestStartup that points to the InMemoryDB, also if I use this then all the controllers/services/repos are initialised and DIed ? Also I think there is a services.Replace function but haven't tested it.Sorcim
Yes, that's the idea. See my answer below, it also shows how you can access the IServiceProvider class (the IoC container to resolve services like DbContext)Natter
N
11

You can't use dependency injection on test classes. You can only let xunit inject special fixtures via constructor (see docs).

For Integration Testing you want to use the TestServer class from Microsoft.AspNetCore.TestHost package and a separate Startup.cs class (easier to setup configuration than inheritance imho).

public class TestStartup : Startup
{
    public TestStartup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    public void ConfigureTestServices(IServiceCollection services)
    {
        services.Replace(ServiceDescriptor.Scoped<IService, MockedService>());
        services.AddEntityFrameworkSqlite()
            .AddDbContext<ApplicationDbContext>(
                options => options.UseSqlite(connection)
            );
    }

    public void Configure(IApplicationBuilder app)
    {
        // your usual registrations there
    }
}

In your unit test project, you need to create an instance of the TestServer and perform the test.

public class DatasourceControllerTest
{
    private readonly TestServer _server; 
    private readonly HttpClient _client;

    public DatasourceControllerTest()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<TestStartup>());
        _client = _server.CreateClient();
    }

    [Xunit.Theory,
    InlineData(1)]
    public async Task GetAll(int companyFk) {
        // Act
        var response = await _client.GetAsync($"/api/datasource/{companyFk}");
        // expected result from rest service
        var expected = @"[{""data"":""value1"", ""data2"":""value2""}]";

        // Assert
        // This makes sure, you return a success http code back in case of 4xx status codes 
        // or exceptions (5xx codes) it throws an exception
        response.EnsureSuccessStatusCode();

        var resultString = await response.Content.ReadAsStringAsync();
        Assert.Equals(resultString, expectedString);
    }
}

Now, when you call operations which write to the database, you can also check if the data is really written to the database:

[Xunit.Theory,
InlineData(1)]
public async Task GetAll(int companyFk) {
    // Act
    var response = await _client.DeleteAsync($"/api/datasource/{companyFk}");
    // expected result from rest service

    // Assert
    response.EnsureSuccessStatusCode();

    // now check if its really gone in the database. For this you need an instance 
    // of the in memory Sqlite DB. TestServer has a property Host, which is an IWebHost
    // and it has a property Services which is the IoC container

    var provider = _server.Host.Services;
    var dbContext = provider.GetRequiredService<ApplicationDbContext>();

    var result = await dbContext.YourTable.Where(entity => entity.Id == companyFk).Any();

    // if it was deleted, the query should result in false
    Assert.False(result);
}
Natter answered 27/12, 2016 at 18:22 Comment(8)
you just also answered my comment on my question :) I will try to implement this later this evening. Thanks a lot this will help greatly!Sorcim
How do I make TestServer use my ConfigureTestServices instead of bases class' ConfigureServices?Phyto
@DmytroBogatov: Use .UseEnvironment("Test") on the WebHostBuilderNatter
Thanks! Is there a way to override service? For example if I have UseStartup which defines a service (like, .AddSingleton<...>(...)) and then I use ConfigureServices on WebHostBuilder, I want to override that service. For me it seems to keep the one defined in Startup no matter what.Phyto
'IServiceCollection' does not contain a definition for 'Replace' and no extension method 'Replace' accepting a first argument of type 'IServiceCollection' could be found.Guanabara
@Marcus: There is a Replace method which accepts Service Descriptor services.Replace(ServiceDescriptor.Scoped<TService, TImplementation>());Natter
Startup controller requires an IConfiguration in AspCore 2.1. Is this still the preferred approach for integration tests or is there any other newer way?Radiation
hi @Natter can you answer this question? #57331895 , MohsenEsmailpour provided good answer but wanted to use Startup class, so we didn't have to reinitialize all the dependency injection again in test fixture, goal is to test the repository, not just the controller, I work with person who asked the question, great answers by the way, gave 50 points on other questions, they are very helpful !Helfand
I
7

Now you can use Xunit.DependencyInjection in your tests.

namespace Your.Test.Project
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IDependency, DependencyClass>();
        }
    }
}

your DI-classes:

public interface IDependency
{
    int Value { get; }
}

internal class DependencyClass : IDependency
{
    public int Value => 1;
}

and XUnit-test:

public class MyAwesomeTests
{
    private readonly IDependency _d;

    public MyAwesomeTests(IDependency d) => _d = d;

    [Fact]
    public void AssertThatWeDoStuff()
    {
        Assert.Equal(1, _d.Value);
    }
}
Intermeddle answered 5/3, 2021 at 12:37 Comment(1)
But, I think you can't put Configuration option in Stratup constructVala

© 2022 - 2024 — McMap. All rights reserved.