Sergey Teplyakov

advertisement
Design for Testability
Mocks, Stubs, Refactoring
by Sergey Teplyakov, @STeplyakov
Что не так с
дизайном наших
систем?
Что приводит к плохому
дизайну?
• Ошибки на начальных этапах?
•
•
•
•
Недопонимание требований
Жесткая архитектура
Предварительное обобщение
…
• Постоянные изменения требований?
Что такое «плохой
дизайн»?
«Главный» критерий
плохого дизайна
«А вот я бы сделал это не так!»
That’s not the way I would have done it,
TNTWIWHDI
Критерии плохого дизайна
• Жесткость (Rigidity)
• Хрупкость (Fragility)
• Неподвижность (Immobility)
Юнит-тесты, как лакмусовая
бумажка хорошего дизайна
ServiceLocator
+ Get<T>() : T
Configuration
+Instance: Configuration
Database
+GetEmployee(): Employee
И как этого зверя
тестировать?
ClassUnderTest
<<Interface>>
<<Interface>>
<<Interface>>
ILogger
IServiceProxy
IViewModelManager
+LogError(error) : void
+Compute(Data) : void
+Show(ViewModel) : void
«Предусловия» юнит тестов
• Требуется ясный «контракт» класса
• Четкий «вход»
• Четкий «выход»
• Минимальное количество связей
Test Doubles: Stubs & Mocks
• Стабы - эмулируют состояние
• Моки - проверяют поведение
Пример стаб-объекта
<<Interface>>
Logger
ILogConfigurator
+GetConfig() : Config
Возвращает
"поддельный" конфиг
LogConfiguratorStub
+GetConfig(): Config
// Добавляем в поддельную конфигурацию из 3-х аппендеров
int appenders = 3;
var stub = new LogConfiguratorStub(new Config(appenders));
var logger = new Logger(stub);
// Проверяем, что логгер сконфигурирован корректно
Assert.That(
logger.GetAppenders().Count, Is.EqualTo(appenders));
Пример мок-объекта
<<Interface>>
Logger
ILogAppender
+Write(message) : Void
Запоминает
информацию о
вызовах
LogAppenderMock
+Write(message) : Void
+WritenMessage: String
// Arrange
var mock = new LogAppenderMock();
var logger = new Logger(mock);
// Act
logger.Write("Msg");
// Assert
Assert.That(mock.WrittenMessage, Is.Not.Null);
Юнит тесты – не
серебряная пуля!
Наивная реализация модуля
расчета заработной платы
«Плохой» дизайн
CheckWriter
+ WriteCheck() : void
Payroll
1
1
1
+ PayEmployees() : void
1
1
EmployeeDatabase
+ GetEmployee() : Employee
+ PutEmployee(e: Employee)
1
Employee
+ CalculatePay() : Money
+ PostPayment(m: Money)
Выделяем интерфейсы!
<<Interface>>
<<Interface>>
Payroll
ICheckWriter
1
+WriteCheck() : void
1
1
+ PayEmployees() : void
1
IEmployee
+ CalculatePay() : Money
+ PostPayment(m: Money)
1
1
CheckWriter
+ WriteCheck() : void
<<Interface>>
IEmployeeDatabase
+ GetEmployee() : Employee
+ PutEmployee(e: Employee)
EmployeeDatabase
+ GetEmployee() : Employee
+ PutEmployee(e: Employee)
Employee
+ CalculatePay() : Money
+ PostPayment(m: Money)
Теперь дизайн
тестируемый!
[Test]
public void TestPayroll()
{
MockEmployeeDatabase db = new MockEmployeeDatabase();
MockCheckWriter w = new MockCheckWriter();
Payroll p = new Payroll(db, w);
p.PayEmployees();
Assert.IsTrue(w.ChecksWereWrittenCorrectly());
Assert.IsTrue(db.PaymentsWerePostedCorrectly());
}
Стал ли дизайн лучше?
• Дизайн не изменился!
• Груда кода в каждом тесте (*)
• Сложность проверки граничных условий
• Динамическая типизация != хороший
дизайн!
Альтернативный подход
• Нужно ли выделять интерфейс для
CheckWriter-а?
• Нужно ли выделять интерфейс для
EmployeeDatabase?
• Нет ли скрытых абстракций?
• Нужен ли IEmployee?
• Не делает ли Employee слишком много?
Альтернативный подход
Проверяем
Переносим
интеграционными
логику из Payroll
тестами
Payroll
+ PayEmployees()
1
1
1
Payroller
EmployeeDatabase
+ PayEmployee(Employee)
+ CalculatePayment(Money)
+ GetEmployee() : Employee
+ PutEmployee(e: Employee)
1
1
1
Employee
+ PostPayment(m: Money)
Убираем метод
CalculatePayment
<<Interface>>
ICheckWriter
+WriteCheck() : void
CheckWriter
+ WriteCheck() : void
Идеальный дизайн для
тестирования
Метод Calculate без
побочных эффектов!
PaymentCalculator
Argument
+Calculate(PaymentInfo) : Money
PaymentInfo
+ WorkScheduler: Scheduler
Result
Money
+ Value: Decimal
[TestCaseSource("GetPaymentInfo")]
public void Test_Payment_Information(PaymentInfo pi, Money expectedPayment)
{
// Arrange
var calculator = new PaymentCalculator();
// Act
var actualPayment = calculator.Calculate(pi);
// Assert
Assert.That(expectedPayment, Is.EqualTo(actualPayment));
}
А в чем разница?
• Отделение инфраструктуры от логики
• Уменьшение связанности
• Возможность повторного
использования
• Простота тестов
Дизайн и борьба со
сложностью
Любая сложная система строится на
основе проверенных модулей более
низкого уровня.
Гради Буч
Аксиома управления
зависимостями
The more complex a class or component is,
the more decoupled it should be.
Ted Faison – Event-Based Programming
Слепое стремление к
тестируемости ведет к …
• нарушению инкапсуляции;
• проблемам сопровождения;
• неявной связности;
Важное следствие …
AS ARULEOF THUMB
Хороший дизайн == тестируемый дизайн
Тестируемый дизайн != хороший дизайн
Design for Testability…
От тестируемости к хорошему дизайну
От хорошего дизайна к тестируемости
Вопросы?
Design for
Testability:
Mocks, Stubs,
Refactoring
Sergey Teplyakov
Visual C# MVP
SergeyTeplyakov.blogspot.com
Заповни Анкету
Виграй Приз
http://anketa.msswit.in.ua
Download