Moq für komplexere Anwendungsfälle

Moq ist eine kleine Mock-Bibliothek, die ich hier bereits einmal vorgestellt habe. Für die meisten Anwendungsfälle genügt es die Rückgabewerte von Methoden zu beeinflussen. Ab und zu wäre es aber praktisch ein wenig mehr mit den Mocks machen zu können. Heute zeige ich wie Moq einem bei spezielleren Anwendungsfällen unterstützen kann.

 

Flexible Rückgabewerte

Mit Moq ist es kein Problem verschiedene Rückgabewerte zu liefern – so lange sich die Parameter beim Methodenaufruf unterscheiden. Der notwendige Code um den Wert „A“ für die Id 1 und „B“ für die Id 2 zu erhalten sieht so aus:

public interface ISimpleDemo
{
    string GetValue(int id);
}

[TestMethod]
public void MultipleReturnsForDifferentInputs()
{
    var mock = new Mock<ISimpleDemo>();
    mock.Setup(service => service.GetValue(1)).Returns("A");
    mock.Setup(service => service.GetValue(2)).Returns("B");

    Assert.AreEqual("A", mock.Object.GetValue(1));
    Assert.AreEqual("B", mock.Object.GetValue(2));
}

Hin und wieder steht man aber vor der Aufgabe bei einer Methode ohne Parameter verschiedene Rückgabewerte definieren zu müssen. Der direkte Versuch die gewünschten Werte nach einander zu definieren schlägt leider fehl:

public interface IBlogDemo
{
    string GetNextName();
}

[TestMethod]
public void MultipleReturnsTheWrongWay()
{
    var mock = new Mock<IBlogDemo>();
    mock.Setup(service => service.GetNextName()).Returns("A");
    mock.Setup(service => service.GetNextName()).Returns("B");
    mock.Setup(service => service.GetNextName()).Returns("C");

    Assert.AreEqual("A", mock.Object.GetNextName()); // Gibt "C"
    Assert.AreEqual("B", mock.Object.GetNextName());
    Assert.AreEqual("C", mock.Object.GetNextName());
}

So überschreibt man nur den Rückgabewert und der Tests endet mit dieser Fehlermeldung:

Assert.AreEqual failed. Expected:<A>. Actual: <C>.

Damit man diesen Test erfüllen kann benötigt es einen anderen Ansatz. Mit Hilfe einer Queue kann man die Werte in der gewünschten Reihenfolge ablegen (auch null-Werte sind so möglich). Die Returns-Funktion von Moq ruft nun die Dequeue-Methode auf und bekommt die für den Test benötigten Werte zurück:

[TestMethod]
public void MultipleReturnsRight()
{
    var mock = new Mock<IBlogDemo>();
    var results = new List<string> {"A", "B", "C", null};
    var pq = new Queue<string>(results);

    mock.Setup(c => c.GetNextName()).Returns(pq.Dequeue);

    Assert.AreEqual("A", mock.Object.GetNextName());
    Assert.AreEqual("B", mock.Object.GetNextName());
    Assert.AreEqual("C", mock.Object.GetNextName());
    Assert.IsNull(mock.Object.GetNextName());
    // ==> Funktioniert
}

 

Wurde eine Methode aufgerufen?

Moq bietet einem die Möglichkeit einzelne Aspekte des verwendeten Mocks zu überprüfen. Muss man wissen wie oft eine Methode des Mocks aufgerufen wurde ist die Funktion Verify sehr hilfreich. Der notwendige Code um zu prüfen ob GetValue mit Parameter 1 genau einmal aufgerufen wurde wird so sehr einfach:

[TestMethod]
public void ExactlyOnceCalled()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);

    mock.Verify(db => db.GetValue(1), Times.Once());
}

Wird die Methode nicht mit dem erwarteten Parameter aufgerufen (zum Beispiel in dem man im oberen Code-Ausschnitt GetValue mit dem Wert 2 aufruft), wirft Moq eine entsprechende Fehlermeldung:

Test method TestProject1.UnitTest1.ExactlyOnceCalled threw exception:
Moq.MockException:
Expected invocation on the mock once, but was 0 times: db => db.GetValue(1)
No setups configured.
Performed invocations:
ISimpleDemo.GetValue(2)

Es kann vorkommen das man nicht genau weiss mit welchem Parameter die Methode aufgerufen wurde (wenn beispielsweise in der zu testenden Klasse neue Instanzen erzeugt werden). In diesem Fall kann man die It-Klasse verwenden:

[TestMethod]
public void ExactlyOnceCalledUnknownParameter()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);

    mock.Verify(db => db.GetValue(It.IsAny<int>()), Times.Once());
}

Auch bei der Anzahl der Aufrufe ist Moq sehr flexibel. Ob eine Methode einmal, nie, genau x mal oder innerhalb eines Bereiches aufgerufen wurde kann man alles mit der Times-Klasse überprüfen lassen:

[TestMethod]
public void TimesHasManyUsages()
{
    var mock = new Mock<ISimpleDemo>();

    mock.Object.GetValue(1);
    mock.Object.GetValue(2);
    mock.Object.GetValue(2);

    mock.Verify(db => db.GetValue(1), Times.Once());
    mock.Verify(db => db.GetValue(2), Times.AtLeastOnce());
    mock.Verify(db => db.GetValue(2), Times.Between(1, 2, Range.Inclusive));
    mock.Verify(db => db.GetValue(2), Times.Between(1, 3, Range.Exclusive));
    mock.Verify(db => db.GetValue(2), Times.Exactly(2));
    mock.Verify(db => db.GetValue(3), Times.Never());
}

 

Fazit

Moq unterstützt einem auch bei komplexeren Einsatzszenarien. Die Möglichkeiten von Verify sind vielfältig und können einem dabei helfen so manches Testszenario abzudecken. Dabei sind It.IsAny und Times eine grosse Hilfe.

Allerdings darf man bei so viel Flexibilität das Ziel des Tests nicht aus den Augen verlieren. Sobald man sich auf zu viele Implementationsdetails einlässt werden die Tests sehr schnell unwartbar. Im Zweifel sollte man daher lieber erst versuchen mit einer besseren Code-Struktur den Test einfacher zu gestalten.