So testen Sie die asp.net-Kernanwendung mit der Konstruktorabhängigkeitsinjektion

76

Ich habe eine asp.net-Kernanwendung, die die Abhängigkeitsinjektion verwendet, die in der Startup.cs-Klasse der Anwendung definiert ist:

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

Dies ermöglicht so etwas:

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

Das ist sehr ordentlich. Wird aber zum Problem, wenn ich Unit-Test machen möchte. Weil meine Testbibliothek keine startup.cs hat, in der ich die Abhängigkeitsinjektion einrichte. Eine Klasse mit diesen Schnittstellen als Parameter ist also einfach null.

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

        public FootballAPIService(IMatchRepository matchRepository, ITeamRepository teamRepository)

        {
            _matchRepository = matchRepository;
            _teamRepository = teamRepository;

Im obigen Code sind in der Testbibliothek _matchRepository und _teamRepository nur null . :(

Kann ich so etwas wie ConfigureServices ausführen, bei dem ich die Abhängigkeitsinjektion in meinem Testbibliotheksprojekt definiere?

Ganjan
quelle
2
Im Rahmen Ihres Tests sollten Sie die Abhängigkeiten für Ihr System Under Test (SUT) einrichten. Normalerweise erstellen Sie dazu Mock's der Abhängigkeiten, bevor Sie das SUT erstellen. Das einfache Aufrufen des SUT new SUT(mockDependency);ist jedoch für Ihren Test in Ordnung.
Stephen Ross

Antworten:

31

Ihre Controller im .net-Kern berücksichtigen von Anfang an die Abhängigkeitsinjektion. Dies bedeutet jedoch nicht, dass Sie einen Abhängigkeitsinjektionscontainer verwenden müssen.

Bei einer einfacheren Klasse wie:

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
    }
}

In Ihrer App verwenden Sie also den Abhängigkeitsinjektionscontainer in Ihrer App, der lediglich startup.cseine Konkretion für MyClassdie Verwendung bereitstellt , wenn er IMyInterfaceauftritt. Dies bedeutet jedoch nicht, dass dies der einzige Weg ist, um Instanzen von zu erhalten MyController.

In einem Unit- Test-Szenario können (und sollten) Sie Ihre eigene Implementierung (oder Mock / Stub / Fake) IMyInterfacewie folgt bereitstellen :

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

und in deinem Test:

[TestClass]
public class MyControllerTests
{

    MyController _systemUnderTest;
    IMyInterface _myInterface;

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

}

Für den Umfang der Unit-Tests MyControllerspielt die tatsächliche Implementierung von IMyInterfacekeine Rolle (und sollte auch keine Rolle spielen), sondern nur die Schnittstelle selbst. Wir haben eine "gefälschte" Implementierung von IMyInterfacethrough bereitgestellt MyTestClass, aber Sie können dies auch mit einem Mock wie through Moqoder tun RhinoMocks.

Unterm Strich benötigen Sie nicht den Abhängigkeitsinjektionscontainer, um Ihre Tests durchzuführen, sondern nur eine separate, kontrollierbare Implementierung / Mock / Stub / Fake Ihrer getesteten Klassenabhängigkeiten.

Kritner
quelle
1
Perfekte Antwort. Ich würde sogar so weit gehen, in Ihrem Unit-Test überhaupt keinen DI-Container zu verwenden. Natürlich mit Ausnahme von Unit-Tests, die darauf abzielen, die Richtigkeit der DI-Konfiguration zu testen, wie zum Beispiel die Reihenfolge der angewendeten Dekorateure
Ric .Net
34
Ich bin mir nicht sicher, wie hilfreich dies ist, wenn Sie Klassen für Klassen haben, für die alle eine Reihe von Abhängigkeiten eingefügt werden müssen. Ich möchte in der Lage sein, Standardimplementierungen (oder Verspottungen mit Standardverhalten) zu registrieren, damit ich diese Objektdiagramme instanziieren kann, ohne zuerst 30 Abhängigkeiten einrichten zu müssen, sondern diejenigen neu konfigurieren muss, die ich für den Test benötige.
Sinaesthetic
1
@Sinaesthetic dafür sind Testing and Mocking Frameworks gedacht. Mit nUnit können Sie einmalige oder Test-pro-Test-Methoden erstellen, mit denen Sie alles verspotten können. In Ihren Tests müssen Sie sich dann nur mit der Konfiguration der zu testenden Methode befassen. Wenn Sie DI für einen Test verwenden, bedeutet dies, dass es sich nicht mehr um einen Unit-Test handelt, sondern um einen Integrationstest mit Microsoft DI (oder 3rd Partys) DI.
Erik Philips
1
"DI für einen Test zu verwenden bedeutet, dass es kein Unit-Test mehr ist" kann Ihnen dort nicht wirklich zustimmen, zumindest nicht zum Nennwert. Oft ist die DI nötig, einfach die Klasse zu initialisieren , damit das Gerät kann getestet werden. Der Punkt ist, dass die Abhängigkeiten verspottet werden, damit Sie das Verhalten der Einheit in Bezug auf die Abhängigkeit testen können. Ich denke, Sie beziehen sich möglicherweise auf ein Szenario, in dem eine voll funktionsfähige Abhängigkeit eingefügt wird. Dann handelt es sich möglicherweise um einen Integrationstest, es sei denn, die Abhängigkeiten dieses Objekts werden ebenfalls verspottet. Es gibt viele einmalige Szenarien, die diskutiert werden könnten.
Sinaesthetic
In unseren Unit-Tests verwenden wir häufig die Abhängigkeitsinjektion. Wir verwenden sie ausgiebig für Spottzwecke. Ich bin mir nicht sicher, warum Sie DI in Ihren Tests überhaupt nicht verwenden möchten. Wir entwickeln unsere Infrastruktur nicht für Tests per Software, sondern verwenden DI, um das Verspotten und Injizieren von Objekten, die ein Test benötigen würde, wirklich einfach zu machen. Und wir können ServiceCollectionvon Anfang an genau einstellen, welche Objekte in unserem verfügbar sind . Es ist besonders hilfreich für Gerüste und auch für Integrationstests. Ja, ich würde DI in Ihren Tests verwenden.
Paul Carlton
123

Obwohl die Antwort von @ Kritner richtig ist, bevorzuge ich Folgendes für die Codeintegrität und eine bessere DI-Erfahrung:

[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>();
    }
}
Madjack
quelle
Wie haben Sie die generische GetService <> -Methode erhalten?
Chazt3n
Entschuldigung, ich konnte nicht verstehen, was du genau meinst. Bitte geben Sie weitere Details an. @ Chazt3n
Madjack
6
GetService<>hat einige Überladungen, die mit gefunden werden könnenusing Microsoft.Extensions.DependencyInjection
Neville Nazerane
10
Ich habe das gerade getestet. Dies ist eine weitaus gültigere Antwort als die markierte Antwort. Dies verwendet DI. Ich habe versucht, dies über dieselbe Erweiterungsfunktion zu verwenden, die ich für die Website verwende. Diese Funktion funktioniert perfekt
Neville Nazerane
9
Dies ist kein Unit-Test des Dienstes, sondern ein Integrationstest mit Microsoft DI. Microsoft hat bereits Komponententests zum Testen von DI, daher gibt es keinen Grund, dies zu tun. Wenn Sie dies testen möchten und das Objekt registriert ist, ist dies eine Trennung von Bedenken und sollte in einem eigenen Test erfolgen. Unit-Testing und Objekt bedeutet, das Objekt selbst ohne externe Abhängigkeiten zu testen.
Erik Philips
37

Auf einfache Weise habe ich eine generische Hilfsklasse für Abhängigkeitsauflöser geschrieben und dann den IWebHost in meiner Unit-Test-Klasse erstellt.

Generic Dependency Resolver

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

Unit Test Projekt

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


    }
Joshua Duxbury
quelle
Ein schönes Beispiel, wie man Autofac durch Microsoft.Extensions.DependencyInjection ersetzt
lnaie
Ja, dies sollte die Standardantwort sein. Wir haben eine ganze Suite eingerichtet, die alle zu injizierenden Dienste testet, und es funktioniert wie ein Zauber. Vielen Dank!
Facundofarias
hi @Joshua Duxbury, kannst du helfen, diese Frage zu beantworten? stackoverflow.com/questions/57331395/… Beim Versuch, Ihre Lösung zu implementieren, haben Sie gerade 100 Punkte gesendet und sich auch Ihre anderen Antworten angesehen. Danke!
3
Die Entsorgung des Bereichs scheint mir falsch zu sein - docs.microsoft.com/en-us/dotnet/api/… - Once Dispose is called, any scoped services that have been resolved from ServiceProvider will be disposed..
Eugene Podskal
1
Möglicherweise müssen Sie die Anweisung "using" entfernen, um "Fehler bei entsorgten Objekten" in Ihren DB-Kontexten zu vermeiden
Mosta
2

Wenn Sie die Program.cs+ Startup.cs-Konvention verwenden und diese schnell zum Laufen bringen möchten, können Sie Ihren vorhandenen Host-Builder mit einem Einzeiler wiederverwenden:

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

Beispieldatei Program.csaus einem Webprojekt:

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>();
                });
    }
}
Matthew Steven Monkan
quelle
0

Warum sollten Sie diese in eine Testklasse injizieren wollen? Normalerweise testen Sie den MatchController beispielsweise mit einem Tool wie RhinoMocks , um Stubs oder Mocks zu erstellen. Hier ist ein Beispiel mit diesem und MSTest, aus dem Sie extrapolieren können:

[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);
    }
Alexandru Marculescu
quelle
Das Paket RhinoMocks 3.6.1 ist nicht mit netcoreapp1.0 (.NETCoreApp, Version = v1.0) kompatibel. Paket RhinoMocks 3.6.1 unterstützt: net (.NETFramework, Version = v0.0)
Ganjan
Andere Frameworks nehmen dieses Set langsam auf.
Alexandru Marculescu