Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fake declared services in Startup.cs during testing?

I would like to write integration tests for my Asp .net core application, but I don't want my tests to use real implemetation of some services.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddTransient<IExternalService,ExternalService>();
        ...
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
    }
}

public interface IExternalService
{
    bool Verify(int id);
}

public class ExternalService : IExternalService
{
    public bool Verify(int id)
    {
        //Implemetation is here.
        //I want to fake this implemetation during testing.
    }
}

[Fact]
public void TestCase()
{
    //Stub out service
    var myExtService = new Mock<IExternalService>();

    //Setup response by stub
    myExtService
        .Setup(p => p.Verify(It.IsAny<int>()))
        .Returns(false);

    var host = new WebHostBuilder()
        .UseStartup<Startup>()
        .ConfigureServices((services) =>
        {
            //Setup injection
            services.AddTransient<IExternalService>((a) =>
            {
                return myExtService.Object;
            });
        });

    var server = new TestServer(host);

    var client = server.CreateClient();

    var response = client.GetAsync("/").Result;

    var responseString = response.Content.ReadAsStringAsync().Result;

    Assert.Contains("Your service returned: False", responseString);
}

Current injection setup in test case does not work, because ExternalService is injected over the mock.

However the test will pass when I remove services.AddTransient<IExternalService,ExternalService>; from Startup.

Most likely the one in Startup is called later and all the setup in that class is preferred by application.

What options do I have to setup some dependecies in tests, but use everything else as they are declared in Startup?

UPDATE

  1. Application code should be unaware of tests.
  2. Tests should be aware of:
    • (weakly typed) Endpoint - if this changes then test should fail
    • IExternalService interface
  3. Tests should not care if application has razor pages or uses mvc or how the application is wired between endpoint and IExternalService.
  4. Tests should not have to setup or configure application (apart from stubbing IExternalService) in order to make it work.
    • I understand that WebHostBuilder still has to be created, but my point is that configuration should be bare minimum in test case and majority of configuration should still be described on application side.
like image 243
Siim Haas Avatar asked Oct 16 '17 10:10

Siim Haas


2 Answers

The only thing yo need to change is to use ConfigureTestServices instead of ConfigureServices. ConfigureTestServices runs after your Startup, therefor you can override real implementations with mocks/stubs. ConfigureServices was newer intended for that purpose, rather, it configures "host services", which are used during the host-building phase of the application, and copied into the application's DI container.

ConfigureTestServices is available in ASP Core version 2.1 and higher.

var host = new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureTestServices((services) =>
    {
        //Setup injection
        services.AddTransient<IExternalService>((a) =>
        {
            return myExtService.Object;
        });
    });
like image 132
Krzysztof Branicki Avatar answered Oct 17 '22 01:10

Krzysztof Branicki


The only option I know of is to setup WebHostBuilder with UseEnvironment:

var host = new WebHostBuilder()
            .UseStartup<Startup>()
            .ConfigureServices(services =>
            {
                //Setup injection
                services.AddTransient<IExternalService>(provider =>
                {
                    return myExtService.Object;
                });
            })
            .UseEnvironment("IntegrationTest");

And then add a condition in the ConfigureServices method in the Startup:

public void ConfigureServices(IServiceCollection services)
    {
        if (Configuration["Environment"] != "IntegrationTest")
        {
            services.AddTransient<IExternalService, ExternalService>();
        }

        services.AddMvc();

        // ...
    }

UPDATE:

I did some more poking around and another option is to not use UseStartup extension method but rather configure the WebHostBuilder directly. You can do this a number of ways but I thought that you could possibly create your own extension method to create a template in your tests:

public static class WebHostBuilderExt
{
    public static WebHostBuilder ConfigureServicesTest(this WebHostBuilder @this, Action<IServiceCollection> configureServices)
    {
        @this.ConfigureServices(services =>
            {
                configureServices(services);

                services.AddMvc();
            })
            .Configure(builder =>
            {
                builder.UseMvc();
            });
        return @this;
    }
}

Now your tests can be setup like the following:

        var host = new WebHostBuilder()
            .ConfigureServicesTest(services =>
            {
                //Setup injection
                services.AddTransient<IInternalService>(provider =>
                {
                    return myExtService.Object;
                });
            });

        var server = new TestServer(host);

This means that you will have to explicitly setup all the implementations that the container will resolve for the specific endpoint you are calling. You can choose to mock or use the the concrete implementations.

like image 27
TheRock Avatar answered Oct 17 '22 02:10

TheRock