Unit testing
Wikipedia: Unit testing
- slouží k testování jednotlivých částí kódu (units) – funkce, třídy, struktury
- každý projekt by měl mít sadu testů, které pokrývají celou funkčnost projektu
- jednotlivé testy by měly být izolované – pokud nějaký test selže, mělo by být snadné dohledat problematickou část kódu
- mělo by se testovat:
- všechny možné kombinace vstupních parametrů (v praxi: významné/charakteristické kombinace)
- ověřit chování pro platné vstupy
- ověřit chování pro neplatné vstupy (např. nastane výjimka)
- v praxi nelze zachytit všechny chyby pomocí testů (nelze pokrýt všechny možné kombinace parametrů, stavů systému, apod.), ale stejně můžou podstatně ulehčit vývoj kódu
- snaha automatizovat testování – to vedlo k continuous integration (např. GitLab CI, GitHub Actions)
Další typy testů
Integration (kombinace různých modulů), GUI (chování grafického rozhraní), installation (postup pro instalaci), regression (ověření předchozích bug fixů, změny konfigurace, apod.), security, atd.
Test-driven development
Vývojový cyklus podle test-driven development:
- Přidat test pro novou funkčnost
- Spustit všechny testy, nový test samozřejmě selže.
- Implementovat co nejjednodušší kód, který zajistí splnění všech testů. (V této fázi nový kód ani nemusí být obecný, může řešit jen konkrétní případ daný testem.)
- Ověřit, že všechny testy projdou bez chyb.
- Refaktorování kódu dle potřeby. V průběhu pomocí testů ověřovat, že nedošlo ke změně chování.
- příklady refaktorování:
- přesun kódu na logičtější místo
- odstranění duplicitního kódu
- přejmenování proměnných, funkcí, tříd
- rozdělení funkcí a metod na menší části (to ale často znamená přidání nového testu, neboť každá funkce či metoda by měla být testovaná samostatně)
- změna hierarchie dědičnosti mezi třídami
- příklady refaktorování:
- (volitelně) Ověřit kvalitu kódu pomocí programů jako např.
clang-tidy
aclang-format
. - (volitelně) Commitovat změny do systému pro správu kódu (např. git).
Tyto kroky se opakují pro každou novou funkčnost, kterou je třeba v programu implementovat.
Změny a testy by měly být malé a inkrementální a commitované samostatně:
- v případě selhání velkého množství testů se pak lze jednoduše vrátit k poslední funkční verzi
- v případě objevení nové chyby lze jednoduše identifikovat problematickou změnu, která ji způsobila
(např. git poskytuje metodu bisekce,
git bisect
, ukážeme si později)
Rozšířením tohoto přístupu je behavior-driven development, kde se pro specifikaci očekávaného chování používá doménově specifický jazyk.
Knihovny pro testování v C++
Existuje spousta knihoven, my použijeme GoogleTest, což je zřejmě nejrozšířenější knihovna pro testování v C++.
Pro zprovoznění miniprojektu nejprve vytvořte soubor CMakeLists.txt
v
prázdném adresáři:
cmake_minimum_required(VERSION 3.25)
project(GoogletestDemo)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "-Wall -pedantic")
set(CMAKE_CXX_FLAGS_DEBUG "-g")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
# add normal target(s)
add_executable(demo demo.cpp)
# configure testing
include(CTest) # this must be in the top-level CMakeLists.txt file!
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
OVERRIDE_FIND_PACKAGE
SYSTEM
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# Prevent installing GTest along with TNL (the tests themselves are not installed)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
set(INSTALL_GMOCK OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# add test target(s)
add_executable(test-demo test_demo.cpp)
target_link_libraries(test-demo PUBLIC GTest::gtest_main)
add_test(test-demo ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test-demo)
Podrobnosti najdete na stránce Quickstart: Building with CMake.
Poté vytvořte soubory demo.h
, demo.cpp
a test_demo.cpp
. V případě
umístění do nějakého podadresáře (např. src
) je potřeba upravit cesty i v
souboru CMakeLists.txt
. Na pořadí, v jakém budete přidávat kód do
jednotlivých souborů, úplně nezáleží – my si vyzkoušíme test-driven development
a jako první naprogramujeme test:
#include <gtest/gtest.h>
#include "demo.h"
// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput)
{
EXPECT_EQ(factorial(0), 1);
}
// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput)
{
EXPECT_EQ(factorial(1), 1);
EXPECT_EQ(factorial(2), 2);
EXPECT_EQ(factorial(3), 6);
EXPECT_EQ(factorial(8), 40320);
}
Podrobnosti k makrům výše najdete na stránce GoogleTest Primer.
Test očekává implementaci funkce factorial
v souboru demo.h
, kterou přidáme
jako druhou. Naposled přidáme kód do souboru demo.cpp
– samotná spustitelná
aplikace.
Pro připomenutí: kompilaci pomocí cmake
můžeme provést následujícími příkazy:
- Konfigurace:
cmake -B build -S .
(případně s dalšími parametry, např.cmake -B build -S . -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
) - Kompilace:
cmake --build build
Poté můžeme spustit výsledný binární soubor, např. ./build/test-demo --help
.
Testy můžeme spouštět buď samostatně s určitými parametry, nebo celý test
suite současně:
cmake --build build --target test