unit tests final
TRANSCRIPT
Практики надежного модульного тестирования
для С++Юрий Ефимочев
О себе
Ведущий разработчик/архитектор в компании SolarWinds, в бэкап-решении MAXBackup.Специализация: высоконагруженные отказоустойчивые системы на C++
Польза > Затраты
Что такое модульные тесты?
Модульное тестирование(unit testing) - это процесс, позволяющий проверить на корректность отдельные модули программы.
● Раннее обнаружение ошибок● Обнаружение регрессионных ошибок● Моделирование сложных сценариев● Получение оценки производительности● Динамический анализ кода(valgrind, …)
Обнаружение ошибок
Дизайн
Дизайн
● Модульность● Поощрение рефакторинга● Ускорение разработки за счет повышения
качества кода● Foolproof design
Пример: foolproof design class ITransactionController { public: virtual void BeginTransaction() = 0; virtual void CommitTransaction() = 0; virtual void RollbackTransaction() = 0;
virtual ~ITransactionController() {} };
Пример: foolproof design BOOST_AUTO_TEST_CASE(DoubleBeginTest) { controller.BeginTransaction(); BOOST_REQUIRE_THROW(controller.BeginTransaction()); }
BOOST_AUTO_TEST_CASE(OutOfOrderCommitTest) { BOOST_REQUIRE_THROW(controller.CommitTransaction()); }
BOOST_AUTO_TEST_CASE(OutOfOrderRollbackTest) { BOOST_REQUIRE_THROW(controller.RollbackTransaction()); }
Пример: foolproof design class ITransactionGuard { public: virtual void Commit() = 0;
virtual ~ITransactionGuard() {} };
typedef std::unique_ptr<ITransactionGuard> ITransactionGuardPtr;
class ITransactionController { public: virtual ITransactionGuardPtr BeginTransaction() = 0;
virtual ~ITransactionController() {} };
Пример: foolproof design{ ITransactionGuardPtr guard = controller.BeginTransaction();
// do something
guard.Commit(); }
Документация
Документация
● Актуальная документация● Верификация на этапе компиляции● Готовые сценарии использования
Хороший тест:
● Актуален○ Запускается при сборке○ Ошибка теста = ошибка компиляции○ Покрытие кода тестами контролируется
Хороший тест:
● Актуален● Не затрудняет разработку
○ Выполняется быстро○ Не спамит в лог компиляции○ Код соответствует guideline-ам○ Результат не зависит от окружения
Пример BOOST_AUTO_TEST_CASE(DnsResolverTest) { DnsResolver resolver; BOOST_REQUIRE_NO_THROW(resolver.Resolve("www.google.com")); }
Хороший тест:
● Актуален● Не затрудняет разработку
○ Выполняется быстро○ Не спамит в лог компиляции○ Код соответствует guideline-ам○ Результат не зависит от окружения○ Ведет себя детерминировано
Пример void SimpleTask(std::atomic<int>& counter) { ++counter; }
BOOST_AUTO_TEST_CASE(ThreadPoolTest) { int const TaskCount = 100;
ThreadPool pool(4);
std::atomic<int> processedCount = 0;
for (int index = 0; index < TaskCount; ++index) { pool.AddTask(std::bind(&SimpleTask, std::ref(processedCount))); }
ThreadHelper::Sleep(Time::Seconds(1));
BOOST_CHECK_EQUAL(processedCount, TaskCount); }
Пример BOOST_AUTO_TEST_CASE(EncryptionTest) { int const IterationsCount = 1000000;
for (int index = 0; index < IterationsCount; ++index) { std::string const phrase = GenerateRandomPhrase(); BOOST_CHECK(Decrypt(Encrypt(phrase)) == phrase); } }
Пример BOOST_AUTO_TEST_CASE(EncryptionTest) { int const IterationsCount = 1000000;
for (int index = 0; index < IterationsCount; ++index) { std::string const phrase = GenerateRandomPhrase(); BOOST_CHECK_EQUAL(Decrypt(Encrypt(phrase)), phrase); } }
Хороший тест:
● Актуален● Не затрудняет разработку
○ Выполняется быстро○ Не спамит в лог компиляции○ Код соответствует guideline-ам○ Результат не зависит от окружения○ Ведет себя детерминировано○ Ошибки информативны
Информативные ошибки
void CheckItem(Item const& item) { BOOST_REQUIRE(0 < item.Id && item.Id < 1000 && 0 < item.Name.size() && item.Name.size() < 50); // error }
BOOST_AUTO_TEST_CASE(CreateTest) { CheckItem(factory.Create(ParameterSet1)); CheckItem(factory.Create(ParameterSet2)); CheckItem(factory.Create(ParameterSet3)); }
BOOST_AUTO_TEST_CASE(SomeOtherTest) { // ... CheckItem(...); // ... }
Информативные ошибки
bool CheckItem(Item const& item) { return 0 < item.Id && item.Id < 1000 && 0 < item.Name.size() && item.Name.size() < 50; }
BOOST_AUTO_TEST_CASE(CreateTest) { BOOST_REQUIRE(CheckItem(factory.Create(ParameterSet1))); // error BOOST_REQUIRE(CheckItem(factory.Create(ParameterSet2))); BOOST_REQUIRE(CheckItem(factory.Create(ParameterSet3))); }
Информативные ошибки
bool CheckItem(Item const& item) { return 0 < item.Id && item.Id < 1000 && 0 < item.Name.size() && item.Name.size() < 50; }
BOOST_AUTO_TEST_CASE(CreateTest) { BOOST_CHECK(CheckItem(factory.Create(ParameterSet1))); // error BOOST_CHECK(CheckItem(factory.Create(ParameterSet2))); BOOST_CHECK(CheckItem(factory.Create(ParameterSet3))); // error }
Информативные ошибки
#define CHECK_ITEM(item) \ BOOST_CHECK_GT(item.Id, 0); \ BOOST_CHECK_LT(item.Id, 1000); \ BOOST_CHECK_GT(item.Name.size(), 0); \ BOOST_CHECK_LT(item.Name.size(), 50);
BOOST_AUTO_TEST_CASE(CreateTest) { CHECK_ITEM(factory.Create(ParameterSet1)); // error: invalid id CHECK_ITEM(factory.Create(ParameterSet2)); CHECK_ITEM(factory.Create(ParameterSet3)); // error: invalid name }
Хороший тест:
● Актуален● Не затрудняет разработку● Прост для понимания
○ Хорошо структурирован
Примерclass IUserManager{public: virtual int AddUser(UserInfo const& user) = 0; virtual void UpdateUser(UserInfo const& user) = 0; virtual void DeleteUser(int const id) = 0;
virtual UserInfo GetUserById(int const id) const = 0; virtual UserInfoIteratorPtr EnumerateUsers() const = 0;
virtual ~IUserManager() {}};
Пример: организация тестов BOOST_AUTO_TEST_SUITE(UserManagerTestSuite);
BOOST_AUTO_TEST_CASE(Test) { // create manager // add 20 users // test EnumerateUsers // test GetUserById // add 2 more users // test add special case // test EnumerateUsers // test GetUserById // update 10 users // test update special case // test EnumerateUsers // test GetUserById // delete 10 users // ... }
BOOST_AUTO_TEST_SUITE_END()
Пример: организация тестов BOOST_AUTO_TEST_SUITE(UserManagerTestSuite)
BOOST_AUTO_TEST_CASE(InsertTest) BOOST_AUTO_TEST_CASE(InsertNonUniqueTest) BOOST_AUTO_TEST_CASE(InsertWithCustomIdTest) BOOST_AUTO_TEST_CASE(InsertWithAlreadyExistingIdTest)
BOOST_AUTO_TEST_CASE(UpdateTest) BOOST_AUTO_TEST_CASE(UpdateWithInvalidValueTest) BOOST_AUTO_TEST_CASE(UpdateNonExistingUserTest)
BOOST_AUTO_TEST_CASE(DeleteTest) BOOST_AUTO_TEST_CASE(DeleteNonExistingUserTest)
BOOST_AUTO_TEST_CASE(GetUserByIdTest) BOOST_AUTO_TEST_CASE(GetNonExistingUserByIdTest)
BOOST_AUTO_TEST_CASE(EnumerateTest) BOOST_AUTO_TEST_CASE(EnumerateEmptyCollectionTest)
BOOST_AUTO_TEST_SUITE_END()
Пример: тест-сценарий BOOST_AUTO_TEST_CASE(EnumerateTest) { // Given manager.AddUser(UserInfo1); manager.AddUser(UserInfo2); manager.AddUser(UserInfo3);
// When manager.DeleteUser(UserInfo2.Id);
// Then BOOST_CHECK_EQUAL(manager.Enumerate().Size(), 2); BOOST_CHECK_EQUAL(manager.GetUserById(UserInfo1.Id), UserInfo1); BOOST_CHECK_THROW(manager.GetUserById(UserInfo2.Id)); BOOST_CHECK_EQUAL(manager.GetUserById(UserInfo3.Id), UserInfo3); }
Хороший тест:
● Актуален● Не затрудняет разработку● Прост для понимания
○ Хорошо структурирован○ Минималистичен
Пример: fixture BOOST_AUTO_TEST_SUITE(UserManagerTestSuite);
BOOST_AUTO_TEST_CASE(UpdateTest) { ManagerFactory const factory; IUserManagerPtr manager = factory.CreateUserManager();
// ... }
BOOST_AUTO_TEST_CASE(DeleteTest) { ManagerFactory const factory; IUserManagerPtr manager = factory.CreateUserManager();
// ... }
BOOST_AUTO_TEST_SUITE_END()
Пример: fixture struct Fixture { Fixture() : Manager(MangerFactory().CreateUserManager()) {}
IUserManagerPtr Manager; }
BOOST_FIXTURE_TEST_SUITE(UserManagerTestSuite, Fixture);
BOOST_AUTO_TEST_CASE(UpdateTest) { // ... }
BOOST_AUTO_TEST_CASE(DeleteTest) { // ...}
BOOST_FIXTURE_TEST_SUITE_END()
Пример: излишняя сложность BOOST_AUTO_TEST_CASE(Test) { int id = 0;
++id; BOOST_CHECK_EQUAL(manager.AddUser(Names[id]), id); ++id; BOOST_CHECK_EQUAL(manager.AddUser(Names[id]), id); ++id; BOOST_CHECK_EQUAL(manager.AddUser(Names[id]), id);
while (id >= 0) { BOOST_CHECK_EQUAL(manager.GetUserById(id).Name, Names[id]); --id; } }
Пример: излишняя сложность BOOST_AUTO_TEST_CASE(Test) { BOOST_CHECK_EQUAL(manager.AddUser(Name1), 1); BOOST_CHECK_EQUAL(manager.AddUser(Name2), 2); BOOST_CHECK_EQUAL(manager.AddUser(Name3), 3);
BOOST_CHECK_EQUAL(manager.GetUserById(1).Name, Name1); BOOST_CHECK_EQUAL(manager.GetUserById(2).Name, Name2); BOOST_CHECK_EQUAL(manager.GetUserById(3).Name, Name3); }
Пример: повторяющиеся проверки BOOST_AUTO_TEST_CASE(DeleteTest) { BOOST_CHECK_EQUAL(manager.AddUser(...), 1); BOOST_CHECK_EQUAL(manager.AddUser(...), 2); BOOST_CHECK_EQUAL(manager.AddUser(...), 3); BOOST_CHECK_EQUAL(manager.AddUser(...), 4);
BOOST_CHECK_NO_THROW(manager.DeleteUser(1)); BOOST_CHECK_NO_THROW(manager.DeleteUser(2)); BOOST_CHECK_NO_THROW(manager.DeleteUser(3)); BOOST_CHECK_NO_THROW(manager.DeleteUser(4)); }
BOOST_AUTO_TEST_CASE(DeleteTest) { BOOST_CHECK_EQUAL(manager.AddUser(...), 1); BOOST_CHECK_NO_THROW(manager.DeleteUser(1)); }
Пример: ортогональные тесты BOOST_AUTO_TEST_CASE(UpdateTest) { BOOST_CHECK_EQUAL(manager.AddUser(UserInfo1), 1); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo2), 2); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo3), 3); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo4), 4); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo5), 5);
// ... }
BOOST_AUTO_TEST_CASE(DeleteTest) { BOOST_CHECK_EQUAL(manager.AddUser(UserInfo1), 1); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo2), 2); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo3), 3); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo4), 4); BOOST_CHECK_EQUAL(manager.AddUser(UserInfo5), 5);
// ... }
Пример: ортогональные тесты BOOST_AUTO_TEST_CASE(UpdateTest) { manager.AddUser(UserInfo1); manager.AddUser(UserInfo2); manager.AddUser(UserInfo3);
// ... }
BOOST_AUTO_TEST_CASE(DeleteTest) { manager.AddUser(UserInfo1); manager.AddUser(UserInfo2);
// ... }
Хороший тест:
● Актуален● Не затрудняет разработку● Прост для понимания
○ Хорошо структурирован○ Минималистичен○ Самодостаточен
Пример: скрытые данные/проверки
BOOST_FIXTURE_TEST_SUITE(UserManagerTestSuite, Fixture);
BOOST_AUTO_TEST_CASE(Test) { ReadDataFromFile("./Data.user", Manager); ValidateData(Manager); }
BOOST_FIXTURE_TEST_SUITE_END()
std::string Encrypt(std::string const& data) { return data; // todo: implement later }
std::string Decrypt(std::string const& data) { return data; // todo: implement later }
Пример: неявные проверки
std::string const SecretPhrase = "secret phrase";
BOOST_AUTO_TEST_CASE(Test) { BOOST_CHECK_EQUAL(Decrypt(Encrypt(SecretPhrase)), SecretPhrase); }
Пример: неявные проверки
std::string const SecretPhrase = "secret phrase"; std::string const EncryptedPhrase = "aL8m52AHJ/Z7Z5G2MkqpIaXOEjRm41OaraiTLYcXM3o=";
BOOST_AUTO_TEST_CASE(EncryptorTest) { BOOST_CHECK_EQUAL(Encrypt(SecretPhrase), EncryptedPhrase); }
BOOST_AUTO_TEST_CASE(DecryptorTest) { BOOST_CHECK_EQUAL(Decrypt(EncryptedPhrase), SecretPhrase); }
Хороший тест:
● Актуален● Не затрудняет разработку● Прост для понимания
○ Хорошо структурирован○ Минималистичен○ Самодостаточен○ Оптимизирован для чтения
Пример: синтаксический сахар BOOST_AUTO_TEST_CASE(TreeTraversalTest) { Tree tree; tree.InsertNode(1); tree.InsertNode(2, 1); tree.InsertNode(3, 2); tree.InsertNode(4, 2); tree.InsertNode(5, 1); tree.InsertNode(6, 5); tree.InsertNode(7, 5);
NodeCollection const nodes = TraversPerOrder(tree);
BOOST_CHECK(nodes.size(), 7); BOOST_CHECK(nodes[0], 1); BOOST_CHECK(nodes[1], 2); BOOST_CHECK(nodes[2], 3); BOOST_CHECK(nodes[3], 4); BOOST_CHECK(nodes[4], 5); BOOST_CHECK(nodes[5], 6); BOOST_CHECK(nodes[6], 7); }
Пример: выразительная сила С++ // http://www.eelis.net/C++/analogliterals.xhtml
assert( ( o-------------o |L \ | L \ | L \ | o-------------o | ! ! ! ! ! o | ! L | ! L | ! L| ! o-------------o ).volume == ( o-------------o | ! ! ! ! ! o-------------o ).area * int(I-------------I) );
Пример: синтаксический сахар BOOST_AUTO_TEST_CASE(TreeTraversalTest) { Tree const tree = Node(1) (Node(2) (Node(3)) (Node(4))) (Node(5) (Node(6)) (Node(7))));
BOOST_CHECK(TraversPreOrder(tree), List(1, 2, 3, 4, 5, 6, 7)); BOOST_CHECK(TraversInOrder(tree), List(3, 2, 4, 1, 6, 5, 7)); BOOST_CHECK(TraversPostOrder(tree), List(3, 4, 2, 6, 7, 5, 1)); }
Пример: синтаксический сахар BOOST_AUTO_TEST_CASE(EnumerateTest) { manager.AddUser(UserInfo("User1", "[email protected]", UserRole::Admin, ...)); manager.AddUser(UserInfo("User2", "[email protected]", UserRole::Reader, ...)); manager.AddUser(UserInfo("User3", "[email protected]", UserRole::Reader, ...));
IUserIteratorPtr iterator = manager.Enumerate(); UserInfo info; BOOST_CHECK(iterator.GetNext(info)); BOOST_CHECK_EQUAL(info.Name, "User1"); BOOST_CHECK(iterator.GetNext(info)); BOOST_CHECK_EQUAL(info.Name, "User2"); BOOST_CHECK(iterator.GetNext(info)); BOOST_CHECK_EQUAL(info.Name, "User3"); }
Пример: синтаксический сахар BOOST_AUTO_TEST_CASE(EnumerateTest) { User("User1"); User("User2"); User("User3");
BOOST_CHECK_EQUAL(Enumerate(), List("User1", "User2", "User3")); }
Пример: синтаксический сахар BOOST_AUTO_TEST_CASE(EnumerateByRoleTest) { User("User1").Role(UserRole::Admin); User("User2").Role(UserRole::Reader); User("User3").Role(UserRole::Reader);
BOOST_CHECK_EQUAL(EnumerateByRole(UserRole::Reader), List("User2", "User3")); }
Рекомендации● Предпочитать check require● Использовать fixture● Оформлять тесты в сценарии● Использовать синтаксический сахар● Убрать все лишнее● Показать все, что нужно для понимания● Написать необходимый минимум тестов