Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56
Вид материала | Тесты |
СодержаниеПример 3. Контроллер в виде интерфейса, а не класса. Decimal IncomeTax(Decimal rate, Decimal value) |
- Некорректные задания, 1276.79kb.
- К техническому регламенту, 835.7kb.
- Правительства Российской Федерации от 11 ноября 2005 г. N 679 о порядке разработки, 494.44kb.
- Постановлением Правительства Российской Федерации от 11 ноября 2005 г. N 679 о порядке, 652.85kb.
- Постановлением Правительства Российской Федерации от 11 ноября 2005 г. N 679 о порядке, 623.18kb.
- Правительства Российской Федерации от 11. 11. 2005 N 679 о порядке разработки и утверждения, 533.6kb.
- Постановления Правительства Российской Федерации от 11. 11. 2005 n 679 о порядке разработки, 613.63kb.
- Об утверждении требований к схемам теплоснабжения, порядку их разработки и утверждения, 450.79kb.
- Рабочая программа учебной дисциплины. Общие требования, порядок разработки, согласования, 414.77kb.
- Постановлением Правительства Российской Федерации от 11 ноября 2005 г. N 679 о порядке, 1924.26kb.
Пример 2
Реализуем по шаблону Мост операцию сложения двух чисел. Замыслом будет необходимость складывать числа, реализацией – сложение чисел определенного типа.
Интерфейс реализации:
public interface IMathematics
{
T Add(T param1, T param2);
T Reset();
}
Одна из реализаций:
internal class IntMathematicsImpl : IMathematics
{
public int Add(int param1, int param2)
{
checked
{
return param1 + param2;
}
}
}
Вопрос: почему не будет работать код:
internal class IntMathematicsImpl
{
public T Add(T param1, T param2)
{
checked
{
return param1 + param2;
}
}
public int Reset()
{
return 0;
}
}
Фабрика, создающая реализацию:
public class FactoryIMathematics
{
public static IMathematics
{
return new IntMathematicsImpl();
}
}
Одной из целей шаблона Мост является упрощение логики приложения. Интерфейс реализации используются вместе с другими типами для решения определенной задачи. Обычно местом совместного использования является некоторый класс, который называют Контроллером. Контроллер описывает логику приложения на абстрактном уровне. Важно то, что в контроллере только лишь выполняются некоторые действия, но не происходит создание объектов, участвующих в этих действиях.
internal class PropertyNotDefined : Exception
{
public PropertyNotDefined(string propIdentifier)
: base("Property " + propIdentifier + " is not defined")
{
}
}
public class Operations
{
private IMathematics
public IMathematics
{
get
{
if (_math == null)
{
throw new PropertyNotDefined("Operations.Math");
}
return _math;
}
set
{
_math = value;
}
}
public T AddArray(T[] numbers)
{
T total = this.Math.Reset();
foreach (T number in numbers)
{
total = this.Math.Add(total, number);
}
return total;
}
}
Итак, Operations – это контроллер, который использует переменную типа IMathematics, но в самом контроллере экземпляр не создается. Зато есть свойство Math, через которое и будет поставлен необходимый экземпляр. Контроллер сфокусирован на наличии операций, предоставляемых интерфейсом, он не знает о деталях их реализации.
Обратите внимание, что для создания пустого объекта используется метод Reset. Он создает аналог нуля. Что такое нуль, решает сам тип T.
Тест:
[TestMethod]
public void AddListDotNet2()
{
Operations
ops = new Operations
ops.Math = FactoryIMathematics.Instantiate();
int[] values = new int[] { 1, 2, 3, 4, 5 };
Assert.AreEqual(15, ops.AddArray(values), "List did not add");
}
Пример 3. Контроллер в виде интерфейса, а не класса.
В каком случае следует создать контроллер в виде интерфейса?
В тех случаях, когда понадобятся разновидности контроллеров.
Допустим, создается программа для работы с налогами:
namespace ITaxation
{
public interface IIncomes
{
}
public interface IDeductions
{
}
public interface ITaxation
{
IIncomes[] Incomes { get; set; }
IDeductions[] Deductions { get; set; }
Decimal CalculateTax();
}
}
Ключевыми в этой программе будут разнообразные параметры, используемые в налоговых алгоритмах (Incomes, Deductions и т.п.). Эти параметры можно было бы сделать параметрами метода CalculateTax, но мы сделаем их свойствами, потому что они могут понадобиться не только в одном методе и скорее всего таких методов будет несколько. Правило: если данные нужны более, чем одному методу, то сделайте их свойствами.
В отличие от предыдущего контроллера, в данном случае разновидностей контроллеров может быть несколько:
internal class SwissTaxes : ITaxation
{
public IIncomes[] Incomes
{
get { return null; }
set { ; }
}
public IDeductions[] Deductions
{
get { return null; }
set { ; }
}
public Decimal CalculateTax()
{
return new Decimal();
}
}
В каждом из контроллеров различия наверняка будут в методе CalculateTax.
Если начать реализовывать интерфейс ITaxation в каждом из производных классов, то появится дублирование во время реализации свойств. Чтобы этого избежать, введем абстрактный базовый класс:
public abstract class BaseTaxation : ITaxation
{
private IIncomes[] _incomes;
private IDeductions[] _deductions;
public IIncomes[] Incomes
{
get
{
if (_incomes == null)
{
throw new PropertyNotDefined("BaseTaxation.Incomes");
}
return _incomes;
}
set { _incomes = value; }
}
public IDeductions[] Deductions
{
get
{
if (_deductions == null)
{
throw new PropertyNotDefined("BaseTaxation.Deductions");
}
return _deductions;
}
set { _deductions = value; }
}
public abstract Decimal CalculateTax();
}
Кстати, таких абстрактных базовых классов может быть несколько.
Абстрактные классы хороши до тех пор, пока их не понадобится протестировать по отдельности. В этом случае не обойтись без создания mock-объектов.
public class MockNotImplemented : Exception
{
public MockNotImplemented()
: base("method not implemented")
{
}
}
public class MockBaseTaxation : BaseTaxation
{
public override Decimal CalculateTax()
{
throw new MockNotImplemented();
}
}
public class MockIncome : IIncomes
{
public void SampleMethod()
{
throw new MockNotImplemented();
}
}
Тесты:
[TestClass]
public class TaxTests
{
[TestMethod]
public void TestAssignIncomeProperty()
{
IIncomes[] inc = new IIncomes[1];
inc[0] = new MockIncome();
ITaxation taxation = new MockBaseTaxation();
taxation.Incomes = inc;
Assert.AreEqual(inc, taxation.Incomes, "Not same object");
}
[TestMethod]
[ExpectedException(typeof(PropertyNotDefined))]
public void TestRetrieveIncomeProperty()
{
ITaxation taxation = new MockBaseTaxation();
IIncomes[] inc = taxation.Incomes;
}
}
В случаях, когда абстрактный метод должен просто выполнить некоторую работу внутри себя, в mock-методе лучше всего сгенерировать исключение. Если же нужно проверить, что метод выполнил некоторую работу и повлиял на что-то вокруг себя, то нужна реализация по умолчанию. Например, если нужно проверить, что метод вызывается, то создаем такой mock-объект:
public class MockBaseTaxationRequiresCall : BaseTaxation
{
private bool _didCall;
public MockBaseTaxationRequiresCall()
{
_didCall = false;
}
public bool DidCall
{
get { return _didCall; }
}
public override Decimal CalculateTax()
{
_didCall = true;
return new Decimal();
}
}
и тест:
[TestMethod]
public void TestDidCallMethod()
{
MockBaseTaxationRequiresCall taxation = new MockBaseTaxationRequiresCall();
// Call some methods
Assert.IsTrue(taxation.DidCall);
}
Для базовых классов с реализаций по умолчанию важно написать отдельные тесты с использованием mock-объектов, т.к. иначе не получится проверить эту реализацию по умолчанию.
Продолжим развитие примера. Допустим при расчете налогов в некоторых реализациях необходимо проводить округление до ближайшего целого, а в некоторых до ближайших сотен. Понадобится метод, который будет производить это округление. Куда его поместить?
Можно его поместить в уже существующий интерфейс:
public interface ITaxation
{
IIncomes[] Incomes { get; set; }
IDeductions[] Deductions { get; set; }
Decimal IncomeTax(Decimal rate, Decimal value);
Decimal CalculateTax();
}
В этом случае и базовые классы должны создать некоторую реализацию. Однако, базовые классы были сфокусированы на алгоритмах, связанных с налогами, а не с математическими операциями.
Поэтому следующее решение – это не ограничиваться одним интерфейсом, а разделить функциональность по двум интерфейсам:
public interface ITaxation
{
IIncomes[] Incomes { get; set; }
IDeductions[] Deductions { get; set; }
ITaxMath[] TaxMath { get; set; }
Decimal CalculateTax();
}
public interface ITaxMath
{
Decimal IncomeTax(Decimal rate, Decimal value);
}
Теперь контроллер будет реализовывать оба интерфейса:
internal class SwissTaxes : ITaxation, ITaxMath
Но не следует применять интерфейсы без необходимости. Иногда оказывается полезным создать класс вместо интерфейса. Это можно сделать, если очевидна функциональность по умолчанию:
public class TaxMath
{
public virtual Decimal IncomeTax(Decimal rate, Decimal value)
{
return new Decimal();
}
}
public class TaxMathFactory
{
public static TaxMath Instantiate()
{
return new TaxMath();
}
}
public class SwissTaxMath : TaxMath
{
public override Decimal IncomeTax(Decimal rate, Decimal value)
{
return new Decimal();
}
}
public class SwissTaxMathFactory
{
public static TaxMath Instantiate()
{
return new SwissTaxMath();
}
}
Создавая классы вместо интерфейсов, не следует забывать указывать abstract. Лучше было сделать так, чтобы пользователь не мог случайно создать экземпляр класса, который для этого не предназначен:
public abstract class TaxMath
{
public virtual Decimal IncomeTax(Decimal rate, Decimal value)
{
return new Decimal();
}
}
internal class StubTaxMath : TaxMath
{
}
public class TaxMathFactory
{
public static TaxMath Instantiate()
{
return new StubTaxMath();
}
}
Вывод такой: если тип нацелен на повторное использование и узко специализирован, то его следует реализовать классом. Если нужны разнообразные реализации методов в производных типах, то класс нужно преобразовать в интерфейс. Если класс должен иметь некоторую реализацию по умолчанию, но при этом необходимо уточнение этой реализации в подклассах, то нужно использовать ключевые слова abstract и virtual.