Moq – Verwendung von Callback() zum Erfassen von Parametern, die an verspottete Methoden übergeben werden

Moq – Verwendung von Callback() zum Erfassen von Parametern, die an verspottete Methoden übergeben werden

Wenn Sie Moq verwenden, um eine verspottete Methode einzurichten, können Sie Callback() verwenden, um die an die verspottete Methode übergebenen Parameter zu erfassen:

string capturedJson; 

mockRepo.Setup(t => t.Save(It.IsAny<string>()))
	.Callback((string json) =>
	{
		Console.WriteLine("Repository.Save(json) called. Captured json parameter");
		capturedJson = json;
	});

//assert against the captured JSON later
Code language: C# (cs)

Es gibt zwei Hauptanwendungsfälle für das Erfassen von Parametern in einem Test:

  • Methodenaufrufe zur Fehlerbehebung protokollieren.
  • Vereinfachung von Behauptungen mit komplexen Parametern.

In diesem Artikel zeige ich Beispiele für die Verwendung von Callback() in diesen beiden Szenarien und erläutere dann einige Probleme, auf die Sie achten sollten, wenn Sie versuchen, ein Callback()-Lambda einzurichten.

Beispiel – Verwenden Sie Callback(), um Methodenaufrufe zur Fehlerbehebung zu protokollieren

Sie können Callback() verwenden, um Methodenaufrufe und ihre Parameter zu protokollieren, was bei der Fehlerbehebung hilfreich sein kann.

Angenommen, Sie haben einen fehlgeschlagenen Komponententest und können nicht herausfinden, warum er fehlschlägt. Sie fügen also ein Callback() ein, um die Anrufe zu protokollieren.

//arrange
var mockRepo = new Mock<IMessageRepository>();
var messageService = new MessageService(mockRepo.Object);

mockRepo.Setup(t => t.Get(10))
	.Returns(() => "{\"Id\":10, \"Text\":\"Test\"}")
	.Callback((int id) =>
	{
		//Log call for troubleshooting
		Console.WriteLine($"Repo.Get({id}) called");
	});

//act
var message = messageService.ProcessMessage(100);

//assert
Assert.IsNotNull(message);
Code language: C# (cs)

Dies protokolliert nichts, was Ihnen sagt, dass die verspottete Methode überhaupt nicht aufgerufen wird. Sie können sehen, dass ProcessMessage(id) Repository.Get(id) aufruft.

Erkennen Sie das Problem im Test? Die verspottete Methode ist für Get(10) eingerichtet, während Sie ProcessMessage(100) aufrufen, weshalb die verspottete Methode den Aufruf überhaupt nicht abfängt (und daher das Callback()-Lambda nicht aufruft). Das ist nur ein Tippfehler.

Nach Behebung des Problems wird der Test bestanden und Folgendes ausgegeben:

Repo.Get(10) calledCode language: plaintext (plaintext)

Sie können auch mit der parameterlosen Callback()-Überladung protokollieren

Sie müssen die Parameter nicht an das Callback()-Lambda übergeben. Sie können die parameterlose Überladung von Callback() verwenden, wenn Sie möchten:

mockRepo.Setup(t => t.Get(10))
	.Returns(() => "{\"Id\":10, \"Text\":\"Test\"}")
	.Callback(() =>
	{
		Console.WriteLine($"Repo.Get() called");
	});
Code language: C# (cs)

Dies ist eine einfachere Option als das Übergeben der Parameter und vermeidet Fehler, auf die Sie stoßen können, wenn Sie versuchen, das Callback()-Lambda korrekt einzurichten.

Beispiel – Verwenden Sie Callback(), um Behauptungen mit den erfassten Parametern zu vereinfachen

Wenn Sie in komplexen Szenarien gegen Parameter, die an mockierte Methoden übergeben werden, Assertion durchführen müssen, können Sie Callback() verwenden, um die Parameter zu erfassen, und dann direkt gegen die Parameter validieren.

Hier ist ein Beispiel. Dadurch wird eine JSON-Zeichenfolge erfasst, deserialisiert und gegen das deserialisierte Objekt bestätigt:

//arrange
var mockRepo = new Mock<IMessageRepository>();
var messageService = new MessageService(mockRepo.Object);

Message capturedMessage = null;
mockRepo.Setup(t => t.Save(It.IsAny<string>()))
	.Callback((string json) =>
	{
		//Capture parameter for assertion later
		capturedMessage = JsonSerializer.Deserialize<Message>(json);
	});

//act
messageService.Send(new Message() { SendAt = DateTimeOffset.Now.AddMinutes(1) });

//Assert against captured parameter
Assert.IsTrue(capturedMessage.SendAt > DateTimeOffset.Now);
Code language: C# (cs)

In sehr einfachen Szenarien können Sie beim Ansatz Verify() + It.Is() bleiben. Aber für alles, was nicht trivial ist, kann die Verwendung dieses Callback()-Ansatzes die Dinge erheblich vereinfachen. Im Folgenden erkläre ich, warum dies die Dinge vereinfacht.

Warum das Erfassen der Parameter Behauptungen vereinfacht

Um zu sehen, warum das Erfassen der Parameter die Behauptungen vereinfacht, werfen wir einen Blick auf einen alternativen Ansatz mit Verify() + It.Is().

Genau wie im obigen Beispiel wird dadurch behauptet, dass der an Repository.Save(json) übergebene JSON-Parameter ein Datum in der Zukunft hat. Wir müssen Verify() und It.Is() zusammen verwenden, um zu versuchen, den übergebenen Parameter zu untersuchen:

mockRepo.Verify(t => t.Save(It.Is<string>(json =>
{
	var message = JsonSerializer.Deserialize<Message>(json);
	return message.SendAt > DateTimeOffset.Now
};
Code language: C# (cs)

Erstens ist dies schwieriger zu lesen als die vereinfachte Assertion, die wir mit dem Callback()-Ansatz machen konnten. Zweitens führt dies zu folgendem Kompilierungsfehler:

Wir können hier keinen Anweisungstext (geschweifte Klammern mit mehreren ausführbaren Zeilen darin) verwenden. Stattdessen müssen wir den folgenden Einzeiler verwenden:

mockRepo.Verify(t => t.Save(It.Is<string>(json => JsonSerializer.Deserialize<Message>(json, null).SendAt > DateTimeOffset.Now)));
Code language: C# (cs)

Erstens ist dies noch schwieriger zu lesen. Beachten Sie zweitens, dass wir null übergeben mussten zu Deserialize(), obwohl es sich um einen optionalen Parameter handelt. Dies liegt daran, dass optionale Parameter bei Verwendung der Moq-API nicht optional sind (aufgrund der Verwendung von System.Linq.Expressions).

Je komplexer das Szenario wird, desto komplizierter wird dieser einzeilige Ansatz.

Dies zeigt, wie die Verwendung von Callback() zum Erfassen von Parametern Behauptungen erheblich vereinfachen kann.

Callback-Lambda-Parameter müssen mit den simulierten Methodenparametern übereinstimmen

Wenn die Callback-Lambda-Parameter nicht mit den verspotteten Methodenparametern übereinstimmen, erhalten Sie die folgende Laufzeitausnahme:

Hinweis:Dies gilt nicht für die parameterlose Callback()-Überladung. Es gilt nur für die unzähligen Callback(Action)-Überladungen.

Angenommen, Sie verspotten IRepository und möchten einen Rückruf für die Delete(int, bool)-Methode einrichten:

public interface IRepository
{
	public void Delete(int id, bool cascadingDelete=true);
}
Code language: C# (cs)

Hier ist ein Beispiel für einen falschen Rückruf:

var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Delete(It.IsAny<int>(), It.IsAny<bool>()))
	.Callback((int id) =>
	{
		Console.WriteLine($"Delete called with {id}");
	});
Code language: C# (cs)

Dies würde die folgende Ausnahme auslösen:

Wie die Ausnahme erwähnt, erwartet sie, dass die Lambda-Parameter mit den Delete(int, bool)-Parametern übereinstimmen. Sie müssen vom gleichen Typ und in der gleichen Reihenfolge sein und sogar optionale Parameter enthalten (beachten Sie, dass bool cascadingDelete ist ein optionaler Parameter).

var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Delete(It.IsAny<int>(), It.IsAny<bool>()))
	.Callback((int id, bool cascadingDelete) =>
	{
		Console.WriteLine($"Delete(id={id}, cascadingDelete={cascadingDelete})");
	});
Code language: C# (cs)

Callback-Lambda-Parametertypen müssen explizit angegeben werden

Wenn Sie die Callback-Lambda-Parametertypen nicht explizit angeben, erhalten Sie den folgenden Kompilierungsfehler:

Dies bezieht sich auf diese Callback()-Überladung in der Moq-API, von der der Compiler annimmt, dass Sie versuchen, sie zu verwenden:

ICallbackResult Callback(InvocationAction action);
Code language: C# (cs)

Angenommen, Sie verspotten IRepository und möchten einen Rückruf für die Save(bool)-Methode einrichten:

public interface IRepository
{
	public void Save(bool inTransaction=false);
}
Code language: C# (cs)

Die folgende Callback-Einrichtung ist falsch, da sie den Typ für inTransaction nicht angibt Parameter. Dies führt zum CS1660-Kompilierungsfehler:

var mockRepo = new Mock<IRepository>();
mockRepo.Setup(t => t.Save(It.IsAny<bool>()))
	.Callback((inTransaction) =>
	{
		Console.WriteLine($"Save({inTransaction})");
	});
Code language: C# (cs)

Sie müssen den Parametertyp explizit angeben. Sie können den Typ entweder wie folgt in der Lambda-Deklaration angeben:

.Callback((bool inTransaction) =>
{
	Console.WriteLine($"Save({inTransaction})");
});
Code language: C# (cs)

Oder Sie können den generischen Typparameter wie folgt deklarieren:

.Callback<bool>((inTransaction) =>
{
	Console.WriteLine($"Save({inTransaction})");
});
Code language: C# (cs)

Der erste Ansatz ist besser, weil er den Parametertyp und -namen zusammenhält, was einfacher zu lesen ist. Wählen Sie jedoch die Option, die Sie bevorzugen.