Net Core: Execute All Dependency Injection in Xunit Test for AppService, Repository, etc
Asked Answered
S

3

26

I am trying to implement Dependency Injection in Xunit test for AppService. Ideal goal is to run the original application program Startup/configuration, and use any dependency injection that was in Startup, instead of reinitializing all the DI again in my test, thats the whole Goal in question.

Update: Mohsen's answer is close. Need to update couple syntax/requirement errors to work.

For some reason, original application works and can call Department App Service. However, it cannot call in Xunit. Finally got Testserver working using Startup and Configuration from original application. Now receiving error below:

Message: The following constructor parameters did not have matching fixture data: IDepartmentAppService departmentAppService

namespace Testing.IntegrationTests
{
    public class DepartmentAppServiceTest
    {
        public DBContext context;
        public IDepartmentAppService departmentAppService;

        public DepartmentAppServiceTest(IDepartmentAppService departmentAppService)
        {
            this.departmentAppService = departmentAppService;
        }

        [Fact]
        public async Task Get_DepartmentById_Are_Equal()
        {
            var options = new DbContextOptionsBuilder<SharedServicesContext>()
                .UseInMemoryDatabase(databaseName: "TestDatabase")
                .Options;
            context = new DBContext(options);

            TestServer _server = new TestServer(new WebHostBuilder()
                .UseContentRoot("C:\\OriginalApplication")
                .UseEnvironment("Development")
                .UseConfiguration(new ConfigurationBuilder()
                    .SetBasePath("C:\\OriginalApplication")
                    .AddJsonFile("appsettings.json")
                    .Build()).UseStartup<Startup>());

            context.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
            context.SaveChanges();

            var departmentDto = await departmentAppService.GetDepartmentById(2);

            Assert.Equal("123", departmentDto.DepartmentCode);
        }
    }
}

I am receiving this error:

Message: The following constructor parameters did not have matching fixture data: IDepartmentAppService departmentAppService

Need to use Dependency injection in testing just like real application. Original application does this. Answers below are not currently sufficient , one uses mocking which is not current goal, other answer uses Controller which bypass question purpose.

Note: IDepartmentAppService has dependency on IDepartmentRepository which is also injected in Startup class, and Automapper dependencies. This is why calling the whole startup class.

Good Resources:

how to unit test asp.net core application with constructor dependency injection

Dependency injection in Xunit project

Surtax answered 2/8, 2019 at 17:55 Comment(4)
Do you want to test at controller level or your application service layer?Tweeter
You requirements are bit confusing, XUnit is a unit testing framework, it's not an integration test framework, you are pretty much trying to create an integration test with unit test framework, which is bound to have confusion. DI shall happen in the application via entry point resolving all the dependencies, not via a xunit test case. You shall use a Mocking framework with XUnit.Stubbed
@Surtax I saw your comments on old answers of mine. Was on holiday. Has your question been answered by now? Please mark the answer then please. If not, I could look in to it somewhere this week. Thanks.Fawcett
Try this xunit di support built into xunit framework: nuget.org/packages/Xunit.Di, so that you can inject services dependencies the same way as you do for any other applications.Habited
W
23

You are mixing the unit test with the integration test. TestServer is for integration tests, and if you want to reuse the Startup class to avoid registering dependencies again, you should use HttpClient and make an HTTP call to the controller and action that use IDepartmentAppService.

If you want to do a unit test, you need to setup DI and register all needed dependencies to test IDepartmentAppService.

Using DI through Test Fixture:

public class DependencySetupFixture
{
    public DependencySetupFixture()
    {
         var serviceCollection = new ServiceCollection();
         serviceCollection.AddDbContext<SharedServicesContext>(options => options.UseInMemoryDatabase(databaseName: "TestDatabase"));
         serviceCollection.AddTransient<IDepartmentRepository, DepartmentRepository>();
         serviceCollection.AddTransient<IDepartmentAppService, DepartmentAppService>();

         ServiceProvider = serviceCollection.BuildServiceProvider();
    }

    public ServiceProvider ServiceProvider { get; private set; }
}

public class DepartmentAppServiceTest : IClassFixture<DependencySetupFixture>
{
    private ServiceProvider _serviceProvide;

    public DepartmentAppServiceTest(DependencySetupFixture fixture)
    {
        _serviceProvide = fixture.ServiceProvider;
    }

    [Fact]
    public async Task Get_DepartmentById_Are_Equal()
    {
        using(var scope = _serviceProvider.CreateScope())
        {   
            // Arrange
            var context = scope.ServiceProvider.GetServices<SharedServicesContext>();
            context.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
            context.SaveChanges();

            var departmentAppService = scope.ServiceProvider.GetServices<IDepartmentAppService>();

            // Act
            var departmentDto = await departmentAppService.GetDepartmentById(2);

            // Arrange
            Assert.Equal("123", departmentDto.DepartmentCode);           
        }
    }
}

Using dependency injection with unit tests is not a good idea, and you should avoid that. By the way, if you want, don't repeat yourself when registering dependencies; You can register your dependencies using an extension method and use that class anywhere you want.

Using DI through Startup.cs:

public static class ServiceCollectionExtension
{
    public static IServiceCollection AddDependencies(this IServiceCollection services, IConfiguration configuration)
    {
         services
            .AddDbContext<SomeContext>(options => options.UseSqlServer(configuration["ConnectionString"]));
         services.AddScoped<IDepartmentRepository, DepartmentRepository>();
         services.AddScoped<IDepartmentAppService, DepartmentAppService>();
         .
         .
         .

         return services;
    }
}

In the Startup class and ConfigureServices method just use the extension method to register your dependencies:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
         services.AddMvc();
         services.AddDependencies();
         .
         .
         .

And in your test project:

public class DependencySetupFixture
{
    public DependencySetupFixture()
    {
          var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", false, true));
         configuration = builder.Build();

         var services = new ServiceCollection();

         // services.AddDependencies(configuration);
         // or
         // services = new Startup(configuration).ConfigureServices(services);

         ServiceProvider = services.BuildServiceProvider();
    }

    public ServiceProvider ServiceProvider { get; private set; }
}

And in the test method:

[Fact]
public async Task Get_DepartmentById_Are_Equal()
{
    using (var scope = _serviceProvider.CreateScope())
    {
        // Arrange
        var departmentAppService = scope.ServiceProvider.GetServices<IDepartmentAppService>();

        // Act
        var departmentDto = await departmentAppService.GetDepartmentById(2);

        // Arrange
        Assert.Equal("123", departmentDto.DepartmentCode);
    }
}
Wakashan answered 4/8, 2019 at 6:20 Comment(7)
Syntax error: not sure why comments deleted, this line is giving me an error and need be fixed // services = new Startup(configuration).ConfigureServices(services); need hostenvironment nameBough
Requirement issue: additionally Configuration builder needs to be set to project directories in question above "C:\\Test\\Test.WebAPI", Not Current Test Directory , new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", false, true)); configuration = builder.Build();Bough
You can load appsetting.josn file by reflection from project directory.Wakashan
ok will research that, spent many hours researching this question, feel free to update answer also, thanksSurtax
hi @MohsenEsmailpour as you recommended, I think we should retain "Using DI through Test Fixture" portion, and delete second portion, "Using DI through Startup.cs", unless second portion has any benefits over Custom Web Application Factory, I think web app factory is MS recommended way, plus you recommended it to me! I will send you points for bounty in couple days, thank you! already gave thumbs upSurtax
is it possible to do constructor injection, so there is no need to use .GetService?Epideictic
I don't think so, this kind of test is integration test and basically you should not do dependency in unit test.Wakashan
S
19

Use Custom Web Application Factory and ServiceProvider.GetRequiredService below, feel free to edit and optimize the answer

CustomWebApplicationFactory:

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration((hostingContext, configurationBuilder) =>
        {
            var type = typeof(TStartup);
            var path = @"C:\\OriginalApplication";

            configurationBuilder.AddJsonFile($"{path}\\appsettings.json", optional: true, reloadOnChange: true);
            configurationBuilder.AddEnvironmentVariables();
        });

        // if you want to override Physical database with in-memory database
        builder.ConfigureServices(services =>
        {
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();

            services.AddDbContext<ApplicationDBContext>(options =>
            {
                options.UseInMemoryDatabase("DBInMemoryTest");
                options.UseInternalServiceProvider(serviceProvider);
            });
        });
    }
}

Integration Test:

public class DepartmentAppServiceTest : IClassFixture<CustomWebApplicationFactory<OriginalApplication.Startup>>
{
    public CustomWebApplicationFactory<OriginalApplication.Startup> _factory;
    public DepartmentAppServiceTest(CustomWebApplicationFactory<OriginalApplication.Startup> factory)
    {
        _factory = factory;
        _factory.CreateClient();
    }

    [Fact]
    public async Task ValidateDepartmentAppService()
    {      
        using (var scope = _factory.Server.Host.Services.CreateScope())
        {
            var departmentAppService = scope.ServiceProvider.GetRequiredService<IDepartmentAppService>();
            var dbtest = scope.ServiceProvider.GetRequiredService<ApplicationDBContext>();
            dbtest.Department.Add(new Department { DepartmentId = 2, DepartmentCode = "123", DepartmentName = "ABC" });
            dbtest.SaveChanges();
            var departmentDto = await departmentAppService.GetDepartmentById(2);
            Assert.Equal("123", departmentDto.DepartmentCode);
        }
    }
}

Resources:

https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2

https://fullstackmark.com/post/20/painless-integration-testing-with-aspnet-core-web-api

Surtax answered 7/8, 2019 at 4:48 Comment(1)
Note that this throws TestServer constructor was not called with a IWebHostBuilder so IWebHost is not available. on .Net core 3+Unger
K
2

When you are testing. You need to use mocking libraries or Inject your service directly on contructor ie.

public DBContext context;
public IDepartmentAppService departmentAppService;

/// Inject DepartmentAppService here
public DepartmentAppServiceTest(DepartmentAppService departmentAppService)
{
    this.departmentAppService = departmentAppService;
}
Kief answered 2/8, 2019 at 18:4 Comment(9)
I cannot test with dependency injection? I can use dependency injection in real application but not with Xunit? Dont want to utilize mock library, this integration test not unit testSurtax
The default DI library uses Startup.cs and you can't inject default DI in your test class. Use your test server API endpoints or inject your services on constructor @AlanWalkerKief
You cant test directly. You can test the controllers that DepartmentAppService used in. You should create client to request on your test server @AlanWalkerKief
well I will need to test repository, app layers, api layer, so controller test may not only work, thanks though, disappointing Microsoft and Xunit did not create libraries for this yet, I have a whole list of dependency injections I need to run throughSurtax
isnt there a way I can replace DepartmentAppService with a variable name? I dont want to refer to it again, after its declared in startupSurtax
is it possible to use this? #37725238 answer from Joshua Duxbury trying to get this to workSurtax
this avoids the questionAstto
hi Yigit answer above, please reedit or feel free to remove answer if needed, thanksBough
added answer aboveSurtax

© 2022 - 2024 — McMap. All rights reserved.