如何使用构造函数依赖项注入对asp.net核心应用程序进行单元测试


77

我有一个使用应用程序的startup.cs类中定义的依赖项注入的asp.net核心应用程序:

    public void ConfigureServices(IServiceCollection services)
    {

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration["Data:FotballConnection:DefaultConnection"]));


        // Repositories
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IUserRoleRepository, UserRoleRepository>();
        services.AddScoped<IRoleRepository, RoleRepository>();
        services.AddScoped<ILoggingRepository, LoggingRepository>();

        // Services
        services.AddScoped<IMembershipService, MembershipService>();
        services.AddScoped<IEncryptionService, EncryptionService>();

        // new repos
        services.AddScoped<IMatchService, MatchService>();
        services.AddScoped<IMatchRepository, MatchRepository>();
        services.AddScoped<IMatchBetRepository, MatchBetRepository>();
        services.AddScoped<ITeamRepository, TeamRepository>();

        services.AddScoped<IFootballAPI, FootballAPIService>();

这允许这样的事情:

[Route("api/[controller]")]
public class MatchController : AuthorizedController
{
    private readonly IMatchService _matchService;
    private readonly IMatchRepository _matchRepository;
    private readonly IMatchBetRepository _matchBetRepository;
    private readonly IUserRepository _userRepository;
    private readonly ILoggingRepository _loggingRepository;

    public MatchController(IMatchService matchService, IMatchRepository matchRepository, IMatchBetRepository matchBetRepository, ILoggingRepository loggingRepository, IUserRepository userRepository)
    {
        _matchService = matchService;
        _matchRepository = matchRepository;
        _matchBetRepository = matchBetRepository;
        _userRepository = userRepository;
        _loggingRepository = loggingRepository;
    }

这很整洁。但是当我要进行单元测试时,这成为一个问题。因为我的测试库没有在其中设置依赖项注入的startup.cs。因此,将这些接口作为参数的类将为null。

namespace TestLibrary
{
    public class FootballAPIService
    {
        private readonly IMatchRepository _matchRepository;
        private readonly ITeamRepository _teamRepository;

        public FootballAPIService(IMatchRepository matchRepository, ITeamRepository teamRepository)

        {
            _matchRepository = matchRepository;
            _teamRepository = teamRepository;

在上面的代码中,在测试库中,_matchRepository_teamRepository仅为null。:(

我可以做类似ConfigureServices之类的事情,在我的测试库项目中定义依赖项注入吗?


2
作为测试的一部分,您应该为被测系统(SUT)设置依赖项。通常,您可以通过在创建SUT之前创建依赖项的模拟来做到这一点。但是创建SUT只需调用即可new SUT(mockDependency);进行测试。
斯蒂芬·罗斯

Answers:


31

.net核心中的控制器从一开始就牢记依赖注入,但这并不意味着您需要使用依赖注入容器。

给定一个更简单的类,例如:

public class MyController : Controller
{

    private readonly IMyInterface _myInterface;

    public MyController(IMyInterface myInterface)
    {
        _myInterface = myInterface;
    }

    public JsonResult Get()
    {
        return Json(_myInterface.Get());
    }
}

public interface IMyInterface
{
    IEnumerable<MyObject> Get();
}

public class MyClass : IMyInterface
{
    public IEnumerable<MyObject> Get()
    {
        // implementation
    }
}

因此,在您的应用程序中,您正在使用的依赖项注入容器startup.cs,它仅提供了遇到MyClass时的用法IMyInterface。这并不意味着它是获取实例的唯一方法MyController

单元测试方案中,您可以(并且应该)提供自己的实现(或模拟/存根/伪造),IMyInterface如下所示:

public class MyTestClass : IMyInterface
{
    public IEnumerable<MyObject> Get()
    {
        List<MyObject> list = new List<MyObject>();
        // populate list
        return list;
    }        
}

并在您的测试中:

[TestClass]
public class MyControllerTests
{

    MyController _systemUnderTest;
    IMyInterface _myInterface;

    [TestInitialize]
    public void Setup()
    {
        _myInterface = new MyTestClass();
        _systemUnderTest = new MyController(_myInterface);
    }

}

因此,对于单元测试的范围而言MyController,的实际实现IMyInterface无关紧要(并且也不重要),只有接口本身才重要。我们提供了一个“伪”的IMyInterface通过实现MyTestClass,但是您也可以使用诸如“通过”Moq或“通过”之类的模拟来实现RhinoMocks

最重要的是,您实际上不需要依赖项注入容器来完成您的测试,只需对测试的类依赖项进行单独的,可控制的实现/模拟/存根/伪造。


1
完美的答案。我什至会在您的单元测试中甚至根本不使用DI容器。除了旨在测试DI配置正确性的单元测试(例如,所应用装饰器的顺序)之外的其他
Ric .Net

34
我不确定当您在所有需要注入许多依赖项的类上创建类时,这有多大帮助。我想做的是能够注册默认实现(或具有默认行为的模拟),这样我可以实例化那些对象图而不必先设置30个依赖关系,而是重新配置测试所需的依赖关系。
Sinaesthetic,

1
@Sinaesthetic就是测试和模拟框架的目的。nUnit允许您创建一次性或每次运行的方法,以模拟所有内容,然后在测试中仅考虑配置要测试的方法。实际上,将DI用于测试意味着它不再是单元测试,而是与Microsoft(或第3方)DI进行的集成测试
Erik Philips

1
“实际上使用DI进行测试意味着它不再是单元测试了”,在这里并不能真正与您达成一致,至少从表面上看不是这样。通常,DI对于简单地初始化类是必需的,以便可以测试该单元。关键是要模拟依赖项,以便您可以测试依赖项周围单元的行为。我认为您可能是指一个场景,其中将注入一个功能齐全的依赖项,然后可能是一个集成测试,除非该对象的依赖项也被模拟了。有很多一次性的情况可以讨论。
Sinaesthetic

我们在单元测试中大量使用依赖注入。我们将它们广泛用于模拟目的。我不确定为什么您根本不想在测试中使用DI。我们不是通过软件来设计用于测试的基础结构,而是使用DI来真正轻松地模拟和注入测试所需的对象。ServiceCollection从一开始,我们就可以微调哪些对象可用。它对脚手架特别有用,对集成测试也有帮助,所以...是的,我很乐意在测试中使用DI。
保罗·卡尔顿

124

尽管@Kritner的答案是正确的,但我更喜欢以下代码,以确保代码完整性和更好的DI体验:

[TestClass]
public class MatchRepositoryTests
{
    private readonly IMatchRepository matchRepository;

    public MatchRepositoryTests()
    {
        var services = new ServiceCollection();
        services.AddTransient<IMatchRepository, MatchRepositoryStub>();

        var serviceProvider = services.BuildServiceProvider();

        matchRepository = serviceProvider.GetService<IMatchRepository>();
    }
}

您如何获得通用的GetService <>方法?
Chazt3n '18

抱歉,我无法理解您的意思。请提供更多详细信息?@ Chazt3n
madjack '18

6
GetService<>可以找到一些超载using Microsoft.Extensions.DependencyInjection
Neville Nazerane '18

10
我只是测试了一下。这是比标记答案更有效的答案。这使用DI。我尝试将其用于网站使用的扩展功能。此功能非常有效
Neville Nazerane

9
这不是对服务进行单元测试,而是对Microsoft DI的集成测试。Microsoft已经有单元测试来测试DI,因此没有理由这样做。如果您想测试对象是否已注册,那是关注点分离,应该在自己的测试中。 单元测试和对象意味着无需外部依赖就可以测试对象本身。
Erik Philips

37

一种简单的方法是,我编写了一个通用的依赖项解析器帮助程序类,然后在单元测试类中构建了IWebHost。

通用依赖性解析器

    public class DependencyResolverHelpercs
    {
        private readonly IWebHost _webHost;

        /// <inheritdoc />
        public DependencyResolverHelpercs(IWebHost WebHost) => _webHost = WebHost;

        public T GetService<T>()
        {
            using (var serviceScope = _webHost.Services.CreateScope())
            {
                var services = serviceScope.ServiceProvider;
                try
                {
                    var scopedService = services.GetRequiredService<T>();
                    return scopedService;
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                    throw;
                }
            };
        }
    }
}

单元测试项目

  [TestFixture]
    public class DependencyResolverTests
    {
        private DependencyResolverHelpercs _serviceProvider;

        public DependencyResolverTests()
        {

            var webHost = WebHost.CreateDefaultBuilder()
                .UseStartup<Startup>()
                .Build();
            _serviceProvider = new DependencyResolverHelpercs(webHost);
        }

        [Test]
        public void Service_Should_Get_Resolved()
        {

            //Act
            var YourService = _serviceProvider.GetService<IYourService>();

            //Assert
            Assert.IsNotNull(YourService);
        }


    }

关于如何用Microsoft.Extensions.DependencyInjection替换Autofac的一个很好的示例
lnaie

是的,这应该是默认答案。我们已经放置了一个完整的套件来测试所有要注入的服务,它的工作原理就像一个魅力。谢谢!
facundofarias

嗨,@ Joshua Duxbury,您能帮忙回答这个问题吗?stackoverflow.com/questions/57331395/…尝试实施您的解决方案,还发送了100分,还看了您的其他答案,谢谢!

3
处置范围看来我错了- docs.microsoft.com/en-us/dotnet/api/... - Once Dispose is called, any scoped services that have been resolved from ServiceProvider will be disposed.
尤金·波德斯克

1
您可能需要删除“ using”语句,以避免DBcontexts上出现“处置对象错误”
Mosta

2

如果您使用Program.cs+Startup.cs约定并希望快速进行此工作,则可以单线重用现有的宿主构建器:

using MyWebProjectNamespace;

public class MyTests
{
    readonly IServiceProvider _services = 
        Program.CreateHostBuilder(new string[] { }).Build().Services; // one liner

    [Test]
    public void GetMyTest()
    {
        var myService = _services.GetRequiredService<IMyService>();
        Assert.IsNotNull(myService);
    }
}

Program.csWeb项目的样本文件:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace MyWebProjectNamespace
{
    public class Program
    {
        public static void Main(string[] args) =>
            CreateHostBuilder(args).Build().Run();

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

0

您为什么要将它们注入测试班?例如,通常可以通过使用RhinoMocks之类的工具来创建存根或模拟来测试MatchController 。这是一个使用该示例和MSTest的示例,可以从中进行推断:

[TestClass]
public class MatchControllerTests
{
    private readonly MatchController _sut;
    private readonly IMatchService _matchService;

    public MatchControllerTests()
    {
        _matchService = MockRepository.GenerateMock<IMatchService>();
        _sut = new ProductController(_matchService);
    }

    [TestMethod]
    public void DoSomething_WithCertainParameters_ShouldDoSomething()
    {
        _matchService
               .Expect(x => x.GetMatches(Arg<string>.Is.Anything))
               .Return(new []{new Match()});

        _sut.DoSomething();

        _matchService.AssertWasCalled(x => x.GetMatches(Arg<string>.Is.Anything);
    }

软件包RhinoMocks 3.6.1与netcoreapp1.0(.NETCoreApp,Version = v1.0)不兼容。RhinoMocks 3.6.1软件包支持:net(.NETFramework,Version = v0.0)
ganjan

其他框架正在慢慢采用这种方法。
亚历山德鲁·马尔库莱斯库
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.