43
Использование юнит-тестов для повышения качества разработки cppconf.ru - 2017 Ястребов Виктор Разработчик компании “Тензор”

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

Embed Size (px)

Citation preview

Использование юнит-тестов для

повышения качества разработки

Ястребов Виктор / разработчик компании “Тен"зор”

cppconf.ru - 2017

Ястребов Виктор Разработчик компании “Тензор”

1. Характеристики хорошего юнит-теста

2. Подходы к созданию тестируемого кода

3. Виды поддельных объектов

4. Пример тестирования класса

5. Приемы создания хороших юнит-тестов

О чем будем говорить

2/43

Допущения

• Код упрощен

•Используется Google Test

https://github.com/google/googletest/tree/master/googletest

3/43

Внешняя зависимость

Взаимодействие есть, а контроля нет

4/43

Жесткий диск, время, база данных

Интеграционный тест VS юнит-тест

Полный контроль

над внешними зависимостями

Интеграционный тест

Юнит тест

нет да

5/43

Признаки хорошего юнит-теста

Полный контроль над внешними зависимостями

Автоматизация

запуска

Результат

• стабилен

• повторим

• независим

• Малое время выполнения

• Простота чтения

6/43

Принцип наименования юнит-теста

Sum_ByDefault_ReturnsZero()

Sum_WhenCalled_CallsTheLogger()

[ИмяТестируемойРабочейЕденицы]_

[СценарийТеста]_

[ОжидаемыйРезультат]

7/43

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/43

Рабочие единицы тестируемого кода

• Возвращаемый результат

• Изменение состояния

системы

Задача

распознавания

Задача

разделения

Разрыв

зависимости

• Взаимодействие между

объектами

10/43

Поддельные объекты (Fakes)

Поддельные

объекты

(Fakes)

Поддельные реализации объектов

Fake-объект

Stub-объект

Задача

разделения

Mock-объект

Задача

распознавания

12/43

Stub-объект

Взаимоде

йствие

Тестовый код

Тестируемый код Stub Взаимодействие

13/43

Stub-объект

Взаимоде

йствие

Тестовый код

Тестируемый код Mock Взаимодействие

14/43

Разбор на примере

Разбор

на примере

Исходный код

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

Исходный код

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

Исходный код

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

Исходный код

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

Разрыв зависимости от базы данных

База данных

IsValid( std::string ename )

IDatabaseManager

FakeDatabaseManager DatabaseManager

20/43

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

Использование stub

для разрыва зависимости

Параметризация

конструктора.

Вариант 1

Вместо конкретной реализации – интерфейс

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

Внедрение зависимости

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

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

Внедрение зависимости

Тестирование возвращаемого значения

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

Использование stub

для разрыва зависимости

Параметризация

конструктора.

Вариант 2

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

Тестирование возвращаемого значения

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

Использование stub

для разрыва зависимости

“Выделить и

переопределить”

Выделение зависимости

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

Переопределение зависимости

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

Тестирование возвращаемого значения

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

Использование Mock для разрыва зависимости

Использование

Mock для разрыва

зависимости

Выделение зависимости

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

Переопределение зависимости

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

Тестирование взаимодействия 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

Тестирование взаимодействия 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

Тестирование взаимодействия 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

Приемы создания

хороших

unit-тестов

Практические приемы

• Один тест - один результат работы

• Тестируем только для публичные методы

• Нет ветвления

• операторы: switch, if, else

• циклы: for, while, std::for_each

• Юнит тест - последовательность вызовов методов + assert

• Используем фабрики

41/43

Где почитать подробнее

• Roy Osherove “The art of unit testing”. 2nd edition

•Майкл Физерс “Эффективная работа

с унаследованным кодом”

•Кент Бек “Экстремальное программирование.

Разработка через тестирование”

42/43

Спасибо за внимание!

Ястребов Виктор / [email protected]

cppconf.ru - 2017

Ястребов Виктор Разработчик компании “Тензор”

[email protected]