如何在我的 XUnit 单元测试中使用 Microsoft.Extensions.Configuration.IConiguration

How to use Microsoft.Extensions.Configuration.IConiguration in my XUnit unit testing

在我的 Asp.net Core 2.0 应用程序中,我正在尝试对使用 Microsoft.Extensions.Configuration.IConfiguration 的数据服务层(.Net Standard Class 库)进行单元测试 依赖注入。 我正在使用 XUnit,但不知道如何通过我的单元测试 IConfiguration class。我尝试了以下实现并收到错误

Message: The following constructor parameters did not have matching fixture data: IConfiguration configuration.

我真的是测试框架的新手,甚至不知道是否可以像我在代码片段中尝试的那样使用依赖注入。

我的单元测试class如下

public class SqlRestaurantDataCLUnitTest
{
    private readonly IConfiguration configuration;
    public SqlRestaurantDataCLUnitTest(IConfiguration configuration)
    {
        this.configuration = configuration;
    }
    [Fact]
    public void AddTest()
    {
        var restaurantDataCL = new SqlRestaurantDataCL(configuration);
        var restaurant = new Restaurant
        {
            Name = "TestName",
            Cuisine = CuisineType.None
        };

        var result = restaurantDataCL.Add(restaurant);

        Assert.IsNotType(null, result.Id);    
    }
}

我的数据服务层如下

public class SqlRestaurantDataCL : IRestaurantDataCL
{
    private readonly IConfiguration configuration;
    public SqlRestaurantDataCL(IConfiguration configuration)
    {
        this.configuration = configuration;
    }
    public Restaurant Add(Restaurant restaurant)
    {
        using (var db = GetConnection())
        {
            string insertSql = @"INSERT INTO [dbo].[RESTAURANTS]([Cuisine], [Name]) 
                                OUTPUT INSERTED.*
                                VALUES (@Cuisine, @Name)";

            restaurant = db.QuerySingle<Restaurant>(insertSql, new
            {
                Cuisine = restaurant.Cuisine,
                Name = restaurant.Name
            });

            return restaurant;
        }
    }

    private IDbConnection GetConnection()
    {
        return new SqlConnection(configuration.GetSection(Connection.Name).Value.ToString());
    }
}

public class Connection
{
    public static string Name
    {
        get { return "ConnectionStrings: OdeToFood"; }
    }
}

单元测试有一个非常有用的暴露设计问题的习惯。在这种情况下,由于与框架问题和静态问题的紧密耦合,您做出了一些难以测试的设计选择。

首先,看起来 SqlRestaurantDataCL 实际上依赖于一个连接工厂

public interface IDbConnectionFactory {
    IDbConnection GetConnection();
}

这将根据建议重构数据实现以依赖于该抽象。

public class SqlRestaurantDataCL : IRestaurantDataCL {
    private readonly IDbConnectionFactory factory;

    public SqlRestaurantDataCL(IDbConnectionFactory factory) {
        this.factory = factory;
    }
    public Restaurant Add(Restaurant restaurant) {
        using (var connection = factory.GetConnection()) {
            string insertSql = @"INSERT INTO [dbo].[RESTAURANTS]([Cuisine], [Name]) 
                                OUTPUT INSERTED.*
                                VALUES (@Cuisine, @Name)";

            restaurant = connection.QuerySingle<Restaurant>(insertSql, new {
                Cuisine = restaurant.Cuisine,
                Name = restaurant.Name
            });

            return restaurant;
        }
    }

    //...
}

假设是使用 Dapper 进行上述查询。

随着抽象依赖项的引入,在隔离测试时可以根据需要模拟它们。

public class SqlRestaurantDataCLUnitTest {

    [Fact]
    public void AddTest() {
        //Arrange
        var connection = new Mock<IDbConnection>();
        var factory = new Mock<IDbConnectionFactory>();
        factory.Setup(_ => _.GetConnection()).Returns(connection.Object);

        //...setup the connection to behave as expected

        var restaurantDataCL = new SqlRestaurantDataCL(factory.Object);
        var restaurant = new Restaurant {
            Name = "TestName",
            Cuisine = CuisineType.None
        };

        //Act
        var result = restaurantDataCL.Add(restaurant);

        //Assert
        Assert.IsNotType(null, result.Id);
    }
}

现在,如果您打算实际接触真实数据库,那么这不是隔离单元测试而是集成测试,它将采用不同的方法。

在生产代码中,工厂可以实现

public class SqlDbConnectionFactory : IDbConnectionFactory {
    private readonly ConnectionSetings connectionSettings;

    SqlDbConnectionFactory(ConnectionSetings connectionSettings) {
        this.connectionSettings = connectionSettings;
    }

    public IDbConnection GetConnection() {
        return new SqlConnection(connectionSettings.Name));
    }
}

其中ConnectionSetings定义为一个简单的POCO来存储连接字符串

public class ConnectionSetings {
    public string Name { get; set; }
}

在组合根中,可以从配置中提取设置

IConfiguration Configuration; //this would have been set previously

public void ConfigureServices(IServiceCollection services) {
    //...

    var settings = Configuration
        .GetSection("ConnectionStrings:OdeToFood")
        .Get<ConnectionSetings>();

    //...verify settings (if needed)

    services.AddSingleton(settings);
    services.AddSingleton<IDbConnectionFactory,SqlDbConnectionFactory>();
    services.AddSingleton<IRestaurantDataCL,SqlRestaurantDataCL>();
    //Note: while singleton was used above, You can decide to use another scope
    //      if so desired.
}

确实没有必要传递 IConfiguration,因为它更像是一个框架问题,实际上只在启动时相关。