Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Пример 3. Контроллер в виде интерфейса, а не класса.
Decimal IncomeTax(Decimal rate, Decimal value)
Подобный материал:
1   ...   39   40   41   42   43   44   45   46   47

Пример 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 : IMathematics

{

public T Add(T param1, T param2)

{

checked

{

return param1 + param2;

}

}


public int Reset()

{

return 0;

}

}

Фабрика, создающая реализацию:

public class FactoryIMathematics

{

public static IMathematics Instantiate()

{

return new IntMathematicsImpl();

}

}

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

internal class PropertyNotDefined : Exception

{

public PropertyNotDefined(string propIdentifier)

: base("Property " + propIdentifier + " is not defined")

{

}

}


public class Operations

{

private IMathematics _math;


public IMathematics Math

{

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.