XUnit DI 通过覆盖启动文件(.net 核心)

XUnit DI through overridden Startup file (.net core)

我已经构建了一个 WebAPI,除了我在 Postman 上的测试 运行 之外,我还想实施一些 Integration/Unit 测试。

现在我的业务逻辑非常薄,大部分时间更多的是 CRUD 操作,因此我想从测试我的控制器开始。

我有一个基本设置。存储库模式(接口)、服务(业务逻辑)和控制器。 流程是 Controller (DI Service) -> Service (DI Repo) -> Repo Action!

所以我所做的是覆盖我的启动文件以更改为内存数据库,其余的应该没问题(我假设)添加了服务,添加了回购协议,现在我指向一个内存数据库适合我的基本测试。

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)
                );
        }
    }
}

我写了第一个测试,但是 DatasourceService 不存在:

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));
        }
    }
}

我错过了什么?

您不能在测试 classes 上使用依赖注入。您只能让 xunit 通过构造函数注入特殊的固定装置(参见 docs)。

对于集成测试,您希望使用 Microsoft.AspNetCore.TestHost 包中的 TestServer class 和单独的 Startup.cs class(比继承更容易设置配置恕我直言)。

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
    }
}

在您的单元测试项目中,您需要创建 TestServer 的实例并执行测试。

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);
    }
}

现在调用写入数据库的操作时,还可以检查数据是否真的写入了数据库:

[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);
}

现在您可以在测试中使用 Xunit.DependencyInjection

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

你的 DI-类:

public interface IDependency
{
    int Value { get; }
}

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

和 XUnit 测试:

public class MyAwesomeTests
{
    private readonly IDependency _d;

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

    [Fact]
    public void AssertThatWeDoStuff()
    {
        Assert.Equal(1, _d.Value);
    }
}