В предыдущем посте я начал проект, чтобы начать изучать модульное тестирование, в частности, с moq и для сервисов WCF. До сих пор мы реализовали только базовые модульные тесты для части кода без внешних зависимостей, которые не нуждались в насмешках.

На этот раз я собираюсь исследовать имитацию и тестирование объектов, опираясь на интерфейсы IDbGetSomeNumbers и INumberFunctions. Напомним, что эти интерфейсы и соответствующие классы определены как:

namespace RussUnitTestSample.Business.Interface
{

    /// <summary>
    /// Interface for number functions
    /// </summary>
    public interface INumberFunctions
    {
        /// <summary>
        /// Add numbers together
        /// </summary>
        /// <param name="numbers">The numbers to add.
        /// <returns>The sum</returns>
        double AddNumbers(double[] numbers);
    }

    /// <summary>
    /// Interface to get some numbers from the database
    /// </summary>
    public interface IDbGetSomeNumbers
    {

        /// <summary>
        /// Get an array of doubles from the database
        /// </summary>
        /// <returns></returns>
        double[] GetSomeNumbers();
    }
}

И класс, использующий их:

namespace RussUnitTestSample.Business
{

    /// <summary>
    /// Get numbers and then add them together
    /// </summary>
    public class GetNumbersAndAddThem
    {

        #region Private
        private readonly IDbGetSomeNumbers _dbGetSomeNumbers;
        private readonly INumberFunctions _numberFunctions;
        #endregion Private

        #region ctor

        /// <summary>
        /// Constructor - provide dependencies
        /// </summary>
        /// <param name="dbGetSomeNumbers">THe IDbGetSomeNumbers implementation.
        /// <param name="numberFunctions">The INumberFunctions implementation.
        public GetNumbersAndAddThem(IDbGetSomeNumbers dbGetSomeNumbers, INumberFunctions numberFunctions)
        {
            if (dbGetSomeNumbers == null)
                throw new ArgumentNullException(nameof(dbGetSomeNumbers));

            if (numberFunctions == null)
                throw new ArgumentNullException(nameof(numberFunctions));

            this._dbGetSomeNumbers = dbGetSomeNumbers;
            this._numberFunctions = numberFunctions;
        }

        #endregion ctor

        #region Public methods

        /// <summary>
        /// Get the numbers and add them.
        /// </summary>
        /// <returns></returns>
        public double Execute()
        {
            var numbers = _dbGetSomeNumbers.GetSomeNumbers();

            return _numberFunctions.AddNumbers(numbers);
        }

        #endregion Public methods

    }

}

В конструкторе я использую реализацию как `IDbGetSomeNumbers`, так и `INumberFunctions`. Я делаю это, поскольку они не являются зависимостями для функциональности класса, и поэтому их реализация не важна. Скорее, это важно, но не для тестирования этого класса. Как сказано в определении модульного тестирования: Модульное тестирование — это процесс разработки программного обеспечения, в котором мельчайшие тестируемые части приложения, называемые модулями, индивидуально и независимо проверяются на предмет правильной работы.

Таким образом, реализации интерфейса нуждаются в тестировании (которое уже было сделано), однако они не нуждаются в тестировании со стороны GetNumbersAndAddThems. Единственное, что требует проверки с этой точки зрения, это то, что класс построен правильно, и что Execute «получает числа из базы данных», а затем «добавляет их».

Поскольку я использую конструктор класса для получения зависимостей класса:

/// <summary>
/// Constructor - provide dependencies
/// </summary>
/// <param name="dbGetSomeNumbers">THe IDbGetSomeNumbers implementation.
/// <param name="numberFunctions">The INumberFunctions implementation.
public GetNumbersAndAddThem(IDbGetSomeNumbers dbGetSomeNumbers, INumberFunctions numberFunctions)
{
    if (dbGetSomeNumbers == null)
        throw new ArgumentNullException(nameof(dbGetSomeNumbers));

    if (numberFunctions == null)
        throw new ArgumentNullException(nameof(numberFunctions));

    this._dbGetSomeNumbers = dbGetSomeNumbers;
    this._numberFunctions = numberFunctions;
}

Первое, что мы можем проверить, это то, что `dbGetSomeNumbers` и `numberFunctions` не равны нулю. Это может быть выполнено следующим образом:

/// <summary>
/// Ensure ArgumentNullException thrown when no IDbGetSomeNumbers implementation is provided
/// </summary>
[ExpectedException(typeof(ArgumentNullException))]
[TestMethod]
public void GetNumbersAndAddThem_Constructor_NullIDbGetSomeNumbers()
{
    // Arrange / Act / Assert
    GetNumbersAndAddThem obj = new GetNumbersAndAddThem(null, _mockNumberFunctions.Object);
}

/// <summary>
/// Ensure ArgumentNullException thrown when no NumberFunction implementation is provided
/// </summary>
[ExpectedException(typeof(ArgumentNullException))]
[TestMethod]
public void GetNumbersAndAddThem_Constructor_NullNumberFunctions()
{
    // Arranage / Act / Assert
    GetNumbersAndAddThem obj = new GetNumbersAndAddThem(_mockIDbGetSomeNumbers.Object, null);
}

Теперь для тестирования Execute мы наконец можем добраться до Moq! Насмешка идет рука об руку с модульным тестированием, как гласит одно определение насмешки:

Трудно тестировать состояния ошибок и исключения с помощью оперативных тестов на уровне системы. Заменив системные компоненты фиктивными объектами, можно смоделировать условия ошибки в модульном тесте. Например, бизнес-логика обрабатывает исключение Database Full, которое создается зависимой службой.

Так как реализация `IDbGetSomeNumbers` и `INumberFunctions` не имеет значения, мы не хотели бы (обязательно) использовать их реальные реализации. Это связано с тем, что они потенциально могут повлиять на систему или данные, чего мы не хотели бы делать, поскольку мы планируем запускать эти тесты при каждой сборке… и редактировать данные приложения при каждой сборке было бы… плохо. В любом случае, с помощью насмешек мы можем указать интерфейсам при вызове возвращать конкретный ответ. Это означает, что мы можем заставить функцию Execute использовать полностью имитированные реализации своих зависимостей, просто протестируйте функцию Execute, которая принимает и передает обратно соответствующие типы значений. Насмешливая установка:

/// <summary>
/// Unit tests for GetNumbersAndAddThem
/// </summary>
[TestClass]
[ExcludeFromCodeCoverage]
public class GetNumbersAndAddThemTests
{

  #region Private
  Mock<inumberfunctions> _mockNumberFunctions;
  Mock<idbgetsomenumbers> _mockIDbGetSomeNumbers;
  #endregion Private

  #region Public methods

  /// <summary>
  /// Setup mock objects
  /// </summary>
  [TestInitialize]
  public void Setup()
  {
      _mockNumberFunctions = new Mock<inumberfunctions>();
      _mockIDbGetSomeNumbers = new Mock<idbgetsomenumbers>();
  }

}

Поля _mockNumberFunctions и _mockIDbGetSomeNumbers настроены как фиктивный интерфейс. В `Setup` мы просто обновляем их. Теперь к хорошим частям, тестам с использованием макетов:

/// <summary>
/// Tests that GetNumbersAndAddThem.Execute gets numbers and then adds them.
/// </summary>
[TestMethod]
public void GetNumbersAndAddThem_Execute()
{
    // Arrange
    double[] numbersToUse = { 1, 2, 3, 4, 5 };
    double expected = numbersToUse.Sum();

    _mockIDbGetSomeNumbers.Setup(s => s.GetSomeNumbers()).Returns(numbersToUse);
    _mockNumberFunctions.Setup(s => s.AddNumbers(It.IsAny<double>())).Returns(expected);

    GetNumbersAndAddThem obj = new GetNumbersAndAddThem(_mockIDbGetSomeNumbers.Object, _mockNumberFunctions.Object);

    // Act
    var result = obj.Execute();

    // Assert
    Assert.AreEqual(expected, result);
}

В `_mockIDbGetSomeNumbers.Setup(…).Returns(…)` мы заявляем, что при вызове функции `GetSomeNumbers()` она должна возвращать `numbersToUse`. Довольно фантазии! Вместо того, чтобы полагаться на нашу конкретную реализацию `IDbGetSomeNumbers`, которая должна передаваться в базу данных, мы говорим ей использовать этот определенный список чисел, который был указан при настройке макета. Теперь мы можем с абсолютной уверенностью сказать, какова будет сумма чисел для использования, потому что мы знаем, какие числа будут предоставляться каждый раз, поскольку они не извлекаются из базы данных. Надеюсь, все это имеет смысл. Во всяком случае, для меня это имеет смысл! :О

В следующий раз я надеюсь заняться созданием и тестированием WCF.

Первоначально опубликовано на сайте kritner.blogspot.com 16 декабря 2015 г.