32

I'm following this tutorial
Integration Testing with Entity Framework Core and SQL Server

My code looks like this

Integration Test Class

public class ControllerRequestsShould : IDisposable
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    private readonly YourContext _context;

    public ControllerRequestsShould()
    {
        // Arrange
        var serviceProvider = new ServiceCollection()
            .AddEntityFrameworkSqlServer()
            .BuildServiceProvider();

        var builder = new DbContextOptionsBuilder<YourContext>();

        builder.UseSqlServer($"Server=(localdb)\\mssqllocaldb;Database=your_db_{Guid.NewGuid()};Trusted_Connection=True;MultipleActiveResultSets=true")
            .UseInternalServiceProvider(serviceProvider);

        _context = new YourContext(builder.Options);
        _context.Database.Migrate();

        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>()
            .UseEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")));
        _client = _server.CreateClient();
    }

    [Fact]
    public async Task ReturnListOfObjectDtos()
    {
        // Arrange database data
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 1, Code = "PTF0001", Name = "Portfolio One" });
        _context.ObjectDbSet.Add(new ObjectEntity{ Id = 2, Code = "PTF0002", Name = "Portfolio Two" });

        // Act
        var response = await _client.GetAsync("/api/route");
        response.EnsureSuccessStatusCode();


        // Assert
        var result = Assert.IsType<OkResult>(response);            
    }

    public void Dispose()
    {
        _context.Dispose();
    }

As I understand it, the .UseStartUp method ensures the TestServer uses my startup class

The issue I'm having is that when my Act statement is hit

var response = await _client.GetAsync("/api/route");

I get an error in my startup class that the connection string is null. I think My understanding of the problem is that when my controller is hit from the client it injects my data repository, which in turn injects the db context.

I think I need to configure the service as part of the new WebHostBuilder section so that it used the context created in the test. But I'm not sure how to do this.

ConfigureServices method in Startup.cs

        public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(setupAction =>
        {
            setupAction.ReturnHttpNotAcceptable = true;
            setupAction.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
            setupAction.InputFormatters.Add(new XmlDataContractSerializerInputFormatter());
        });

        // Db context configuration
        var connectionString = Configuration["ConnectionStrings:YourConnectionString"];
        services.AddDbContext<YourContext>(options => options.UseSqlServer(connectionString));

        // Register services for dependency injection
        services.AddScoped<IYourRepository, YourRepository>();
    }

3 Answers 3

52

@ilya-chumakov's answer is awesome. I just would like to add one more option

3. Use ConfigureTestServices method from WebHostBuilderExtensions.

The method ConfigureTestServices is available in the Microsoft.AspNetCore.TestHost version 2.1(on 20.05.2018 it is RC1-final). And it lets us override existing registrations with mocks.

The code:

_server = new TestServer(new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureTestServices(services =>
    {
        services.AddTransient<IFooService, MockService>();
    })
);
Sign up to request clarification or add additional context in comments.

4 Comments

The best method. Also you can remove some registration, for example services.RemoveAll(typeof(IHostedService))
Before I saw this answer I was putting my dependency injection in .ConfigureServices in my tests. This meant that I had to use .TryAddTransient in my actual code instead of AddTransient. I like this way a lot better.
Since I updated to .net core 3.0, it looks like I need to delete the previous registration(s) first :-(
best option, saved the day :D
28

Here are two options:

1. Use WebHostBuilder.ConfigureServices

Use WebHostBuilder.ConfigureServices together with WebHostBuilder.UseStartup<T> to override and mock a web application`s DI registrations:

_server = new TestServer(new WebHostBuilder()
    .ConfigureServices(services =>
    {
        services.AddScoped<IFooService, MockService>();
    })
    .UseStartup<Startup>()
);

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        //use TryAdd to support mocking IFooService
        services.TryAddTransient<IFooService, FooService>();
    }
}

The key point here is to use TryAdd methods inside the original Startup class. Custom WebHostBuilder.ConfigureServices is called before the original Startup, so the mocks are registered before the original services. TryAdd doesn't do anything if the same interface has already been registered, thus the real services will not be even touched.

More info: Running Integration Tests For ASP.NET Core Apps.

2. Inheritance / new Startup class

Create TestStartup class to re-configure ASP.NET Core DI. You can inherit it from Startup and override only needed methods:

public class TestStartup : Startup
{
    public TestStartup(IHostingEnvironment env) : base(env) { }

    public override void ConfigureServices(IServiceCollection services)
    {
        //mock DbContext and any other dependencies here
    }
}

Alternatively TestStartup can be created from scratch to keep testing cleaner.

And specify it in UseStartup to run the test server:

_server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());

This is a complete large example: Integration testing your asp .net core app with an in memory database.

4 Comments

How it can override - method ``` public void ConfigureServices(IServiceCollection services)``` in Startup.cs is not Virtual ?
@GauravAroraa, just make a base method virtual. It doesn't break anything.
it did not resolve the problem unless, pushed into another issue
@GauravAroraa, what is the issue? You just need to execute a correct code inside TestStartup class. The way to do it is doesn't matter. It's even possible to pass a variable inside base Startup class stackoverflow.com/a/44486236/5112433
0

@Ilya-Chumakov @lilo.jacob, both answeres are in the good path, but I would like to add the final addition that would do answer completly perfect.

If you have

.ConfigureServices(services =>
                   {
                       serviceCollection
                      .Replace(ServiceDescriptor.Scoped(typeof(IFooService), a => MockService));
                   }
               )

This way if IFooService it's being already added by the Startup, it will be removed and substituted. And in case it's not it will added it.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.