Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
878 views
in Technique[技术] by (71.8m points)

asp.net core mvc - How to use Microsoft.Extensions.Configuration.IConiguration in my XUnit unit testing

In my Asp.net Core 2.0 application, I am trying to unit test my data service layer (.Net Standard Class Library) that uses the Microsoft.Extensions.Configuration.IConfiguration dependency injection. I am using XUnit and don't know how to pass IConfiguration from my unit test class. I tried the following implementation and getting the error

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

I am really new to the testing frameworks and don't even know if dependency injection can be used as I am trying to do in my code snippet.

My Unit test class is as follow

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

My data service layer is as follow

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"; }
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Unit tests have a very useful habit of exposing design issues. In this case you have made some design choices that prove difficult to test because of tight coupling to framework concerns as well as static concerns.

First, it looks like SqlRestaurantDataCL actually depends on a connection factory

public interface IDbConnectionFactory {
    IDbConnection GetConnection();
}

Which would refactor the data implementation as advised to depend on that abstraction.

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

    //...
}

The assumption is that Dapper is being used to make the query above.

With the introduction of the abstracted dependencies, they can be mocked as needed when testing in isolation.

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

Now if you meant to actually touch the real database then this is not an isolation unit test but instead an integration test, that will have a different approach.

In production code, the factory can be implemented

public class SqlDbConnectionFactory : IDbConnectionFactory {
    private readonly ConnectionSetings connectionSettings;

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

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

Where ConnectionSetings is defined as a simple POCO to store the connection string

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

In the composition root the settings can be extracted from configurations

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

There was really no need to be passing IConfiguration around as it is more of a framework concern that is really only relevant at start up.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...