Verspottungserweiterungsmethoden mit Moq

172

Ich habe eine bereits vorhandene Schnittstelle ...

public interface ISomeInterface
{
    void SomeMethod();
}

und ich habe dieses intreface mit einem mixin erweitert ...

public static class SomeInterfaceExtensions
{
    public static void AnotherMethod(this ISomeInterface someInterface)
    {
        // Implementation here
    }
}

Ich habe eine Klasse, die das nennt und die ich testen möchte ...

public class Caller
{
    private readonly ISomeInterface someInterface;

    public Caller(ISomeInterface someInterface)
    {
        this.someInterface = someInterface;
    }

    public void Main()
    {
        someInterface.AnotherMethod();
    }
}

und ein Test, bei dem ich die Schnittstelle verspotten und den Aufruf der Erweiterungsmethode überprüfen möchte ...

    [Test]
    public void Main_BasicCall_CallsAnotherMethod()
    {
        // Arrange
        var someInterfaceMock = new Mock<ISomeInterface>();
        someInterfaceMock.Setup(x => x.AnotherMethod()).Verifiable();

        var caller = new Caller(someInterfaceMock.Object);

        // Act
        caller.Main();

        // Assert
        someInterfaceMock.Verify();
    }

Das Ausführen dieses Tests erzeugt jedoch eine Ausnahme ...

System.ArgumentException: Invalid setup on a non-member method:
x => x.AnotherMethod()

Meine Frage ist, gibt es eine gute Möglichkeit, den Mixin-Aufruf zu verspotten?

Russell Giddings
quelle
3
Nach meiner Erfahrung sind die Begriffe Mixin und Erweiterungsmethoden getrennte Dinge. Letzteres würde ich in diesem Fall verwenden, um Verwechslungen zu vermeiden: P
Ruben Bartelink
3
Duplikat von: stackoverflow.com/questions/562129/… .
Oliver

Antworten:

33

Sie können die statische Methode (daher die Erweiterungsmethode) mit dem Mocking-Framework nicht "direkt" verspotten. Sie können Moles ( http://research.microsoft.com/en-us/projects/pex/downloads.aspx ) ausprobieren , ein kostenloses Tool von Microsoft, das einen anderen Ansatz implementiert. Hier ist die Beschreibung des Tools:

Moles ist ein leichtes Framework für Teststubs und Umwege in .NET, das auf Delegaten basiert.

Maulwürfe können verwendet werden, um jede .NET-Methode zu umgehen, einschließlich nicht virtueller / statischer Methoden in versiegelten Typen.

Sie können Moles mit jedem Testframework verwenden (davon ist es unabhängig).

Daniele Armanasco
quelle
2
Neben Moles gibt es noch andere (nicht freie) Mocking-Frameworks, die die .NET-Profiler-API zum Verspotten von Objekten verwenden und so alle Aufrufe ersetzen können. Die beiden, die ich kenne, sind JustMock und TypeMock Isolator von Telerik .
Marcel Gosselin
6
Maulwürfe sind theoretisch gut, aber ich habe beim Testen drei Probleme festgestellt, die mich davon abgehalten haben, sie zu verwenden ... 1) Sie laufen nicht im Resharper NUnit-Läufer 2) Sie müssen manuell eine Maulwurfsbaugruppe für jede Stichbaugruppe erstellen 3 ) Sie müssen eine Maulwurfsbaugruppe manuell neu erstellen, wenn sich eine Stub-Methode ändert.
Russell Giddings
26

Ich habe einen Wrapper verwendet, um dieses Problem zu umgehen. Erstellen Sie ein Wrapper-Objekt und übergeben Sie Ihre verspottete Methode.

Siehe Verspotten statischer Methoden für Unit-Tests von Paul Irwin, es gibt schöne Beispiele.

Alvis
quelle
12
Ich mag diese Antwort, weil es heißt (ohne es direkt zu sagen), dass Sie Ihren Code ändern müssen, um ihn testbar zu machen. So funktioniert es einfach. Verstehen Sie, dass diese Chips im Mikrochip- / IC / ASIC-Design nicht nur für die Funktionsweise ausgelegt sein müssen, sondern auch noch weiter, um testbar zu sein. Wenn Sie einen Mikrochip nicht testen können, ist er nutzlos - Sie können dies nicht garantieren Arbeit. Gleiches gilt für Software. Wenn Sie es nicht so gebaut haben, dass es testbar ist, ist es ... nutzlos. Erstellen Sie es so, dass es testbar ist, was in einigen Fällen bedeutet, dass Sie den Code neu schreiben (und Wrapper verwenden), und erstellen Sie dann die automatisierten Tests, die ihn testen.
Michael Plautz
2
Ich habe eine kleine Bibliothek erstellt, die Dapper, Dapper.Contrib und IDbConnection umschließt. github.com/codeapologist/DataAbstractions.Dapper
Drew Sumido
15

Ich stellte fest, dass ich das Innere der Erweiterungsmethode entdecken musste, für die ich die Eingabe verspotten wollte, und verspotten musste, was in der Erweiterung vor sich ging.

Ich habe die Verwendung einer Erweiterung als direktes Hinzufügen von Code zu Ihrer Methode angesehen. Dies bedeutete, dass ich mich über das lustig machen musste, was in der Erweiterung passiert, und nicht über die Erweiterung selbst.

chris31389
quelle
11

Sie können eine Testschnittstelle verspotten, die von der realen erbt und ein Mitglied mit derselben Signatur wie die Erweiterungsmethode hat.

Sie können dann die Testschnittstelle verspotten, die echte zum Verspotten hinzufügen und die Testmethode im Setup aufrufen.

Ihre Implementierung des Mocks kann dann jede gewünschte Methode aufrufen oder einfach überprüfen, ob die Methode aufgerufen wird:

IReal //on which some extension method is defined
{
    ... SomeRegularMethod(...);
}

static ExtensionsForIReal
{
    static ... SomeExtensionMethod(this IReal iReal,...);
}

ITest: IReal
{
    //This is a regular method with same name and signature as the extension without the "this IReal iReal" parameter
    ... SomeExtensionMethod(...);
}

var someMock = new Mock<ITest>();
Mock.As<IReal>(); //ad IReal to the mock
someMock.Setup(x => x.SomeExtensionMethod(...)).Verifiable(); //Calls SomeExtensionMethod on ITest
someMock.As<IReal>().Setup(x => x.SomeRegularMethod(...)).Verifiable(); //Calls SomeRegularMethod on IReal

Vielen Dank an die Håvard S-Lösung in diesem Beitrag für die Implementierung eines Modells, das zwei Schnittstellen unterstützt. Sobald ich es gefunden hatte, war es ein Kinderspiel, es mit der Testschnittstelle und der statischen Methode anzupassen.

pasx
quelle
Können Sie uns bitte die gleiche Mustersignatur für SomeNotAnExtensionMethodund zeigen SomeNotAnExtensionMethod? Ich habe jetzt eine Idee, wie man eine Signatur für eine Erweiterungsmethode innerhalb einer Schnittstelle erstellt ...
Peter Csala
1
@ Peter Csala: Entschuldigung, wenn dies unklar ist. Ich habe den Beitrag aktualisiert, um ihn klarer zu gestalten, und SomeNotAnExtensionMethod in SomeRegularMethod umbenannt.
Pasx
Wie übergeben Sie iRealParameter SomeExtensionMethodim Fall von ITest? Wenn Sie es als ersten Parameter übergeben, wie richten Sie es dann ein? Setup( x=> x.SomeExtensionMethod(x, ...)Dies führt zu einer Laufzeitausnahme.
Peter Csala
Sie bestehen es nicht. Vom Standpunkt des Mocks aus enthält ITest eine reguläre Methode ohne diesen Parameter, um die Signatur von Aufrufen mit der Erweiterungsmethode in Ihrem Code abzugleichen, sodass Sie hier niemals ein IReal erhalten, und in jedem Fall ist die Implementierung von IReal / ITest das Mock selbst . Wenn Sie in SomeExtensionMethod auf einige Eigenschaften des verspotteten IReal zugreifen möchten, sollten Sie dies alles im Mock tun, z. B.: object _mockCache = whatever... `Setup (x => x.SomeExtensionMethod (...) .. Callback (() => Sie können Zugriff auf _mockCache hier);)
pasx
8

Mit JustMock können Sie eine Erweiterungsmethode leicht verspotten . Die API entspricht der Verspottung einer normalen Methode. Folgendes berücksichtigen

public static string Echo(this Foo foo, string strValue) 
{ 
    return strValue; 
}

Verwenden Sie Folgendes, um diese Methode zu arrangieren und zu überprüfen:

string expected = "World";

var foo = new Foo();
Mock.Arrange(() => foo.Echo(Arg.IsAny<string>())).Returns(expected);

string result = foo.Echo("Hello");

Assert.AreEqual(expected, result);

Hier ist auch ein Link zur Dokumentation: Extension Methods Mocking

Mihail Vladov
quelle
2

Ich verwende gerne den Wrapper (Adaptermuster), wenn ich das Objekt selbst umhülle. Ich bin mir nicht sicher, ob ich das zum Umschließen einer Erweiterungsmethode verwenden würde, die nicht Teil des Objekts ist.

Ich verwende eine interne Lazy Injectable-Eigenschaft vom Typ Action, Func, Predicate oder delegate und erlaube das Injizieren (Austauschen) der Methode während eines Unit-Tests.

    internal Func<IMyObject, string, object> DoWorkMethod
    {
        [ExcludeFromCodeCoverage]
        get { return _DoWorkMethod ?? (_DoWorkMethod = (obj, val) => { return obj.DoWork(val); }); }
        set { _DoWorkMethod = value; }
    } private Func<IMyObject, string, object> _DoWorkMethod;

Dann rufen Sie die Func anstelle der eigentlichen Methode auf.

    public object SomeFunction()
    {
        var val = "doesn't matter for this example";
        return DoWorkMethod.Invoke(MyObjectProperty, val);
    }

Ein vollständigeres Beispiel finden Sie unter http://www.rhyous.com/2016/08/11/unit-testing-calls-to-complex-extension-methods/

Rhyous
quelle
Das ist gut, aber die Leser sollten sich bewusst sein, dass _DoWorkMethod ein neues Feld der Klasse ist, dem jede Instanz der Klasse jetzt ein weiteres Feld zuweisen muss. Es ist selten, dass dies wichtig ist, aber manchmal hängt es von der Anzahl der Instanzen ab, die Sie gleichzeitig zuweisen. Sie können dieses Problem umgehen, indem Sie _DoWorkMethod statisch machen. Der Nachteil dabei ist, dass bei gleichzeitiger Ausführung von Komponententests zwei verschiedene Komponententests abgeschlossen werden, die möglicherweise denselben statischen Wert ändern.
zumalifeguard
-1

Wenn Sie also Moq verwenden und das Ergebnis einer Erweiterungsmethode verspotten möchten, können Sie es SetupReturnsDefault<ReturnTypeOfExtensionMethod>(new ConcreteInstanceToReturn())für die Instanz der Scheinklasse verwenden, die die Erweiterungsmethode enthält, die Sie verspotten möchten .

Es ist nicht perfekt, aber für Unit-Testzwecke funktioniert es gut.

ckernel
quelle
Ich glaube, es ist SetReturnsDefault <T> ()
David
das gibt niemals die konkrete Instanz zurück. Nur null im Fall einer benutzerdefinierten c # -Klasse!
HelloWorld