Внедрение зависимости и ее связь с автоматизированным тестированием на примере - PullRequest
3 голосов
/ 01 октября 2011

Через SO я нашел путь к этой странице: http://www.blackwasp.co.uk/DependencyInjection.aspx

Там они предоставляют фрагмент кода C # для использования в качестве примера кода, который может извлечь выгоду из внедрения зависимостей:

public class PaymentTerms
{
    PaymentCalculator _calculator = new PaymentCalculator();

    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }

    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}


public class PaymentCalculator
{
    public decimal GetMonthlyPayment(decimal Price, decimal Deposit, int Years)
    {
        decimal total = Price * (1 + Years * 0.1M);
        decimal monthly = (total - Deposit) / (Years * 12);
        return Math.Round(monthly, 2, MidpointRounding.AwayFromZero);
    }
}

Они также включают эту цитату:

Одной из ключевых проблем с приведенным выше кодом является создание объекта PaymentCalculator из класса PaymentTerms.Поскольку зависимость инициализируется внутри содержащего класса, два класса тесно связаны.Если в будущем потребуется несколько типов калькулятора платежей, их будет невозможно интегрировать без изменения класса PaymentTerms. Точно так же, если вы хотите использовать другой объект во время автоматического тестирования для изоляции тестирования класса PaymentTerms, это не может быть введено.


Мой вопрос касается утвержденияжирным шрифтом:

  • Автор имел в виду модульное тестирование или что-то не хватает в автоматическом тестировании?
  • Если автор намеревался написать автоматическое тестирование, как бымодифицировать этот класс, чтобы использовать вспомогательное средство для ввода зависимостей в процессе автоматического тестирования?
  • В любом случае это применимо только при наличии нескольких типов калькуляторов платежей?
  • Если да, то обычно ли этоСтоит ли внедрять DI с самого начала, даже не зная, какие требования изменятся в будущем?Очевидно, что это требует некоторой осмотрительности, которую можно извлечь из опыта, поэтому я просто пытаюсь получить базовый уровень, на котором можно строить.

Ответы [ 3 ]

4 голосов
/ 01 октября 2011

Автор действительно имел в виду Unit Testing или есть что-то про автоматизированное тестирование, которое мне не хватает?

Я прочитал это, чтобы иметь в виду модульное тестирование. Вы можете запускать модульные тесты вручную или в автоматическом режиме, если у вас есть непрерывный процесс интеграции / сборки.

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

Модификация поможет всем испытаниям, автоматизированным или нет.

В любом случае это применимо только в случае нескольких типов платежных калькуляторов?

Это также может пригодиться, если ваш внедренный класс основан на интерфейсе, и вы хотите ввести прокси без необходимости изменения клиентского кода.

Если это так, то стоит ли применять DI с самого начала, даже без знания требований, которые меняются в будущем? Очевидно, что это требует некоторого усмотрения, который будет изучен через опыт, поэтому я просто пытаюсь получить базовый уровень, на котором можно строить.

Это может помочь с самого начала, если у вас есть некоторое понимание того, как это работает и для чего это хорошо.

Преимущество есть, даже если требования не меняются. Ваши приложения будут лучше наслоены и будут основаны на интерфейсах для не значащих объектов (неизменяемые объекты, такие как Address и Phone, которые являются просто данными и не изменяются). Это обе передовые практики, независимо от того, используете ли вы механизм DI или нет.

ОБНОВЛЕНИЕ: Вот еще немного о преимуществах дизайна на основе интерфейса и неизменяемых объектов значения.

Объект значения является неизменным: после его создания вы не меняете его значение. Это означает, что это по сути потокобезопасно. Вы можете поделиться им в любом месте вашего приложения. Примерами могут служить примитивные оболочки Java (например, java.lang.Integer, класс Money и т. Д.)

Допустим, вам нужен человек для вашего приложения. Вы можете сделать его неизменным значением:

package model; 

public class Person {
    private final String first; 
    private final String last;

    public Person(String first, String last) {
        this.first = first;
        this.last = last;
    }

    // getters, setters, equals, hashCode, and toString follow
}

Вы хотели бы сохранить Person, поэтому вам потребуется объект доступа к данным (DAO) для выполнения операций CRUD. Начните с интерфейса, потому что реализации могут зависеть от того, как вы решите сохранить объекты.

package persistence;

public interface PersonDao {
    List<Person> find();
    Person find(Long id);
    Long save(Person p);    
    void update(Person p);
    void delete(Person p);
}

Вы можете попросить механизм DI внедрить конкретную реализацию для этого интерфейса в любую службу, которая должна сохранять экземпляры Person.

Что если вы хотите транзакции? Легко. Вы можете использовать аспект, чтобы посоветовать ваши методы обслуживания. Одним из способов обработки транзакций является использование «throws advice» для открытия транзакции при входе в метод и либо фиксации после, если он успешен, либо откат ее, если она выдает исключение. Код клиента не должен знать, что есть аспект, обрабатывающий транзакции; все, что он знает, это интерфейс DAO.

3 голосов
/ 02 октября 2011

Автор статьи BlackWasp означает автоматическое модульное тестирование - это было бы ясно, если бы вы перешли по его ссылке automated testing, которая ведет на страницу под названием " Создание модульных тестов ", которая начинается " В третьей части учебника «Автоматическое модульное тестирование» рассматриваются ...".

Сторонники юнит-тестирования обычно любят инъекцию зависимостей, потому что она позволяет им видеть то, что они тестируют. Таким образом, если вы знаете, что PaymentTerms.GetMonthlyPayment() должен вызывать PaymentCalculator.GetMonthlyPayment() для выполнения вычисления, вы можете заменить калькулятор одной из ваших собственных конструкций, которая позволяет вам увидеть, что он действительно был вызван. Не потому, что вы хотите изменить расчет m=((p*(1+y*.1))-d)/(y*12) на 5, а потому, что приложение, которое использует PaymentTerms, может когда-нибудь захотеть изменить способ расчета платежа, и поэтому тестер хочет убедиться, что калькулятор действительно вызывается .

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

Существует совершенно другая дискуссия о том, является ли Dependency Injection хорошей или плохой вещью с точки зрения дизайна и программирования. Но вы не просили об этом, и я не собираюсь бросать ручные гранаты в этом вопросе.

Вы также спросили в комментарии " Это суть того, что я пытаюсь понять. Часть, с которой я все еще борюсь, - это почему это должен быть FakePaymentCalculator? Почему бы просто не создать экземпляр реального, законного PaymentCalculator и теста с этим?", и ответ на самом деле очень прост: нет никаких оснований для этого для этого примера , потому что объект фальсифицируется (« поддельный ») это более распространенный термин) очень легкий и простой. Но представьте, что объект PaymentCalculator каким-то образом сохранил свои правила вычислений в базе данных и что правила могут различаться в зависимости от того, когда выполнялись вычисления, или от продолжительности ссуды и т. Д. . Модульное тестирование теперь требует установки сервера базы данных, создания его схемы, заполнения его правил, и т. Д. . Для такого более реалистичного примера наличие FakePaymentCalculator() может иметь значение между тестом, который вы запускаете каждый раз, когда вы компилируете код, и тестом, который вы запускаете настолько редко, насколько это возможно.

2 голосов
/ 01 октября 2011

Если автор DID намеревался написать автоматизированное тестирование, как бы изменил этот класс для использования средства добавления зависимостей в процессе автоматического тестирования?

Одним из самых больших преимуществ было быбыть в состоянии заменить PaymentCalculator реализацией макет / фальшивка во время теста.

Если PaymentTerms был реализован следующим образом:

public class PaymentTerms
{
    IPaymentCalculator _calculator;

    public PaymentTerms(IPaymentCalculator calculator)
    {
         this._calculator = calculator;
    }

    ...
}

(где IPaymentCalculator - это интерфейс, объявляющий сервисы PaymentCalculatorкласс.) Таким образом, в модульном тесте вы сможете сделать это:

IPaymentCalculator fakeCalculator = new FakePaymentCalculator()
PaymentTerms paymentTerms = new PaymentTerms(fakeCalculator);
// Test the behaviour of PaymentTerms, which uses a fake in the test.

С типом PaymentCalculator, жестко закодированным в PaymentTerms, не будет никакого способа сделать это.

ОБНОВЛЕНИЕ: Вы спросили в комментарии:

Гипотетически говоря, если бы у класса PaymentCalculator были некоторые свойства экземпляра, то человек, разрабатывающий модульный тест, вероятно, создал бы класс FakePaymentCalculator с конструктором, который всегда использовал одни и те же значениядля свойств экземпляра, верно?Так как же тогда тестируются перестановки?Или идея в том, что модульный тест для PaymentTerms заполняет свойства FakePaymentCalculator и тестирует несколько перестановок?

Не думаю, что вам нужно проверять какие-либо перестановки.В этом конкретном случае единственной задачей PaymentTerms.GetMonthlyPaymend() является вызов _calculator.GetMonthlyPayment() с указанными параметрами.И это единственное, что вам нужно для модульного тестирования, когда вы пишете модульный тест для этого метода.Например, вы можете сделать следующее:

public class FakePaymentCalculator
{
    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }

    public void GetMonthlyPayment(decimal price, decimal deposit, int years)
    {
        this.Price = price;
        this.Deposit = deposit;
        this.Years = years;
    }
}

И в модульном тесте вы можете сделать это:

IPaymentCalculator fakeCalculator = new FakePaymentCalculator()
PaymentTerms paymentTerms = new PaymentTerms(fakeCalculator);

// Calling the method which we are testing.
paymentTerms.GetMonthlyPayment(1, 2, 3);

// Check if the appropriate method of the calculator has been called with the correct parameters.
Assert.AreEqual(1, fakeCalculator.Price);
Assert.AreEqual(2, fakeCalculator.Deposit);
Assert.AreEqual(3, fakeCalculator.Years);

Таким образом, мы проверили единственное, что является ответственностьюPaymentTerms.GetMonthlyPayment(), который вызывает метод GetMonthlyPayment() калькулятора.Однако для такого рода тестов использование mock было бы намного проще, чем реализация собственной fake .Если вам интересно, я рекомендую вам попробовать Moq , который является действительно простой, но полезной библиотекой Mock для .NET.

...