ASP.NET Core Integration Testing

2022年3月30日 100点热度 0人点赞 0条评论
内容目录
Packages:

Microsoft.AspNetCore.Mvc.Testing
Microsoft.AspNetCore.TestHost
Moq

Integration testing can ensure that application components are functioning correctly at levels that include application support infrastructure such as databases, file systems, and networks. ASP.NET Core supports integration testing by combining unit testing frameworks with testing web hosts and in-memory test servers.

Integration tests confirm that two or more application components work together to produce the expected results, potentially involving every component necessary to handle a complete request.

These broader tests are used to test the application's infrastructure and overall framework, typically including the following components:

  • Databases
  • File Systems
  • Network Devices
  • Request-Response Pipelines

Unit tests use fabricated or mocked objects rather than infrastructure components.

Compared to unit tests, integration tests:

  • Use the actual components that the application would use in a production environment.
  • Require more code and data handling.
  • Take longer to run.

Infrastructure components such as the testing web host and the in-memory test server (TestServer) are provided or managed by the TestServer package. Utilizing this package simplifies the creation and execution of tests.

Do not write integration tests for every possible combination of data and file access via databases and file systems. Regardless of how many locations in the application interact with databases and file systems, a centralized set of read, write, update, and delete integration tests is usually sufficient to thoroughly test the database and file system components. Use unit tests for routine testing of method logic that interacts with these components. In unit tests, using infrastructure fakes/mocks results in faster test execution.

The Microsoft.AspNetCore.Mvc.Testing package handles the following tasks:

  • Copies the dependency files (.deps) from the SUT to the bin directory of the test project.

  • Sets the content root directory to the project root directory of the SUT so that static files and pages/views can be found during test execution.

  • Provides the WebApplicationFactory class to simplify the startup process of the SUT.

Quickly creating a web server:

    public TestServer CreateServer()
    {
        var path = Assembly.GetAssembly(typeof(BasketScenarioBase))
            .Location;

        var hostBuilder = new WebHostBuilder()
            .UseContentRoot(Path.GetDirectoryName(path))
            .ConfigureAppConfiguration(cb =>
            {
                cb.AddJsonFile("appsettings.json", optional: false)
                .AddEnvironmentVariables();
            }).UseStartup<BasketTestsStartup>();

        return new TestServer(hostBuilder);
    }

Next is the actual Startup used, which is inherited by a subclass and injected into ASP.NET Core.

    class BasketTestsStartup : Startup
    {
        public BasketTestsStartup(IConfiguration env) : base(env)
        {
        }

        public override IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.Configure<RouteOptions>(Configuration);
            return base.ConfigureServices(services);
        }

        protected override void ConfigureAuth(IApplicationBuilder app)
        {
            if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant())
            {
                app.UseMiddleware<AutoAuthorizeMiddleware>();
            }
            else
            {
                base.ConfigureAuth(app);
            }
        }
    }

Then by creating a TestServer server object, a corresponding client is created.

    [Fact]
    public async Task Post_basket_and_response_ok_status_code()
    {
        using (var server = CreateServer())
        {
            var content = new StringContent(BuildBasket(), UTF8Encoding.UTF8, "application/json");
            var response = await server.CreateClient()
                .PostAsync("url", content);		// The URL only needs to be filled in as /api/xxx, without the host address.

            response.EnsureSuccessStatusCode();
        }
    }

This allows requests to any API.

Additionally, an instance can also be extracted from the Web Host:

            using (var server = CreateServer())
            {
                var redis = server.Host.Services.GetRequiredService<ConnectionMultiplexer>();

                var redisBasketRepository = BuildBasketRepository(redis);

                var basket = await redisBasketRepository.UpdateBasketAsync(new CustomerBasket("customerId")
                {
                    BuyerId = "buyerId",
                    Items = BuildBasketItems()
                });

                Assert.NotNull(basket);
                Assert.Single(basket.Items);
            }

You can substitute services in tests by calling ConfigureTestServices on the host builder. To inject mock services, the SUT must have a class that includes the Startup.ConfigureServices method.

    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

Set the Web Host to be in development or testing environment:

protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));
			
// If the SUT uses a Web Host (IWebHostBuilder), substitute CreateWebHostBuilder:

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

In integration tests, it's also convenient to configure loading of appsettings.json, appsettings.development.json, appsettings.production.json files.

        config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

痴者工良

高级程序员劝退师

文章评论