【问题标题】:how to unit test asp.net core application with constructor dependency injection如何使用构造函数依赖注入对 asp.net 核心应用程序进行单元测试
【发布时间】:2016-10-10 00:49:32
【问题描述】:

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

    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。因此,具有这些接口作为参数的类将只是空的。

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 的事情,在我的测试库项目中定义依赖注入吗?

【问题讨论】:

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

标签: c# unit-testing asp.net-core testing dependency-injection


【解决方案1】:

虽然@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 方法的?
  • GetService&lt;&gt; 有一些可以通过using Microsoft.Extensions.DependencyInjection 找到的重载
  • 我刚刚对此进行了测试。这是一个比标记答案更有效的答案。这使用 DI。我尝试在与网站相同的扩展功能上使用它。此功能完美运行
  • 添加了“使用 Microsoft.Extensions.DependencyInjection”,但 ServiceCollection 中仍然缺少“AddTransient”。 Any1 知道如何解决这个问题?
  • 这不是对服务进行单元测试,这是与Microsoft DI 进行的集成测试。微软已经有单元测试来测试 DI,所以没有理由这样做。如果您想测试并且对象已注册,那是关注点分离,应该在它自己的测试中。 单元测试和对象意味着在没有外部依赖的情况下测试对象本身。
【解决方案2】:

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

通用依赖解析器

        using Microsoft.AspNetCore.Hosting;
        using Microsoft.AspNetCore.Mvc;
        using Microsoft.Extensions.Configuration;
        using Microsoft.Extensions.DependencyInjection;
        using Microsoft.Extensions.Hosting;
        public class DependencyResolverHelper
        {
            private readonly IWebHost _webHost;
    
            /// <inheritdoc />
            public DependencyResolverHelper(IWebHost webHost) => _webHost = webHost;
    
            public T GetService<T>()
            {
                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 DependencyResolverHelper _serviceProvider;

            public DependencyResolverTests()
            {

                var webHost = WebHost.CreateDefaultBuilder()
                    .UseStartup<Startup>()
                    .Build();
                _serviceProvider = new DependencyResolverHelper(webHost);
            }
    
            [Test]
            public void Service_Should_Get_Resolved()
            {
                
                //Act
                var YourService = _serviceProvider.GetService<IYourService>();
    
                //Assert
                Assert.IsNotNull(YourService);
            }
    

        }

【讨论】:

  • 一个关于如何用 Microsoft.Extensions.DependencyInjection 替换 Autofac 的好例子
  • 是的,这应该是默认答案。我们已经放置了一个完整的套件来测试所有要注入的服务,它就像一个魅力。谢谢!
  • 嗨@Joshua Duxbury 你能帮忙回答这个问题吗? stackoverflow.com/questions/57331395/… 尝试实施您的解决方案,刚刚发送了 100 分,同时查看您的其他答案,谢谢!
  • 处置范围对我来说似乎是错误的 - docs.microsoft.com/en-us/dotnet/api/… - Once Dispose is called, any scoped services that have been resolved from ServiceProvider will be disposed.
  • 您可能需要删除“using”语句以避免 DBcontexts 上出现“disposed object error”
【解决方案3】:

您在 .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的实际实现并不重要(不应该重要),只有接口本身重要.我们通过MyTestClass 提供了IMyInterface 的“假”实现,但您也可以通过MoqRhinoMocks 等模拟来做到这一点。

归根结底,您实际上并不需要依赖注入容器来完成您的测试,只需要一个单独的、可控的、实施/模拟/存根/假的测试类依赖项。

【讨论】:

  • 完美答案。我什至会在你的单元测试中完全不使用 DI 容器。 Offcourse 除了旨在测试 DI 配置正确性的单元测试,例如应用装饰器的顺序
  • 我不确定当您的类上的类都需要注入许多依赖项时,这有多大帮助。我想做的是能够注册默认实现(或具有默认行为的模拟),以便我可以实例化这些对象图,而无需先设置 30 个依赖项,而是重新配置测试所需的那些。
  • @Sinaesthetic 这就是测试和模拟框架的用途。 nUnit 允许您创建一次性或每次测试运行的方法,允许您模拟所有内容,然后在您的测试中只关心配置您正在测试的方法。实际上,使用 DI 进行测试意味着它不再是 Unit-Test,而是与 Microsoft(或第 3 方)DI 进行的 Integration-Test
  • “实际上使用 DI 进行测试意味着它不再是一个单元测试”在这里不能真正同意你的观点,至少从表面上看是这样。通常,DI 需要简单地初始化类,以便单元可以被测试。关键是依赖项被模拟,以便您可以围绕依赖项测试单元的行为。我认为您可能指的是一种场景,即注入一个全功能依赖项,然后它可能是一个集成测试,除非该对象的依赖项也被模拟。可以讨论许多一次性场景。
  • 我们在单元测试中大量使用依赖注入。我们将它们广泛用于模拟目的。我不确定为什么你不想在测试中使用 DI。我们不是对测试基础设施进行软件工程,而是使用 DI 来让模拟和注入测试所需的对象变得非常容易。我们可以从一开始就微调ServiceCollection 中可用的对象。它对脚手架特别有用,对集成测试也很有帮助......是的,我会在你的测试中使用 DI。
【解决方案4】:

如果您正在使用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.cs 文件:

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

【讨论】:

  • 太棒了!非常感谢。我可以看到在[SetUp] 中设置一个新的 ServiceCollection 可能很有用,甚至可以模拟依赖项。但实际上,我想做的是使用与我的 Web 应用程序相同的服务集合,并针对相同的环境运行测试。干杯!
  • 简洁的绝妙之处!
  • 太棒了。只是一个班轮让我在我想编写的一堆单元测试上相当快。谢谢马修!
【解决方案5】:

您可以使用 asp.net core DI 并在您的测试中注入模拟实例对象。 这是一个完整的工作示例:

为了这个例子:

  • 我只保留了初始问题的代码 sn-p 中的 IMatchService 依赖项
  • 我在MatchController 中添加了DoSomething 操作,以便进行一些测试。
  • 我在IMatchServiceMatchService 类中添加了一个Add 方法,以便模拟一些东西。

请注意,具有SetupMoq 的方法应该是虚拟的。

[Route("api/[controller]")]
public class MatchController : AuthorizedController
{
  private readonly IMatchService _matchService;

  public MatchController(IMatchService matchService)
  {
    _matchService = matchService;
  }

  public virtual int DoSomething()
  {
    return _matchService.Add(1, 2);
  }
}

public interface IMatchService
{
  int Add(int a, int b);
}

public class MatchService : IMatchService
{
  public virtual int Add(int a, int b)
  {
    return a + b;
  }
}

总是可以通过调用Mock.Get 方法来获取Mock。 为了方便每个依赖项,我创建了两个属性,例如 MatchServiceMockedMatchService

public class MyTests
{
  protected IMatchService MatchService { get; set; }

  protected Mock<IMatchService> MockedMatchService => Mock.Get(MatchService);

  private IServiceProvider ServicesProvider { get; set; }

  [SetUp]
  public void SetupBeforeEachTest()
  {
    // Configure DI container
    ServiceCollection services = new ServiceCollection();
    ConfigureServices(services);
    ServicesProvider = services.BuildServiceProvider();

    // Use DI to get instances of IMatchService
    MatchService = ServicesProvider.GetService<IMatchService>();
  }

  // In this test I mock the Add method of the dependency (IMatchService) so that it returns a value I choose
  [Test]
  public void TestMethod()
  {
    // Prepare
    var matchController = ServicesProvider.GetService<MatchController>();
    int expectedResult = 5;
    MockedMatchService.Setup(x => x.Add(It.IsAny<int>(), It.IsAny<int>())).Returns(expectedResult);

    // Act - This will call the real DoSomething method because the MatchController has comes from a Mock with CallBase = true
    int result = matchController.DoSomething();

    // Check
    Assert.AreEqual(expectedResult, result);
  }

  private static void ConfigureServices(IServiceCollection services)
  {
    services.AddScoped<IMatchService>();
    services.AddScoped<MatchController>();
  }
}

【讨论】:

    【解决方案6】:

    为什么要将它们注入测试类? 您通常会测试 MatchController,例如,使用像 RhinoMocks 这样的工具来创建存根或模拟。这是一个使用它和 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);
        }
    

    【讨论】:

    • Package RhinoMocks 3.6.1 与 netcoreapp1.0 (.NETCoreApp,Version=v1.0) 不兼容。软件包 RhinoMocks 3.6.1 支持:net (.NETFramework,Version=v0.0)
    • Other frameworks 正在慢慢接受这一套。
    【解决方案7】:

    我研究了@madjack 和@Kritner 的答案并做出了自己的

    用于依赖注入的基本可继承基测试类

    只需在其中注册您的服务并继承即可。

    public class BaseTester 
    {
        protected IProductService _productService; 
        protected IEmployeeService _employeeService; 
    
        public BaseTester()
        {
            var services = new ServiceCollection();
    
            services.AddTransient<IProductService, ProductService>();
            services.AddTransient<IEmployeeService, EmployeeService>();
    
            var serviceProvider = services.BuildServiceProvider();
    
            _productService = serviceProvider.GetService<IProductService>();
            _employeeService = serviceProvider.GetService<IEmployeeService>();
        }
    }
    

    【讨论】:

      【解决方案8】:
      改进的解决方案

      我改进了 madjack 的解决方案,将其包装在单个 abstract class 中,并添加了四个方法(包括两个 async 等效项),并将回调作为参数。 GetRequiredScopedService&lt;TSvc&gt;() 现在正在使用 private static 属性 services 进行缓存,因此派生类不会一遍又一遍地创建新实例。另一个优化是使hoststatic,所以我们不会每次都在派生类中构建它。我还删除了无意义的 try/catch:

          public abstract class TestWithDependencyInjection
          {
              private static readonly IHost host =
                  Program.CreateHostBuilder(Constants.CommandArgs).Build();
              private static readonly IList<object> services =
                  new List<object>();
      
              private IServiceScope svcScope;
      
              protected async Task<TResult> UseSvcAsync<TSvc, TResult>(
                  Func<TSvc, Task<TResult>> callback, 
                  bool shouldBeDisposed = true)
              {
                  var scopedSvc = GetRequiredScopedService<TSvc>();
                  TResult result = await callback(scopedSvc);
                  if(shouldBeDisposed) 
                      svcScope.Dispose();
                  return result;
              }
      
              protected async Task UseSvcAsync<TSvc>(
                  Func<TSvc, Task> callback)
              {
                  var scopedSvc = GetRequiredScopedService<TSvc>();
                  await callback(scopedSvc);
                  svcScope.Dispose();
              }
      
              protected TResult UseSvc<TSvc, TResult>(
                  Func<TSvc, TResult> callback, bool shouldBeDisposed = true)
              {
                  var scopedSvc = GetRequiredScopedService<TSvc>();
                  TResult result = callback(scopedSvc);
                  if(shouldBeDisposed)
                      svcScope.Dispose();
                  return result;
              }
      
              protected void UseSvc<TSvc>(Action<TSvc> callback)
              {
                  var scopedSvc = GetRequiredScopedService<TSvc>();
                  callback(scopedSvc);
                  svcScope.Dispose();
              }
      
              private TSvc GetRequiredScopedService<TSvc>()
              {
                  var requiredScopedSvc = (TSvc)services.SingleOrDefault(
                      svc => svc is TSvc);
                  if (requiredScopedSvc != null)
                      return requiredScopedSvc;
                  svcScope = host.Services.CreateScope();
                  requiredScopedSvc = svcScope.ServiceProvider
                      .GetRequiredService<TSvc>();
                  services.Add(requiredScopedSvc);
                  return requiredScopedSvc;
              }
          }
      
      使用注入服务返回asyncresult的示例:
                  int foobarsCount = await UseSvcAsync<IFoobarSvc, int>(
                          foobarSvc => foobarSvc.GetCountAsync());
      
      附加信息

      我在返回TResultTask&lt;TResult&gt; 的方法中添加了在true 上设置的可选shouldBeDisposed 参数,以防万一您想在回调主体之外使用相同的服务实例:

                  IFoobarSvc foobarSvc = UseSvc<IFoobarSvc, IFoobarSvc>(
                          foobarSvc => foobarSvc, false);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-12-07
        • 1970-01-01
        • 2021-10-16
        • 1970-01-01
        • 1970-01-01
        • 2015-05-12
        • 1970-01-01
        相关资源
        最近更新 更多