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

我有一个 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;

在上面的代码中,在测试库中,_ match Repository_ team Repository将只是 无效. : (

我是否可以做一些像 ConfigureServices 这样的事情,在我的测试库项目中定义依赖注入?

102498 次浏览

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

把你的手柄放进去。Net core 从一开始就考虑到了依赖注入,但这并不意味着你必须使用依赖注入容器。

给出一个更简单的类,比如:

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的模拟来做到这一点。

底线是,你实际上并不需要依赖注入容器来完成你的测试,只需要一个单独的、可控的、实现/模拟/存根/假的测试类依赖关系。

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

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



}

如果您正在使用 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);
}
}

来自 web 项目的 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>();
});
}
}

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

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

举个例子:

  • 我只保留了最初问题的代码片段中的 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>();
}
}
改进的解决方案

我改进了 madjack 的解决方案,将其封装在单个 abstract class中,并添加了四个方法(包括两个 async等价物) ,以回调作为参数。GetRequiredScopedService<TSvc>()现在使用 private static属性 services进行缓存,所以派生类不会一次又一次地创建新实例。另一个优化是使 host static,所以我们不会每次都在派生类中构建它。我还删除了毫无意义的 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;
}
}
从使用的注入服务返回 async result的示例:
            int foobarsCount = await UseSvcAsync<IFoobarSvc, int>(
foobarSvc => foobarSvc.GetCountAsync());
附加信息

我在返回 TResultTask<TResult>的方法中添加了在 true上设置的可选 shouldBeDisposed参数,以备在回调函数主体之外使用相同的服务实例时使用:

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