Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Использование юнит-тестов для повышения качества разработки

5,496 views

Published on

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

Published in: Technology
  • Be the first to comment

Использование юнит-тестов для повышения качества разработки

  1. 1. Использование юнит-тестов для повышения качества разработки cppconf.ru - 2017 Ястребов Виктор Разработчик компании “Тензор”
  2. 2. 1. Характеристики хорошего юнит-теста 2. Подходы к созданию тестируемого кода 3. Виды поддельных объектов 4. Пример тестирования класса 5. Приемы создания хороших юнит-тестов О чем будем говорить 2/43
  3. 3. Допущения • Код упрощен •Используется Google Test https://github.com/google/googletest/tree/master/googletest 3/43
  4. 4. Внешняя зависимость Взаимодействие есть, а контроля нет 4/43 Жесткий диск, время, база данных
  5. 5. Интеграционный тест VS юнит-тест Полный контроль над внешними зависимостями Интеграционный тест Юнит тест нет да 5/43
  6. 6. Признаки хорошего юнит-теста Полный контроль над внешними зависимостями Автоматизация запуска Результат • стабилен • повторим • независим • Малое время выполнения • Простота чтения 6/43
  7. 7. Принцип наименования юнит-теста Sum_ByDefault_ReturnsZero() Sum_WhenCalled_CallsTheLogger() [ИмяТестируемойРабочейЕденицы]_ [СценарийТеста]_ [ОжидаемыйРезультат] 7/43
  8. 8. TEST_F( CalculatorTest, Sum_ByDefault_ReturnsZero() ) { Calculator calc; int last_sum = calc.Sum(); ASSERT_EQ( 0, last_sum ); } Структура юнит-теста 1. Arrange 2. Act 3. Assert 8/43
  9. 9. Рабочие единицы тестируемого кода • Возвращаемый результат • Изменение состояния системы • Взаимодействие между объектами 9/43
  10. 10. Рабочие единицы тестируемого кода • Возвращаемый результат • Изменение состояния системы Задача распознавания Задача разделения Разрыв зависимости • Взаимодействие между объектами 10/43
  11. 11. Поддельные объекты (Fakes)
  12. 12. Поддельные реализации объектов Fake-объект Stub-объект Задача разделения Mock-объект Задача распознавания 12/43
  13. 13. Stub-объект Взаимоде йствие Тестовый код Тестируемый код Stub Взаимодействие 13/43
  14. 14. Stub-объект Взаимоде йствие Тестовый код Тестируемый код Mock Взаимодействие 14/43
  15. 15. Разбор на примере
  16. 16. Исходный код class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; 16/43
  17. 17. Исходный код class WebService { public: void LogError( std::string msg ) { /* логика, включающая работу с сетевым соединением*/ } }; class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; 17/43
  18. 18. Исходный код class WebService { public: void LogError( std::string msg ) { /* логика, включающая работу с сетевым соединением*/ } }; class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; class DatabaseManager { public: bool IsValid( std::string ename ) { /* логика, включающая операции чтения из базы данных*/ } }; 18/43
  19. 19. Исходный код class WebService { public: void LogError( std::string msg ) { /* логика, включающая работу с сетевым соединением*/ } }; class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; 2. Внешняя зависимость 1. Внешняя зависимость class DatabaseManager { public: bool IsValid( std::string ename ) { /* логика, включающая операции чтения из базы данных*/ } }; 19/43
  20. 20. Разрыв зависимости от базы данных База данных IsValid( std::string ename ) IDatabaseManager FakeDatabaseManager DatabaseManager 20/43
  21. 21. class DatabaseManager : public IDatabaseManager { public: bool IsValid( std::string ename ) override { /* сложная логика, включающая операции чтения из базы данных*/ } }; class IDatabaseManager { public: virtual bool IsValid( std::string ename ) = 0; virtual ~IDatabaseManager() = default; }; class FakeDatabaseManager : public IDatabaseManager { public: bool WillBeValid; FakeDatabaseManager( bool will_be_valid ) : WillBeValid( will_be_valid ) { } bool IsValid( std::string ename ) override { return WillBeValid; } }; Разрыв зависимости от базы данных 21/43
  22. 22. Использование stub для разрыва зависимости Параметризация конструктора. Вариант 1
  23. 23. Вместо конкретной реализации – интерфейс class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; class EntryAnalyzer { public: EntryAnalyzer() : pDbManager( std::make_unique<DatabaseManager>() ) { } bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == pDbManager->IsValid( ename ) ) { return false; } return true; } private: std::unique_ptr<IDatabaseManager> pDbManager; WebService webService; }; 23/43
  24. 24. Внедрение зависимости class EntryAnalyzer { public: EntryAnalyzer( std::unique_ptr<IDatabaseManager> &&p_db_mng ) : pDbManager( std::move( p_db_mng ) ) { } bool Analyze( std::string ename ) { ... if( false == pDbManager->IsValid( ename ) ) return false; return true; } private: std::unique_ptr<IDatabaseManager> pDbManager; ... }; внедрение зависимости 24/43
  25. 25. class EntryAnalyzer { public: EntryAnalyzer() : pDbManager( std::make_unique<DatabaseManager>() ) { } bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == pDbManager->IsValid( ename ) ) return false; return true; } private: std::unique_ptr<IDatabaseManager> pDbManager; WebService webService; }; class EntryAnalyzer { public: EntryAnalyzer() : pDbManager( std::make_unique<DatabaseManager>() ) { } EntryAnalyzer( std:: unique_ptr<IDatabaseManager> &&p_mng ) : pDbManager( std::move( p_mng ) ) { } bool Analyze( std::string ename ) { ... } private: std::unique_ptr<IDatabaseManager> pDbManager; WebService webService; }; 25/43 Внедрение зависимости
  26. 26. Тестирование возвращаемого значения class FakeDatabaseManager : public IDatabaseManager { public: bool WillBeValid; FakeDatabaseManager( bool will_be_valid ) : WillBeValid( will_be_valid ) { } bool IsValid( std::string ename ) override { return WillBeValid; } }; TEST_F( EntryAnalyzerTest, Analyze_ValidEntryName_ReturnsTrue ) { EntryAnalyzer ea( std::make_unique<FakeDatabaseManager>( true ) ); bool result = ea.Analyze( "valid_entry_name" ); ASSERT_EQ( result, true ); } 26/43
  27. 27. Использование stub для разрыва зависимости Параметризация конструктора. Вариант 2
  28. 28. class EntryAnalyzer { public: EntryAnalyzer() : pDbManager( DbMngFactory::Create() ) { } bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == pDbManager->IsValid( ename ) ) return false; return true; } private: std::unique_ptr<IDatabaseManager> pDbManager; WebService webService; }; Использование фабрики class EntryAnalyzer { public: EntryAnalyzer() : pDbManager( std::make_unique<DatabaseManager>() ) { } bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == pDbManager->IsValid( ename ) ) return false; return true; } private: std::unique_ptr<IDatabaseManager> pDbManager; WebService webService; }; 28/43
  29. 29. Тестирование возвращаемого значения class DbMngFactory { public: static std::unique_ptr<IDatabaseManager> Create() { if( nullptr == pDbMng ) return std::make_unique<DatabaseManager>(); return std::move( pDbMng ); } static void SetManager( std::unique_ptr<IDatabaseManager> &&p_mng ) { pDbMng = std::move( p_mng ); } private: static std::unique_ptr<IDatabaseManager> pDbMng; }; TEST_F( EntryAnalyzerTest, Analyze_ValidEntryName_ReturnsTrue ) { DbMngFactory::SetManager( std::make_unique<FakeDatabaseManager>( true ) ); EntryAnalyzer ea; bool result = ea.Analyze( "valid_entry_name" ); ASSERT_EQ( result, true ); } 29/43
  30. 30. Использование stub для разрыва зависимости “Выделить и переопределить”
  31. 31. Выделение зависимости class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; class EntryAnalyzer { public: bool Analyze( std::string ename ) { ... if( false == IsValid( ename ) ) return false; return true; } protected: bool IsValid( std::string ename ) { return dbManager.IsValid( ename ); } private: DatabaseManager dbManager; ... }; 31/43
  32. 32. Переопределение зависимости class TestingEntryAnalyzer : public EntryAnalyzer { public: bool WillBeValid; private: bool IsValid( std::string ename ) override { return WillBeValid; } }; наследование внедрение зависимости class EntryAnalyzer { public: bool Analyze( std::string ename ) { ... if( false == IsValid( ename ) ) return false; return true; } protected: virtual bool IsValid( std::string ename ) { return dbManager.EntryIsValid( ename ); } private: DatabaseManager dbManager; ... }; тестируемый класс 32/43
  33. 33. Тестирование возвращаемого значения TEST_F( EntryAnalyzerTest, Analyze_ValidEntryName_ReturnsTrue) { TestingEntryAnalyzer ea; ea.WillBeValid = true; bool result = ea.Analyze( "valid_entry_name" ); ASSERT_EQ( result, true ); } class TestingEntryAnalyzer : public EntryAnalyzer { public: bool WillBeValid; private: bool IsValid( std::string ename ) override { return WillBeValid; } }; 33/43
  34. 34. Использование Mock для разрыва зависимости
  35. 35. Выделение зависимости class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { webService.LogError( "Error: " + ename ); return false; } if( false == dbManager.IsValid( ename ) ) return false; return true; } private: DatabaseManager dbManager; WebService webService; }; class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { LogError( "Error: " + ename); return false; } ... } protected: virtual void LogError( std::string err ) { webService.LogError( err ); } private: ... WebService webService; }; 35/43
  36. 36. Переопределение зависимости class EntryAnalyzer { public: bool Analyze( std::string ename ) { if( ename.size() < 2 ) { LogError( "Error: " + ename); return false; } ... } protected: virtual void LogError( std::string err ) { webService.LogError( err ); } private: DatabaseManager dbManager; WebService webService; }; class TestingEntryAnalyzer : public EntryAnalyzer { public: TestingEntryAnalyzer( std::shared_ptr<IWebService> p_service ) : pWebService( p_service ) { } private: void LogError( std::string err ) override { pWebService->LogError( err ); } std::shared_ptr<IWebService> pWebService; }; 36/43
  37. 37. Тестирование взаимодействия TEST_F( EntryAnalyzerTest, Analyze_TooShortEntryName_LogsErrorToWebServer ) { std::shared_ptr<FakeWebService> p_web_service = std::make_shared<FakeWebService>(); TestingEntryAnalyzer ea( p_web_service ); bool result = ea.Analyze( "e" ); ASSERT_EQ( p_web_service->lastError, "Error: e" ); } 37/43
  38. 38. Тестирование взаимодействия TEST_F( EntryAnalyzerTest, Analyze_TooShortEntryName_LogsErrorToWebServer ) { std::shared_ptr<FakeWebService> p_web_service = std::make_shared<FakeWebService>(); TestingEntryAnalyzer ea( p_web_service ); bool result = ea.Analyze( "e" ); ASSERT_EQ( p_web_service->lastError, "Error: e" ); } class TestingEntryAnalyzer : public EntryAnalyzer { public: TestingEntryAnalyzer( std::shared_ptr<IWebService> p_service ) : pWebService( p_service ) { } private: void LogError( std::string err ) override { pWebService->LogError( err ); } std::shared_ptr<IWebService> pWebService; }; 38/43
  39. 39. Тестирование взаимодействия TEST_F( EntryAnalyzerTest, Analyze_TooShortEntryName_LogsErrorToWebServer ) { std::shared_ptr<FakeWebService> p_web_service = std::make_shared<FakeWebService>(); TestingEntryAnalyzer ea( p_web_service ); bool result = ea.Analyze( "e" ); ASSERT_EQ( p_web_service->lastError, "Error: e" ); } class FakeWebService : public IWebService { public: void LogError( std::string error ) override { lastError = error; } std::string lastError; }; class TestingEntryAnalyzer : public EntryAnalyzer { public: TestingEntryAnalyzer( std::shared_ptr<IWebService> p_service ) : pWebService( p_service ) { } private: void LogError( std::string err ) override { pWebService->LogError( err ); } std::shared_ptr<IWebService> pWebService; }; 39/43
  40. 40. Приемы создания хороших unit-тестов
  41. 41. Практические приемы • Один тест - один результат работы • Тестируем только для публичные методы • Нет ветвления • операторы: switch, if, else • циклы: for, while, std::for_each • Юнит тест - последовательность вызовов методов + assert • Используем фабрики 41/43
  42. 42. Где почитать подробнее • Roy Osherove “The art of unit testing”. 2nd edition •Майкл Физерс “Эффективная работа с унаследованным кодом” •Кент Бек “Экстремальное программирование. Разработка через тестирование” 42/43
  43. 43. Спасибо за внимание! cppconf.ru - 2017 Ястребов Виктор Разработчик компании “Тензор” va.yastrebov@tensor.ru

×