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