it-swarm.com.de

ASP.Net Core 2.0 SignInAsync gibt eine Ausnahme zurück. Der Wert darf nicht null sein

Ich habe eine ASP.Net Core 2.0-Webanwendung, die ich mit Komponententests (mit NUnit) nachrüste. Die Anwendung funktioniert einwandfrei, und die meisten Tests funktionieren bisher einwandfrei.

Das Testen der Authentifizierung/Autorisierung (wird ein Benutzer angemeldet und kann auf [Authorize] gefilterte Aktionen zugreifen) schlägt jedoch fehl mit ...

System.ArgumentNullException: Value cannot be null.
Parameter name: provider

...nach dem...

await HttpContext.SignInAsync(principal);

... aber es ist nicht klar, was tatsächlich die zugrunde liegende Ursache ist. Hier wird die Codeausführung in der aufgerufenen Methode gestoppt, und in IDE wird keine Ausnahme angezeigt, aber die Codeausführung kehrt zum Aufrufer zurück und wird dann beendet (dennoch wird im Ausgabefenster von VS immer noch The program '[13704] dotnet.exe' has exited with code 0 (0x0). angezeigt).

Der Test Explorer wird rot angezeigt und gibt die Ausnahme an, auf die verwiesen wird (ansonsten hätte ich keine Ahnung, was das Problem ist.)

Ich arbeite daran, einen Repro zu erstellen, auf den die Leute hinweisen sollen (es hat sich herausgestellt, dass dies bisher ein wenig zu tun hatte.)

Weiß jemand, wie man die zugrunde liegende Ursache lokalisiert? Handelt es sich um ein DI-Problem (etwas, das im Test nicht bereitgestellt wird, aber in der normalen Ausführung ist)?

UPDATE1: Angeforderten Authentifizierungscode bereitstellen ...

public async Task<IActionResult> Registration(RegistrationViewModel vm) {
    if (ModelState.IsValid) {
        // Create registration for user
        var regData = createRegistrationData(vm);
        _repository.AddUserRegistrationWithGroup(regData);

        var claims = new List<Claim> {
            new Claim(ClaimTypes.NameIdentifier, regData.UserId.ToString())
        };
        var ident = new ClaimsIdentity(claims);
        var principal = new ClaimsPrincipal(ident);

        await HttpContext.SignInAsync(principal); // FAILS HERE

        return RedirectToAction("Welcome", "App");
    } else {
        ModelState.AddModelError("", "Invalid registration information.");
    }

    return View();
}

Der Testcode, der fehlschlägt ...

public async Task TestRegistration()
{
    var ctx = Utils.GetInMemContext();
    Utils.LoadJsonData(ctx);
    var repo = new Repository(ctx);
    var auth = new AuthController(repo);
    auth.ControllerContext = new ControllerContext();
    auth.ControllerContext.HttpContext = new DefaultHttpContext();

    var vm = new RegistrationViewModel()
    {
        OrgName = "Dev Org",
        BirthdayDay = 1,
        BirthdayMonth = "January",
        BirthdayYear = 1979 
    };

    var orig = ctx.Registrations.Count();
    var result = await auth.Registration(vm); // STEPS IN, THEN FAILS
    var cnt = ctx.Registrations.Count();
    var view = result as ViewResult;

    Assert.AreEqual(0, orig);
    Assert.AreEqual(1, cnt);
    Assert.IsNotNull(result);
    Assert.IsNotNull(view);
    Assert.IsNotNull(view.Model);
    Assert.IsTrue(string.IsNullOrEmpty(view.ViewName) || view.ViewName == "Welcome");
}

UPDATE3: Basierend auf chat @nkosi vorgeschlagen , dass dies ein Problem ist, weil ich die Anforderungen der Abhängigkeitsinjektion für HttpContext nicht erfüllte.

Jedoch , was noch nicht klar ist: Wenn es tatsächlich darum geht, nicht die richtige Dienstabhängigkeit bereitzustellen, warum funktioniert der Code dann normal (wenn er nicht getestet wird)? Der SUT (Controller) akzeptiert nur einen IRepository-Parameter (das ist also in jedem Fall alles, was bereitgestellt wird.) Warum sollte ein überladener ctor (oder ein Mock) nur zu Testzwecken erstellt werden, wenn der vorhandene ctor alles ist, was beim Ausführen des Programms und aufgerufen wird? läuft es ohne probleme?

UPDATE4 : Während @Nkosi den Fehler/das Problem mit einer Lösung beantwortete, frage ich mich immer noch, warum die IDE die zugrunde liegende Ausnahme nicht genau/konsistent darstellt. Ist dies ein Fehler oder liegt dies an den asynchronen/erwarteten Operatoren und dem NUnit-Testadapter/-Läufer? Warum "poppen" Ausnahmen nicht so, wie ich es beim Debuggen des Tests erwartet hätte, und der Exit-Code ist immer noch Null (was normalerweise auf einen erfolgreichen Rückgabestatus hinweist)?

8
t.j.

Was noch nicht klar ist: Wenn es tatsächlich darum geht, nicht die richtige Dienstabhängigkeit bereitzustellen, warum funktioniert der Code dann normal (wenn er nicht getestet wird)? Der SUT (Controller) akzeptiert nur einen IRepository-Parameter (das ist in jedem Fall alles, was bereitgestellt wird). Warum sollte ein überladener ctor (oder Mock) nur zu Testzwecken erstellt werden, wenn der vorhandene ctor alles ist, was beim Ausführen des Programms und aufgerufen wird? läuft es ohne probleme?

Sie verwechseln hier einige Dinge: Zunächst müssen Sie keine separaten Konstruktoren erstellen. Nicht zum Testen und nicht zum Ausführen als Teil Ihrer Anwendung.

Sie sollten alle direkten Abhängigkeiten definieren, die Ihr Controller als Parameter für den Konstruktor hat, damit der Abhängigkeitsinjektionscontainer diese Abhängigkeiten dem Controller bereitstellt, wenn dieser als Teil der Anwendung ausgeführt wird.

Das ist aber auch das Wichtige: Wenn Sie Ihre Anwendung ausführen, gibt es einen Abhängigkeitsinjektionscontainer, der für das Erstellen von Objekten und das Bereitstellen der erforderlichen Abhängigkeiten verantwortlich ist. Sie brauchen sich also nicht zu viele Gedanken darüber zu machen, woher sie kommen. Dies ist jedoch beim Testen von Einheiten anders. In Komponententests möchten wir keine Abhängigkeitsinjektion verwenden, da dadurch lediglich Abhängigkeiten und mögliche Nebenwirkungen ausgeblendet werden, die mit unserem Test in Konflikt geraten können. Die Abhängigkeitsinjektion innerhalb eines Unit-Tests ist ein sehr gutes Zeichen dafür, dass Sie nicht unit testing, sondern integration test ausführen (zumindest, wenn Sie tatsächlich einen DI-Container testen).

Stattdessen möchten wir in Komponententests alle Objekte explizit erstellen und alle Abhängigkeiten explizit bereitstellen. Dies bedeutet, dass wir den Controller neu einrichten und alle Abhängigkeiten des Controllers übergeben. Im Idealfall verwenden wir Mocks, damit wir in unserem Komponententest nicht auf externes Verhalten angewiesen sind.

Das ist die meiste Zeit ziemlich einfach. Leider haben Controller etwas Besonderes: Controller verfügen über die Eigenschaft ControllerContext, die während des MVC-Lebenszyklus automatisch bereitgestellt wird. Einige andere Komponenten in MVC verfügen über ähnliche Funktionen (z. B. wird auch ViewContext automatisch bereitgestellt). Diese Eigenschaften werden nicht vom Konstruktor injiziert, sodass die Abhängigkeit nicht explizit sichtbar ist. Je nachdem, was der Controller tut, müssen Sie möglicherweise auch diese Eigenschaften festlegen, wenn Sie den Controller als Einheit testen.


Wenn Sie zu Ihrem Unit-Test kommen, verwenden Sie HttpContext.SignInAsync(principal) in Ihrer Controller-Aktion, sodass Sie leider direkt mit dem HttpContext arbeiten.

SignInAsync ist eine Erweiterungsmethode, die im Grunde Folgendes tut :

context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

Bei dieser Methode wird der Einfachheit halber Dienstlokalisierungsmuster verwendet, um einen Dienst aus dem Abhängigkeitsinjektionscontainer abzurufen und die Anmeldung durchzuführen. Nur dieser eine Methodenaufruf für HttpContext zieht also weitere implizite Abhängigkeiten heran, die Sie erst feststellen, wenn Ihr Test fehlschlägt. Dies sollte als gutes Beispiel dienen für warum Sie das Service-Locator-Muster vermeiden sollten : Explizite Abhängigkeiten im Konstruktor sind viel leichter zu handhaben. - Aber hier ist dies eine bequeme Methode, also müssen wir damit leben und den Test einfach anpassen, um damit zu arbeiten.

Bevor ich fortfahre, möchte ich hier eine gute alternative Lösung erwähnen: Da der Controller ein AuthController ist, kann ich mir nur vorstellen, dass einer seiner Hauptzwecke darin besteht, Authentifizierungsaufgaben zu erledigen, Benutzer an- und abzumelden und andere Dinge. Daher ist es möglicherweise eine gute Idee, HttpContext.SignInAsync nicht zu verwenden, sondern stattdessen IAuthenticationService als explizite Abhängigkeit auf dem Controller zu verwenden und die Methoden direkt aufzurufen. Auf diese Weise haben Sie eine eindeutige Abhängigkeit, die Sie in Ihren Tests erfüllen können, und müssen sich nicht auf den Service Locator einlassen.

Dies wäre natürlich ein Sonderfall für diesen Controller und würde nicht für jeden möglichen Aufruf der Erweiterungsmethoden auf dem HttpContext funktionieren. Lassen Sie uns untersuchen, wie wir dies richtig testen können:

Wie wir aus dem Code sehen können, was SignInAsync tatsächlich tut, müssen wir ein IServiceProvider für HttpContext.RequestServices bereitstellen und dieses in der Lage sein, ein IAuthenticationService zurückzugeben. Wir werden uns also über Folgendes lustig machen:

var authenticationServiceMock = new Mock<IAuthenticationService>();
authenticationServiceMock
    .Setup(a => a.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.CompletedTask);

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IAuthenticationService)))
    .Returns(authenticationServiceMock.Object);

Dann können wir diesen Dienstanbieter in ControllerContext übergeben, nachdem wir den Controller erstellt haben:

var controller = new AuthController();
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

Das ist alles, was wir tun müssen, damit HttpContext.SignInAsync funktioniert.

Leider steckt noch ein bisschen mehr dahinter. Wie ich in dieser anderen Antwort (die Sie bereits gefunden haben) erklärt habe, führt die Rückgabe eines RedirectToActionResult von einem Controller zu Problemen, wenn Sie den RequestServices in einem Komponententest eingerichtet haben. Da RequestServices nicht null ist, versucht die Implementierung von RedirectToAction, ein IUrlHelperFactory aufzulösen, und dieses Ergebnis muss nicht null sein. Aus diesem Grund müssen wir unsere Mocks ein wenig erweitern, um auch diese bereitzustellen:

var urlHelperFactory = new Mock<IUrlHelperFactory>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IUrlHelperFactory)))
    .Returns(urlHelperFactory.Object);

Zum Glück brauchen wir nichts weiter zu tun, und wir müssen dem Factory-Mock auch keine Logik hinzufügen. Es ist genug, wenn es nur da ist.

Damit können wir die Controller-Aktion richtig testen:

// mock setup, as above
// …

// arrange
var controller = new AuthController(repositoryMock.Object);
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

var registrationVm = new RegistrationViewModel();

// act
var result = await controller.Registration(registrationVm);

// assert
var redirectResult = result as RedirectToActionResult;
Assert.NotNull(redirectResult);
Assert.Equal("Welcome", redirectResult.ActionName);

Ich frage mich immer noch, warum das IDE die zugrunde liegende Ausnahme nicht genau/konsistent darstellt. Ist dies ein Fehler oder liegt dies an den asynchronen/erwarteten Operatoren und dem NUnit-Testadapter/-Läufer?

Ich habe in der Vergangenheit auch bei meinen asynchronen Tests etwas Ähnliches gesehen, nämlich, dass ich sie nicht richtig debuggen konnte oder dass Ausnahmen nicht richtig angezeigt würden. Ich kann mich nicht erinnern, dies in den letzten Versionen von Visual Studio und xUnit gesehen zu haben (ich persönlich verwende xUnit, nicht NUnit). Wenn dies hilft, funktioniert das Ausführen der Tests über die Befehlszeile mit dotnet test normalerweise ordnungsgemäß und Sie erhalten korrekte (asynchrone) Stack-Traces für Fehler.

9
poke

Handelt es sich um ein DI-Problem (etwas, das im Test nicht bereitgestellt wird, das aber normal ausgeführt wird)?

JA

Sie rufen Funktionen auf, die das Framework zur Laufzeit für Sie einrichten würde. Bei isolierten Komponententests müssen Sie diese selbst einrichten.

Im HttpContext des Controllers fehlt eine IServiceProvider, mit der IAuthenticationService aufgelöst wird. Dieser Service nennt sich eigentlich SignInAsync

Um zu lassen ....

await HttpContext.SignInAsync(principal);  // FAILS HERE

... in der Aktion Registration, die während des Komponententests vollständig ausgeführt werden soll, müssen Sie einen Dienstanbieter verspotten, damit die Erweiterungsmethode SignInAsync nicht fehlschlägt.

Aktualisieren Sie die Komponententestanordnung

//...code removed for brevity

auth.ControllerContext.HttpContext = new DefaultHttpContext() {
    RequestServices = createServiceProviderMock()
};

//...code removed for brevity

Wobei createServiceProviderMock() eine kleine Methode ist, die verwendet wird, um einen Dienstanbieter zu verspotten, der zum Auffüllen des HttpContext.RequestServices verwendet wird.

public IServiceProvider createServiceProviderMock() {
    var authServiceMock = new Mock<IAuthenticationService>();
    authServiceMock
        .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
        .Returns(Task.FromResult((object)null)); //<-- to allow async call to continue

    var serviceProviderMock = new Mock<IServiceProvider>();
    serviceProviderMock
        .Setup(_ => _.GetService(typeof(IAuthenticationService)))
        .Returns(authServiceMock.Object);

    return serviceProviderMock.Object;
}

Ich würde auch vorschlagen, die Repository für die Zwecke eines isolierten Komponententests dieser Controller-Aktion zu verspotten, um sicherzustellen, dass sie ohne negative Auswirkungen vollständig ausgeführt wird

2
Nkosi

wie @poke bereits erwähnt hat, sollten Sie Dependency Injection in Komponententests nicht verwenden und die Abhängigkeiten nicht explizit bereitstellen (unter Verwendung von Mocking). Ich hatte jedoch dieses Problem bei meinen Integrationstests und stellte fest, dass das Problem auf die Eigenschaft RequestServices von HttpContext zurückzuführen ist, die in nicht ordnungsgemäß initialisiert ist Tests (da wir in Tests keinen tatsächlichen HttpContext verwenden), habe ich mein HttpContextAccessor wie folgt registriert und den gesamten erforderlichen Service selbst (manuell) bestanden und das Problem behoben. siehe Code unten

Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });

Ich bin damit einverstanden, dass dies keine sehr saubere Lösung ist. Beachten Sie jedoch, dass ich diesen Code nur in meinen Tests geschrieben und verwendet habe, um die erforderlichen HttContext-Abhängigkeiten (die in der Testmethode nicht automatisch angegeben wurden) in Ihrer Anwendung IHttpContextAccessor, HttpContext und den erforderlichen Services bereitzustellen automatisch vom Framework bereitgestellt.

hier finden Sie alle meine Methoden zur Registrierung von Abhängigkeiten in meinem Test-Basisklassenkonstruktor

 public class MyTestBaseClass
 {
  protected ServiceCollection Services { get; set; } = new ServiceCollection();
  MyTestBaseClass
 {

   Services.AddDigiTebFrameworkServices();
        Services.AddDigiTebDBContextService<DigiTebDBContext> 
        (Consts.MainDBConnectionName);
        Services.AddDigiTebIdentityService<User, Role, DigiTebDBContext>();
        Services.AddDigiTebAuthServices();
        Services.AddDigiTebCoreServices();
        Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });
}
}
1
Code_Worm