Как я могу отделить дизайн системы от модульных тестов (как предложено дядей Бобом)? - PullRequest
0 голосов
/ 26 сентября 2018

Дядя Боб (Боб Мартин) упомянул в своем блоге , что для того, чтобы отделить дизайн нашей системы от модульных тестов, мы не должны подвергать наши конкретные классы непосредственно модульным тестам.Скорее, мы должны просто предоставить API, который представляет нашу систему, а затем использовать этот API для модульного тестирования.

Грубое представление предложения дяди Боба

Согласно моимпонимая, я думаю, что под API он имел в виду интерфейс .Поэтому модульные тесты должны взаимодействовать с интерфейсами, а не с реальными классами.

У меня такой вопрос: если мы предоставляем только интерфейсы для наших модульных тестов, как эти модульные тесты получают доступ к реальным реализациям для проверки ихповедение?Должны ли мы использовать DI в наших тестах для внедрения реальных классов во время выполнения?Есть ли способ, чтобы код ниже работал?

ILoanEligibility.cs

public interface ILoanEligibility
{
    bool HasCorrectType(string loanType);
}

LoanEligibility.cs

public class LoanEligibility : ILoanEligibility
{
    public bool HasCorrectType(string loanType)
    {
        if(loanType.Equals("Personal"))
        {
            return true;
        }
        return false;
    }
}

Модульный тест

[TestClass]
public class LoanEligibilityTest
{
    ILoanEligibility _loanEligibility;
    [TestMethod]
    public void TestLoanTypePersonal()
    {
        //Arrange
        string loanType = "Personal";

        //Act
        bool expected = _loanEligibility.HasCorrectType(loanType);

        //Assert
        Assert.IsTrue(expected);
    }
}

Вышеприведенный модульный тест пытается определить, правильно ли работает метод LoanEligibility.HasCorrectType () для типа «Персональный».Очевидно, что тест не пройден, так как мы используем не конкретный класс, а интерфейс, в соответствии с предложением дяди Боба (если я правильно понял).

Как мне пройти этот тест?Любые предложения будут полезны.

Редактировать 1 Спасибо, @bleepzter за предложение Moq.Ниже приведен модифицированный класс модульного тестирования, проверяющий как действительные, так и недействительные случаи.

[TestClass]
public class LoanEligibilityTest
{
    private Mock<ILoanEligibility> _loanEligibility;

    [TestMethod]
    public void TestLoanTypePersonal()
    {
        SetMockLoanEligibility();
        //Arrange
        string loanType = "Personal";
        //Act
        bool expected = _loanEligibility.Object.HasCorrectType(loanType);
        //Assert
        Assert.IsTrue(expected);
    }

    [TestMethod]
    public void TestLoanTypeInvalid()
    {
        SetMockLoanEligibility();
        //Arrange
        string loanType = "House";
        //Act
        bool expected = _loanEligibility.Object.HasCorrectType(loanType);
        //Assert
        Assert.IsFalse(expected);
    }

    public void SetMockLoanEligibility()
    {
        _loanEligibility = new Mock<ILoanEligibility>();
        _loanEligibility.Setup(loanElg => loanElg.HasCorrectType("Personal"))
                        .Returns(true);
    }
}

Но теперь я запутался.Поскольку мы не действительно тестируем наш конкретный класс, а скорее его имитацию, эти модульные тесты действительно говорят нам что-нибудь, кроме, вероятно, того, что наши макеты работают нормально?

Ответы [ 3 ]

0 голосов
/ 26 сентября 2018

Если мы предоставляем только интерфейсы нашим модульным тестам, как эти модульные тесты получают доступ к фактическим реализациям для проверки их поведения?

Любым способом, который вам нравится.

Один из подходов, который я нашел удовлетворительным, - написать проверки в абстрактном классе и передать экземпляр тестируемой системы из конструктора.в противном случае пустой класс, который расширяет абстрактный класс.

Во многих отношениях тестовые фреймворки ... ну ... "фреймворки" (очевидно) ... и поэтому может иметь смысл подуматьо ваших тестируемых компонентах как о чем-то, что нужно внедрить в фреймворк.См. Марк Сееманн (Mark Seemann), чтобы узнать, как может выглядеть DI-дружественный фреймворк , и решите, считаете ли вы, что эти идеи подходят для ваших тестовых наборов.

Вы можете сначала выполните тестирование в этом стиле, но я собираюсь признать, что некоторые шаги, которые разделяют проблемы, будут казаться немного надуманными - введение интерфейсов на ранней стадии, потому что вы действительно понимаете, какой API будет удобно использовать, возможно, сомнительно.

(Один из ответов может заключаться в том, чтобы потратить время на всплески интерфейса, прежде чем вкладывать средства в написание проверок для реализации.)

0 голосов
/ 29 сентября 2018

Скорее, мы должны просто предоставить API, представляющий нашу систему, а затем использовать этот API для модульного тестирования.

Исправить

Согласно моимпонимая, я думаю, что под API он подразумевал интерфейс.Таким образом, модульные тесты должны взаимодействовать с интерфейсами, а не с реальными классами.

Здесь вы неправильно поняли первое утверждение.
Сначала в модульных тестах вам нужно протестировать фактическую реализацию, чтобы проверить их поведение.
Затем в модульных тестах вы создадите экземпляры реальных классов , но разрешите использовать только те методы и типы, у которых потребитель вашего API имеет доступ к .

В вашем конкретном примере

[TestClass]
public class LoanEligibilityTest
{        
    [TestMethod]
    public void TestLoanTypePersonal()
    {
        //Arrange
        ILoanEligibility loanEligibility = new LoanEligibility(); // actual implementation
        string loanType = "Personal";

        //Act
        bool expected = _loanEligibility.HasCorrectType(loanType);

        //Assert
        Assert.IsTrue(expected);
    }
}

Предложение: с подходом Arrange-Act-Assert в разделе «Act» вы разрешаете использовать только методы и типы, предоставляемые API.

0 голосов
/ 26 сентября 2018

Чтобы ответить на ваш вопрос - вы бы использовали фальшивый фреймворк, такой как Moq.

Общая идея состоит в том, что Интерфейсы или абстрактные классы предоставляют "контракты" или набор стандартизированных API, которые вы можете кодировать.*

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

Однако сложность возникает, когда эти реализации являются зависимостями других объектов.В связи с этим - для модульного тестирования такого сложного объекта сначала необходимо создать реализацию зависимости, включить эту зависимость в экземпляр того, что вы тестируете.

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

То, что предоставляет макет, - это способ «подделать» реализацию с конкретными параметрами (ввод / вывод).какими бы они ни были) и вставьте эти фальшивки в граф зависимостей.И хотя да - вы можете смоделировать конкретные объекты - гораздо проще смоделировать контракты, определенные интерфейсом или абстрактным классом.

Достойной отправной точкой для понимания этих концепций является документация по инфраструктуре moq.https://github.com/Moq/moq4/wiki/Quickstart

Редактировать:

Я вижу, что существует путаница в том, что это значит, поэтому я хотел уточнить.

Общие шаблоны проектирования (известные как SOLID) диктуют, чтообъект должен делать 1 вещь и только 1 вещь и делать это хорошо.Это известно как принцип единой ответственности.

Другая основная концепция заключается в том, что объект должен зависеть от абстракций, а не от конкретных реализаций.Эта концепция известна как принцип обращения зависимостей.

Наконец - принцип подстановки Лискова, который гласит, что объект в программе должен быть заменен экземплярами их подтипов без изменения правильности программы.Другими словами - если ваши объекты зависят от абстракций, то вы можете обеспечить различные реализации (используя преимущества наследования) для этих абстракций без существенного изменения поведения приложения.

, которое также аккуратно переходит в Open / Closedпринцип.IE - программные объекты должны быть открыты для расширения, но закрыты для модификации.(Подумайте о предоставлении различных реализаций для этих абстракций.)

Наконец - у нас есть принцип инверсии управления - сложный объект не должен отвечать за создание своих собственных зависимостей;что-то еще должно отвечать за их создание, и они должны быть «внедрены» через конструктор, метод или внедрение свойства, где бы они ни были нужны.

Так как это применимо в «отделении системного дизайна» отмодульные тесты?

Ответ очень прост.

Предположим, мы пишем программное обеспечение, которое моделирует автомобили.Автомобиль имеет кузов и колеса, а также всевозможные другие внутренние компоненты.Для простоты скажем, что объект типа Car имеет конструктор, который принимает четыре wheel объекта в качестве параметров:

public class Wheel {
   public double Radius { get; set; }
   public double RPM { get; set; }
   public void Spin(){ ... }
   public double GetLinearVelocity() { ... }
}

public class LinearMovement{
   public double Velocity { get; set; }     
}

public class Car {

  private Wheel wheelOne;
  private Wheel wheelTwo;
  private Wheel wheelThree;
  private Wheel wheelFour;

  public Car(Wheel one, Wheel two, Wheel three, Wheel four){
    wheelOne = one;
    wheelTwo = two;
    wheelThree = three;
    wheelFour = four;
  } 

  public LinearMovement Move(){
    wheelOne.Spin();
    wheelTwo.Spin();
    wheelThree.Spin();
    wheelFour.Spin();

    speedOne = wheelOne.GetLinearVelocity();
    speedTwo = wheelTwo.GetLinearVelocity();
    speedThree = wheelThree.GetLinearVelocity();
    speedFour = wheelFour.GetLinearVelocity();

    return new LinearMovement(){ 
       Velocity = (speedOne + speedTwo + speedThree + speedFour) / 4
    };
  }
}

Способность автомобиля двигаться определяется типом колес автомобиля.есть.Колесо может иметь мягкую резину, тем самым приклеивая автомобиль к дороге по поворотам, или оно может быть очень узким для глубокого снега, но очень медленных скоростей.

Поэтому - идея колеса становится абстракцией.Есть все виды колес, и конкретная реализация колеса не может охватить все из них.Введите принцип инверсии зависимости.

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

public interface IWheel {
    double Radius { get; set; }
    double RPM { get; set; }
    void Spin();
    double GetLinearVelocity();
}

public class BasicWheel : IWheel {
   public double Radius { get; set; }
   public double RPM { get; set; }
   public void Spin(){ ... }
   public double GetLinearVelocity() { ... }   
}

public class Car {
    ...
    public Car(IWheel one, IWheel two, IWheel three, IWheel four){
    ...
    } 

    public LinearMovement Move(){
        wheelOne.Spin();
        wheelTwo.Spin();
        wheelThree.Spin();
        wheelFour.Spin();

        speedOne = wheelOne.GetLinearVelocity();
        speedTwo = wheelTwo.GetLinearVelocity();
        speedThree = wheelThree.GetLinearVelocity();
        speedFour = wheelFour.GetLinearVelocity();

        return new LinearMovement(){ 
            Velocity = (speedOne + speedTwo + speedThree + speedFour) / 4
        };
    }
}

Так что это здорово, у нас есть абстракция для определения базовой функциональности колеса, и мы закодировали машину против этой абстракции.Ничего не изменилось в коде того, как машина движется - тем самым удовлетворяя принципу замены Лискова.

Так что теперь, если вместо создания автомобиля с базовыми колесами мы создадим автомобиль с помощью RacingPerformanceWheels, код, управляющий движением автомобиля, останется прежним.Это удовлетворяет принципу Open / Closed.

Однако - это создает другую проблему.Фактическая скорость автомобиля - зависит от средней линейной скорости всех 4 колес.Таким образом, в зависимости от колеса - автомобиль будет вести себя по-разному.

Как мы можем проверить поведение автомобиля, учитывая, что там может быть миллион различных типов колес?!?

Введите рамки для насмешек.Поскольку движение автомобиля зависит от абстрактного понятия колеса, определенного в интерфейсе IWheel, теперь мы можем смоделировать различные варианты реализации такого колеса, каждое из которых имеет предопределенные параметры.

Сами бетонные реализации / объекты (BasicWheel, RacingPerformanceWheel и т. д.) должны проходить модульное тестирование без макетов. Причина в том, что они не имеют собственных зависимостей.Если колесо имеет зависимость в своем конструкторе - тогда для этой зависимости следует использовать mocking.

Для проверки объекта car - mock следует использовать для описания каждого экземпляра IWheel (зависимость), который передается конструктору автомобиля. Это дает пару преимуществ - отсоединение общей конструкции системы от юнит-тестов:

1) Нам все равно, какие есть колесав системе.Их может быть 1 миллион.

2) Мы заботимся о том, чтобы для конкретных размеров колес при заданной угловой скорости (об / мин) автомобиль достигал очень определенной линейной скорости.

Макет IWheel для требований№ 2 скажет нам, если наш автомобиль работает нормально, а если нет - мы могли бы изменить наш код, чтобы исправить ошибку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...