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

时间:2016-06-09 11:29:57

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

我有一个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。所以这些接口作为params的类只是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的操作,在我的测试库项目中定义依赖注入吗?

7 个答案:

答案 0 :(得分:55)

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

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

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

        var serviceProvider = services.BuildServiceProvider();

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

答案 1 :(得分:21)

.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的实际实现并不重要( 不应该 重要),界面本身很重要。我们提供了IMyInterfaceMyTestClass的“假”实现,但您也可以通过MoqRhinoMocks之类的模拟执行此操作。

总而言之,您实际上并不需要依赖注入容器来完成测试,只需要一个单独的,可控制的,实现/模拟/存根/假的测试类依赖项。

答案 2 :(得分:13)

一种简单的方法,我编写了一个通用的依赖关系解析器帮助程序类,然后在我的单元测试类中构建了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);
        }


    }

答案 3 :(得分:3)

如果您使用的是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>();
                });
    }
}

答案 4 :(得分:2)

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

举个例子:

  • 我只保留了初始问题代码片段中的 IMatchService 依赖项
  • 我在 DoSomething 中添加了一个 MatchController 操作,以便进行测试。
  • 我向 AddIMatchService 类添加了一个 MatchService 方法,以便可以模拟。

请注意,具有 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>();
  }
}

答案 5 :(得分:1)

为什么要在测试类中注入它们? 您通常会测试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);
    }

答案 6 :(得分:0)

我仔细研究了@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>();
    }
}