Kurs TDD cz. 15: Wstęp do Moq

Moq to najpopularniejszy framework do tworzenia atrap w .NET. W tej części kursu poznamy jego składnię i podstawowe możliwości.

moq

Krótka historia Moq

Moq powstał w czasach kiedy najpopularniejszym frameworkiem do atrap w .NET był Rhino Mocks. Rhino, który jest również open source’owy, miał jednak jedną wielką wadę – syntaktyka. Kod powstały przez Rhino opierał się na syntaktyce “Record & Play”, która była nieczytelna, trudna w utrzymaniu oraz uciążliwa w debugowaniu. Moq budowany był od początku w oparciu o wyrażenia lambda. Jednym z założeń Moq jest krótka ścieżka nauki, w przeciwieństwie do Rhino, gdzie nauka “Record & Play” wymaga ciut więcej zaangażowania. Moq wprowadził jeszcze jedną nowinkę — Rhino rozróżniał dwa typy atrap: stub i mock, podczas gdy w Moq wszystko zostało uproszczone do jednego typu: mock (co niektórzy uznają jako wadę, inni jako zaletę).

Z czasem Rhino Mocks przestał się rozwijać, przepadł w otchłań zapomnienia, a Moq przybyło dwóch konkurentów – FakeItEasy i NSubstitute. W dalszym ciągu Moq pozostał dalej bardzo mocnym graczem. Który framework wybrać? Wszystkie trzy są dobre. Warto zaznajomić się z każdym z nich i wybrać ten, który najbardziej przypada do gustu.

Wstęp do Moqowania

Na tapetę weźmy przykład z ostatniej części, w której tworzyliśmy ręczną atrapę.

Nasz kod bazowy wygląda następująco:

public class CustomerValidator
{
    private const int MinimumAge = 18;
 
    public bool Validate(ICustomer customer)
    {
        if (customer == null) throw new ArgumentNullException(nameof(customer));
 
        if (customer.GetAge() < MinimumAge) return false;
        return true;
    }
}

Dla tego kodu stworzyliśmy już testy jednostkowe, jednak tym razem zamiast ręcznie stworzonego mocka (CustomerMock), posłużymy się Moq.

Dla przypomnienia, CustomerMock, nasz ręcznie wygenerowany mock, wyglądał następująco:

internal class CustomerMock : ICustomer
{
    private readonly int _expectedAge;

    public CustomerMock(int expectedAge)
    {
        _expectedAge = expectedAge;
    }

    public int GetAge() => _expectedAge;
}

Przypomnijmy, jeden z testów sprawdzał czy wartość walidatora jest równa “fałsz” pod warunkiem jeśli wiek klienta był mniejszy niż 18:

[Test]
public void WhenCustomerHasAgeLessThan18_ThenValidationFails()
{
    var validator = new CustomerValidator();
    var customer = new CustomerMock(expectedAge: 16);

    bool validate = validator.Validate(customer);

    validate.Should().BeFalse();
}

Aby stworzyć atrapę przy pomocy Moq, musimy posłużyć się obiektem klasy Mock. Składnia dla naszego przypadku będzie więc wyglądać następująco:

var customerMock = new Mock<ICustomer>();

Aby ustawić wartość oczekiwaną dla naszego mocka musimy wywołać metody Setup i Returns:

customerMock.Setup(x => x.GetAge()).Returns(16);

W metodzie Setup mówimy Moq co chcemy zamockować (metoda/właściwość), a Returns co ma być zwrócone. Innymi słowy, powyższa linijka może być przeczytana następująco:

“Dla mocka interfejsu ICustomer, przy wywołaniu metody GetAge(), zwróć wartość równą 16.”

Proste? I to bardzo! Należy pamiętać o jeszcze jednej kwestii. Aby przekazać nasz interfejs ICustomer do metody Validate(), musimy skorzystać z właściwości “Object” mocka. Właściwość “Object” jest w naszym przypadku typu ICustomer. Zależność ta wygląda następująco:

Mock<ICustomer> customerMock = new Mock<ICustomer>(); // Mock dla typu T
ICustomer customer = customerMock.Object; // Obiekt typu T

Nasz zaktualizowany test wygląda następująco:

[Test]
public void WhenCustomerHasAgeLessThan18_ThenValidationFails()
{
    var validator = new CustomerValidator();

    var customerMock = new Mock<ICustomer>();
    customerMock.Setup(x => x.GetAge()).Returns(16);

    bool validate = validator.Validate(customerMock.Object);

    validate.Should().BeFalse();
}

MockBehavior

Pytanie — Co jeśli nie wywołamy metody Setup i Returns, a przekażemy mock jako parametr w następujący sposób:

Mock<ICustomer> customerMock = new Mock<ICustomer>();

bool validate = validator.Validate(customerMock.Object);

Moq może zachować się na dwa sposoby dla nieustawionych właściwości i metod:

  1. Zwrócona zostanie wartość domyślna (default(T)).
  2. Przy pierwszym wywołaniu nieustawionej właściwości/metody, wyrzucony zostanie wyjątek typu MockException.

Domyślne zachowanie to zachowanie nr 1, a manipulować nim możemy przy pomocy typu enum MockBehavior:

public enum MockBehavior
{
	/// <summary>
	/// Causes the mock to always throw 
	/// an exception for invocations that don't have a 
	/// corresponding setup.
	/// </summary>
	Strict,
	/// <summary>
	/// Will never throw exceptions, returning default  
	/// values when necessary (null for reference types, 
	/// zero for value types or empty enumerables and arrays).
	/// </summary>
	Loose,
	/// <summary>
	/// Default mock behavior, which equals <see cref="Loose"/>.
	/// </summary>
	Default = Loose,
}

MockBehavior ustawiony może być tylko w konstruktorze dla typu Mock, np.:

var customerMock = new Mock<ICustomer>(MockBehavior.Loose);

Kod imperatywny, a funkcyjny

Przedstawiana powyżej składnia dotyczy imperatywnego stylu. Moq pozwala także na zapis w stylu funkcyjnym, co pozwala na zwiększenie czytelności kodu.

Tworzenie mocka w stylu fukcjonalnym ma następujące różnice:
– Składnia dla tworzenia mocka to Mock.Of().
– Do ustawienia oczekiwanej wartości używamy operatora “==”.
– Zwracany typ to T. Nie musimy więc odwoływać się do obiektu poprzez właściwość Object, gdyż jej już nie ma.
– Aby później zmodyfikować taki mock (np. dodać Setup), należy posłużyć się metodą Mock.Get().

Ustawienie mocka w stylu funkcyjnym wygląda następująco:

ICustomer customerMock = Mock.Of<ICustomer>(customer => customer.GetAge() == 16);
bool validate = validator.Validate(customerMock);

Dzięki syntaktyce funkcyjnej możemy w prosty sposób zbudować złożone mocki. Np.:

ICustomer customerMock = Mock.Of<ICustomer>(customer => 
	customer.FirstName == "John" &&
	customer.LastName == "Kowalski" &&
	customer.PercentageDiscount == 20 &&
	customer.PhoneNumber == Mock.Of<IPhoneNumber>(number => number.MobileNumber == "123-456-789") &&
	customer.Orders == new List<IOrder>
	{
	    Mock.Of<IOrder>(order => order.Id == 23 && order.Price == 20.01m),
	    Mock.Of<IOrder>(order => order.Id == 65 && order.Price == 59.99m),
	    Mock.Of<IOrder>(order => order.Id == 82 && order.Price == 9.99m),
	} &&
	customer.GetAge() == 20);

Co możemy, a czego nie możemy mockować?

W podanych przykładach tworzyliśmy atrapy dla interfejsów. Co jednak z mockowaniem klasy? Jest taka możliwość, ale wiąże się to z następującymi ograniczeniami i efektami ubocznymi, m.in:

  • Nie możemy mockować statycznych klas ani metod.
  • Wywołany zostanie któryś z konstruktorów oraz logika związana z inicjalizacją obiektu (np. inicjalizacja pól).
  • Metoda musi być wirtualna, w przeciwnym razie dostaniemy wyjątek typu NotSupportedException o treści:

“NotSupportedException was unhandled by user code

An exception of type ‘System.NotSupportedException’ occurred in Moq.dll but was not handled in user code
Additional information: Invalid setup on a non-virtual (overridable in VB) member”

Zaleca się więc tworzenie interfejsów, jeśli takowych nie ma (refaktoring “Extract Interface”), a są potrzebne. Unikniemy dzięki temu wielu pułapek, a przy okazji zmniejszymy zależność między naszymi modułami (class coupling). Wyjątkiem mogą być np. obiekty typu DTO (Data Transfer Object), które nie posiadają żadnej logiki.

Podsumowanie

W tej cześci artykułu poznaliśmy podstawowe koncepcje mockowania przy pomocy frameworka Moq. Moq pozwala na zdefiniowanie atrap przy użyciu dwóch styli: imperatywnego lub funkcyjnego. Aby uniknąć efektów ubocznych, zaleca się mockowanie interfejsów, nie klas.

W dalszych częściach kursu poznamy bardziej zaawansowane techniki (argument matching, verify, callback) oraz pozostałe frameworki (NSubstitute, FakeItEasy).

Przypominam, że kod źródłowy całego kursu TDD, jak i tego rozdziału jest dostępny na GitHubie: https://github.com/dariusz-wozniak/TddCourse.

Linki zewnętrzne

9 thoughts on “Kurs TDD cz. 15: Wstęp do Moq

  1. Kolega z Javy czy JS przychodzi?
    “public int GetAge() => _expectedAge;“
    W C# są właściwości:
    “public int Age { get; } => _expectedAge;“
    Coś przespałem, stały się ostatnio niemodne…?

      • Właściwość też przecież może mieć kod – na tym właśnie polega ich przewaga nad polami składowymi…

        public int Age {
        get {
        return DateTime.Now.Year – dateOfBirth.Year – (DateTime.Now.DayOfYear < dateOfBirth.DayOfYear ? 1 : 0);
        }
        }

      • To prawda, jest taka możliwość. Przy czym osobiście preferuję, poza kilkoma wyjątkami, użycie metod w miejsce gdzie wykonywana jest logika, a właściwości dla danych i tylko danych.

  2. Pingback: Kurs TDD cz. 17: FakeItEasy | DariuszWoźniak .NET
  3. Pingback: Kurs TDD cz. 18: NSubstitute | DariuszWoźniak .NET
  4. Pingback: Kurs TDD cz. 19: Mock, stub, fake, spy, dummy | DariuszWoźniak .NET
  5. Pingback: Kurs TDD cz. 20: Mockowanie DateTime.Now, random, static, itp. | DariuszWoźniak .NET
  6. Pingback: constrained – Justyna i Adam

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s