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

gan*_*jan 49 c# asp.net testing unit-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>();
Run Code Online (Sandbox Code Playgroud)

这允许这样的事情:

[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;
    }
Run Code Online (Sandbox Code Playgroud)

这很整洁.但是当我想进行单元测试时会成为一个问题.因为我的测试库没有启动依赖注入的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;
Run Code Online (Sandbox Code Playgroud)

在上面的代码中,在测试库中,_matchRepository和_teamRepository将只为null.:(

我可以执行像ConfigureServices这样的操作,在我的测试库项目中定义依赖注入吗?

mad*_*ack 71

虽然@ 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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我刚测试了这个.这是一个比明确答案更有效的答案.这使用DI.我尝试使用相同的扩展功能,我用于网站.此功能完美无缺 (7认同)
  • `GetService <>`有一些重载,可以使用`使用Microsoft.Extensions.DependencyInjection`找到 (5认同)
  • 这不是“单元测试”服务,这是与Microsoft DI的“集成测试”。Microsoft已经有了用于测试DI的单元测试,因此没有理由这样做。如果您想测试对象是否已注册,那是关注点分离,应该在自己的测试中。* Unit-Testing *和对象意味着无需外部依赖就可以测试对象本身。 (4认同)
  • @ErikPhilips我相信你错过了这里的目的,例如我有一个具有多个依赖项的类,并且每个类都有多个依赖项。通过使用 DI 在测试中创建这些对象,就像这个答案一样,我不是在测试 DI,我只是使用 DI 构建我的对象,而不是编写 30 行“更新”代码来完成同样的事情 - 这需要很多时间来弄清楚。因此,这只是使用 DI 构建用于单元测试的对象。如果使用 DI 在产品代码中有用,那么它也必须对测试代码有用。 (4认同)

Kri*_*ner 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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

在你的测试中:

[TestClass]
public class MyControllerTests
{

    MyController _systemUnderTest;
    IMyInterface _myInterface;

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

}
Run Code Online (Sandbox Code Playgroud)

所以对于单元测试的范围来说MyController,实际执行IMyInterface并不重要(而且应该没关系),只有界面本身才有意义.我们提供了一个"假"的实施IMyInterface通过MyTestClass,但你也可以做到这一点与像通过模拟Moq或RhinoMocks.

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

  • 我不确定当你对所有需要注入多个依赖项的类进行类时,这是多么有用.我想要做的是能够注册默认实现(或具有默认行为的模拟),这样我就可以实例化那些对象图,而不必先设置30个依赖项,而是重新配置我需要进行测试的那些. (19认同)
  • 完美的答案。我什至会在您的单元测试中根本不使用 DI 容器。Offcourse 除了旨在测试 DI 配置正确性的单元测试,例如应用装饰器的顺序 (2认同)
  • @Sinaesthetic 这就是测试和模拟框架的用途。nUnit 允许您创建一次性或每次测试运行的方法,允许您模拟所有内容,然后在您的测试中只关心配置您正在测试的方法。实际上,使用 DI 进行测试意味着它不再是*单元测试*,而是与 Microsoft(或第 3 方)DI 的*集成测试*。 (2认同)
  • “实际上使用 DI 进行测试意味着它不再是单元测试”在那里不能真正同意你的看法,至少不是表面价值。通常,DI 需要简单地初始化类,以便*可以*测试单元。关键是对依赖项进行了模拟,以便您可以围绕依赖项测试单元的行为。我认为您可能指的是一种情况,即注入一个功能齐全的依赖项,然后它可能是一个集成测试,除非该对象的依赖项也被模拟。有许多一次性方案可以讨论。 (2认同)

Jos*_*ury 21

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

单元测试项目

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


    }
Run Code Online (Sandbox Code Playgroud)

  • 处置范围对我来说似乎是错误的 - https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicescope?view=aspnetcore-2.2 - `一旦调用 Dispose,任何范围内的服务已从 ServiceProvider 解决的问题将被处置。`。 (4认同)

Mat*_*kan 13

如果您正在使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Program.cs来自 Web 项目的示例文件:

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>();
                });
    }
}
Run Code Online (Sandbox Code Playgroud)


Yan*_*lla 6

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

为了举例:

  • 我只保留了IMatchService最初问题的代码片段中的依赖关系
  • 我DoSomething在 中添加了一个操作MatchController,以便有一些东西可以测试。
  • 我Add向IMatchService和 类添加了一个方法MatchService,以便有一些东西可以模拟。

请注意,带有Setupwith的方法Moq应该是虚拟的。

[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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

[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;
  }
}
Run Code Online (Sandbox Code Playgroud)