Top.Mail.Ru
онлайн-митап
19 декабря в 17:00 МСК
Матрица компетенций: как инструмент для развития разработчика

Применение Mock объектов для модульного тестирования (Mock testing) с примером на C#

В этой статье речь пойдет о применении Mock-объектов для модульного тестирования (Mock testing). Суть заключается в том, что для эмуляции еще не реализованных объектов мы применяем вместо Fake-объектов так называемые Mock-объекты.

Самая важная идея хорошего ООП – это избавление от зависимостей. Классы должны зависеть только от абстракций, но не от конкретной реализации.  Но как все это тестировать? Когда у нас есть конкретный класс, мы можем создать его объект, вызвать метод и проверить возвращенный результат. Но если у нас имеется зависимость от абстракций, мы не можем создать конкретный экземпляр класса, пока не реализуем все классы, от которых он зависит. И тут нам на помощь приходят Mock-объекты.

Например, пусть у нас имеется класс товара:

    /// <summary>
    /// Товар
    /// </summary>
    public class Product
    {
        /// <summary>
        /// Категория товара
        /// </summary>
        public string Category;

        /// <summary>
        /// Наименование товара
        /// </summary>
        public string Name;

        /// <summary>
        /// Цена товара
        /// </summary>
        public decimal Price;
    }

Этот класс хранит информацию о конкретном товаре. Чтобы хранить информацию о нескольких товарах, мы можем воспользоваться какой-нибудь коллекцией, например, списком или массивом.

Нам необходимо вычислить стоимость товаров с учетом скидки. Но как считается скидка – мы пока не знаем, и алгоритм расчета скидки в будущем может измениться. Хороший повод применить абстракцию (абстрактный класс или интерфейс). В нашем случае мы создадим интерфейс для расчета скидки в зависимости от стоимости всех товарных позиций:

    /// <summary>
    /// Представитель скидки
    /// </summary>
    public interface IDiscountHelper
    {
        /// <summary>
        /// Применить скидку
        /// </summary>
        /// <param name="v">Сумма без скидки</param>
        /// <returns>Сумма с учетом скидки</returns>
        decimal ApplyDiscount(decimal v);
    }

Тогда класс, рассчитывающий стоимость товаров (с учетом скидки) будет выглядеть так:

    /// <summary>
    /// Расчет стоимости товаров
    /// </summary>
    public class LinqValueCalculator
    {
        /// <summary>
        /// Предоставитель скидки
        /// </summary>
        private IDiscountHelper discounter;

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="discounterParam">Предоставитель скидки</param>
        public LinqValueCalculator(IDiscountHelper discounterParam)
        {
            discounter = discounterParam;
        }

        /// <summary>
        /// Расчет стоимости
        /// </summary>
        /// <param name="products">Товары</param>
        /// <returns>Стоимость товаров</returns>
        public decimal ValueProducts(IEnumerable<Product> products)
        {
            return discounter.ApplyDiscount(products.Sum(p => p.Price));
        }
     }

Обратите внимание, аргумент функции расчета суммы – абстрактная коллекция товаров, поддерживающая интерфейс IEnumerable. Это может быть обычный список или массив или даже отдельно разработанная коллекция, лишь бы она поддерживала IEnumerable.

Чтобы протестировать класс LinqValueCalculator нам необходимо реализовать интерфейс IDiscountHelper. Можно, конечно, поставить заглушку. Но если подобным интерфейсов много? Поэтому, есть способ лучше – эмулировать заглушки при помощи специальных объектов (Mock-объектов). Это и есть Mock-testing.

Итак нам сначала нужно подключить NuGet пакет с библиотеками, необходимыми для Mock-testing-а. Для этого идем в «Управление NuGet пакетами»:

У нас открывается список NuGet-пакетов, которые вообще есть, и которые теоретический можно загрузить и установить:

Их очень много, но можно воспользоваться поиском, чтобы найти библиотеку Mock-testing-а, которая называется Moq:

Нас интересует пакет MOQ. Просто щелкаем по нему, чтобы установить:

После установки вы можете подключить библиотеки там, где это необходимо:

И все, теперь мы можем использовать Mock-testing. Вот как у нас выглядит тестовый модуль:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ClassLibrary1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;

namespace ClassLibrary1.Tests
{
    [TestClass()]
    public class LinqValueCalculatorTests
    {
        private Product[] products = {
          new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
          new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
          new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
          new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
        };

        [TestMethod()]
        public void LinqValueCalculatorTest()
        {
            // arrange
            var discounter = new MinimumDiscountHelper();
            var target = new LinqValueCalculator(discounter);
            var goalTotal = products.Sum(e => e.Price);
            // act
            var result = target.ValueProducts(products);
            // assert
            Assert.AreEqual(goalTotal, result);
        }

        [TestMethod]
        public void Sum_Products_Correctly()
        {
            Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
            mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(2,3000, Range.Inclusive)))
            .Returns<decimal>(total => total);
            var target = new LinqValueCalculator(mock.Object);
            var result = target.ValueProducts(products);
            Assert.AreEqual(products.Sum(e => e.Price), result);
        }
    }
}

Для наглядности, тестовый метод реализован в двух вариантах: LinqValueCalculatorTest – когда требуется реализовать все классы, и Sum_Products_Correctly – через Mock объекты, которые эмулируют недостающие классы.

Теперь немножко о том, как, собственно говоря, работают Mock-объекты.

Вот в этой строке кода:

mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(2,3000, Range.Inclusive))).Returns<decimal>(total => total);

Мы задаем правила эмуляции метода ApplyDiscount. В данной случае это правило такое: «Число из диапазона от 2 до 3000 включительно».

Если мы хотим задать вообще любое число, тогда используем вот такой код:

mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);

Можно симулировать применение 10%-ой скидки на все товары:

        [TestMethod]
        public void Sum_Products_Correctly()
        {

            Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
            mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total*0.9m);
            var target = new LinqValueCalculator(mock.Object);
            var result = target.ValueProducts(products);
            Assert.AreEqual(products.Sum(e => e.Price * 0.9m), result);
        }

По своей сути метод Setup задает, что должен возвращать определённый метод интерфейса, который связан с данным Mock – объектом.

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar