Użycie biblioteki Moq w testowaniu aplikacji

Ostatnim razem mówiliśmy o testach jednostkowych i integracyjnych, dziś przyszedł czas (tak jak śpiewali w Królu Lwie „przyjdzie czas…”) na zagłębienie się w tych pierwszych.

Na tapetę leci temat Mockowania – zewnętrznej biblioteki która pozwoli nam testować pojedyncze moduły w aplikacjach o wielkiej złożoności.

Pominę tu cały proces dodawania odpowiedniej paczki Nuget.

Zanim zaczniemy omawiać bibliotekę Moq zróbmy błyskawiczną powtórkę z testów jednostkowych:

  • Testują pojedyncze elementy (klasy, metody) w izolacji od pozostałych.
  • Działają błyskawicznie.
  • Niepowodzenie oznacza jedno miejsce do naprawy.
  • Nie wymagają dodatkowej konfiguracji.
  • Operujemy na pamięci (nie tykamy się zewnętrznych zasobów takich jak bazy danych czy serwisy internetowe).

Postawmy przed sobą zadanie

napisania testów jednostkowych dla metody korzystającej z bazy danych (oczywiście nie chcemy się z nią łączyć).

Aby temu sprostać mamy możliwe kilka rozwiązań, przykładowo takie:

  1. Napisać dodatkowy kod w postaci „FakeDatabase” jedynie na potrzeby wykonania testów.
  2. Skorzystać z biblioteki Moq za pomocą której będzie „naśladować” pewne zachowania.

A czym jest Mockowanie?

To sposób na utworzenie sztucznego tworu zastępującego wybraną implementację, którego zachowanie będzie zgodne z naszymi oczekiwaniami.

W naszym przypadku będzie to naśladowanie połączenia się z bazą danych.

Jakie płyną z tego korzyści?

  • Testujemy pojedynczy element, a poprzedzającego go operacje jedynie sztucznie wykorzystujemy w teście.
  • Unikamy scenariuszy w których tworzylibyśmy instancje potrzebnych nam obiektów, nie powinno dojść do nieoczekiwanych błędów po drodze do testowej funkcjonalności.

Mockowanie w praktyce

Bibliotekami używanymi w przykładzie będą Moq oraz Nunit. Na potrzeby naszego przykładu stwórzmy na szybko prosty fragment kodu który nie będzie zbytnio odbiegać od rzeczywistości.

Dodatkowo chciałbym zaznaczyć, że będzie to tylko szkielet który pozwoli zobrazować praktyczne zastosowanie Mockowania (stąd klasy nie będą kompletne, pomijam walidację, większość potrzebnych nam propertisów oraz implementację poszczególnych metod).

Stwórzmy sklep w którym docelowo będziemy chcieli przetestować metodę ProcessOrder.

Na początek dwie bazowy klasy User:

namespace Blog.Moq
{
    public class User
    {
        public string Email { get; private set; }

        public string Password { get; private set; }

        public User(string email, string password)
        {
            Email = email;
            Password = password;
        }

        public void PurchaseOrder(Order order)
        {
            order.Purchase();
        }
    }
}

oraz Order:

namespace Blog.Moq
{
    public class Order
    {
        public int Id { get; private set; }

        public decimal Price { get; private set; }

        public bool IsPurchased { get; private set; }

        public Order(int id, decimal price)
        {
            Id = id;
            Price = price;
        }

        public void Purchase()
        {
            IsPurchased = true;
        }
    }
}

Dane o użytkownikach jak i zamówieniach będą przetrzymywane w bazie danych, do której stworzymy prosty Interface:

namespace Blog.Moq
{
    public interface IDatabase
    {
        User GetUser(string email);

        Order GetOrder(int id);
    }
}

Czas na jego szybką implementację

namespace Blog.Moq
{
    public class Database : IDatabase
    {
        public User GetUser(string email)
        {
            throw new System.NotImplementedException();
        }

        public Order GetOrder(int id)
        {
            throw new System.NotImplementedException();
        }
    }
}

Zapewne każdy użytkownik chciałby dostać informację o złożonym zamówieniu, przygotujmy szybki serwis wysyłający odpowiednia powiadomienia email (zaczynając oczywiście od interfejsu – lekkiego jak piórko)

namespace Blog.Moq
{
    public interface IEmailSender
    {
        void SendMessage(string receiver, string title, string message);
    }
}

implementujemy:

namespace Blog.Moq
{
    public class EmailSender : IEmailSender
    {
        public void SendMessage(string receiver, string title, string message)
        {
            throw new System.NotImplementedException();
        }
    }
}

Powolutku zbliżamy się do clue naszej sprawy. Zajmijmy się przetwarzaniem zamówienia, do tego będzie nam potrzebny konkretny użytkownik (pobierany po mailu) oraz zamówienia (po Id).

namespace Blog.Moq
{
    public interface IOrderProcessor
    {
        void ProcessOrder(string email, int orderId);
    }
}

Zaimplementujemy powyższy interfejs, przekażmy w konstruktorze interfejsy potrzebne nam by dobrać się do bazy danych jak i mieć możliwość wysłania wiadomości email użytkownikowi.

namespace Blog.Moq
{
    public class OrderProcessor : IOrderProcessor
    {
        private readonly IDatabase _database;
        private readonly IEmailSender _emailSender;

        public OrderProcessor(IDatabase database, IEmailSender emailSender)
        {
            _database = database;
            _emailSender = emailSender;
        }

        public void ProcessOrder(string email, int orderId)
        {
            User user = _database.GetUser(email);
            Order order = _database.GetOrder(orderId);

            user.PurchaseOrder(order);
            _emailSender.SendMessage(email, "Title", "Message");
        }
    }
}

I tu pojawia się metodą którą zamierzamy przetestować – ProcessOrder.

Jak widać po drodze było trochę zamieszania – a to wyciągnij mi użytkownika i numer zamówienia z bazy danych, a to wyślij maila, ustaw flagę IsPurchased na true. A my biedni chcemy napisać prosty test jednostkowy i jak się za to zabrać?

Zmockujmy co nieco!

W oddzielnym projekcie z dodanymi paczuszkami Nuget ( Moq oraz Nunit) i referencją do projektu zawierającego powyższy kod stwórzmy klasę do przetestowania metody ProcessOrder.

using Blog.Moq;
using Moq;
using NUnit.Framework;

namespace BlogUnitTests
{
    [TestFixture]
    public class OrderProcessorTests
    {
        public User User;
        public Order Order;

        public Mock<IDatabase> DatabaseMock;
        public Mock<IEmailSender> EmailSenderMock;

        public IOrderProcessor OrderProcessor;

        [SetUp]
        public void Setup()
        {
            User = new User("example@email.com", "password");
            Order = new Order(1, 49.99M);
            
            DatabaseMock = new Mock<IDatabase>();
            EmailSenderMock = new Mock<IEmailSender>();

            DatabaseMock.Setup(x => x.GetUser(User.Email)).Returns(User);
            DatabaseMock.Setup(x => x.GetOrder(Order.Id)).Returns(Order);

            OrderProcessor = new OrderProcessor(DatabaseMock.Object, EmailSenderMock.Object);
        }

        [Test]
        public void process_order_should_succeed()
        {
            OrderProcessor.ProcessOrder(User.Email,Order.Id);

            DatabaseMock.Verify( x => x.GetUser(It.IsAny<string>()), Times.Once);
            DatabaseMock.Verify( x => x.GetOrder(It.IsAny<int>()), Times.Once);

            Assert.IsTrue(Order.IsPurchased);
        }
    }
}

W końcu pojawia się wspomniane Mockowanie.

  1. Tworzymy nowe obiekty mockujące na bazie interfejsów do bazy danych – IDatabase oraz do systemu email – IEmailSender.
  2. Wykorzystując metodę Setup (z biblioteki Moq) pobieramy już ze zmockowanej bazy danych użytkownika oraz zamówienie.
  3. By stworzyć nową instancję klasy OrderProcessor przekazujemy do konstruktora DatabaseMock.Object i EmailSenderMock.Object.
  4. Tworzymy metodę testującą (z biblioteką Nunit za pan brat), sprawdzamy czy flaga IsPurchased została przestawiona na true.

Dodatkowo jak widać, możemy dokonać sprawdzenia czy pobieranie użytkownika jak i zamówienia z bazy danych nastąpiło jedynie raz.

Wynik testu zakończony został sukcesem.

Podsumujmy

Stworzone w ten sposób atrapy obiektów pozwoliły nam na przeprowadzenie testów jednostkowych bez konieczności używania rzeczywistych obiektów.

Choć cały proces może wydawać się skomplikowany i niepotrzebny, płynie z niego mimo wszystko wiele korzyści.

Zaliczyć do nich mogę między innymi zwiększenie prędkości wykonywalnych testów jednostkowych z racji symulowanego łączenia z bazą. Korzystanie wpłynie to na cały przebieg budowania aplikacji, bo któż chciałby często korzystać z testów jednostkowych które zamulają?

Korzyści z Mockowania jest oczywiście o wiele więcej ale to już zostawię Tobie drogi czytelniku. Przemyśl to w wolnej chwili i daj znać w komentarzach!

Pozdrawiam Wojtek