Moq to najpopularniejszy framework do tworzenia atrap w .NET. W tej części kursu poznamy jego składnię i podstawowe możliwości.
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.
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() _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();
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 metodyGetAge
, 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 customerMock = new Mock(); // 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();
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 customerMock = new Mock();
bool validate = validator.Validate(customerMock.Object);
Moq może zachować się na dwa sposoby dla nieustawionych właściwości i metod:
default(T)
).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(MockBehavior.Loose);
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:
Mock.Of()
.==
.T
.Object
, gdyż jej już nie ma.Setup
), należy posłużyć się metodą Mock.Get
.Ustawienie mocka w stylu funkcyjnym wygląda następująco:
ICustomer customerMock = Mock.Of(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(customer =>
customer.FirstName == "John" &&
customer.LastName == "Kowalski" &&
customer.PercentageDiscount == 20 &&
customer.PhoneNumber == Mock.Of(number => number.MobileNumber == "123-456-789") &&
customer.Orders == new List
{
Mock.Of(order => order.Id == 23 && order.Price == 20.01m),
Mock.Of(order => order.Id == 65 && order.Price == 59.99m),
Mock.Of(order => order.Id == 82 && order.Price == 9.99m),
} &&
customer.GetAge() == 20);
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:
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 (refaktoryzacja “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.
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.
Część I: Testy jednostkowe – wstęp
Część II: Atrapy obiektów
Część III: Teoria