Testy jednostkowe - Unit Tests#
Posiadanie zestawu testów jednostkowych obejmujących kod produkcyjny jest kluczowe w utrzymaniu projektu. Testy znacznie zwiększają możliwości rozwijania aplikacji, ponieważ pozwalają na zmiany.
Cele testów jednostkowych#
Testy powinny pomagać w poprawianiu jakości
Pomagają specyfikować zachowanie systemu w różnych scenariuszach definiowanych w uruchamialnej formie (“executable specification”)
Wyłapują i lokalizują błędy
Testy powinny pomóc w zrozumieniu systemu
Działają jak dokumentacja
Testy powinny redukować ryzyko
Umożliwiają bezpieczne przeprowadzenie refaktoringu
Testy powinny być łatwe w uruchamianiu
Powinny być szybkie, w pełni zautomatyzowane i powtarzalne (patrz FIRST)
Testy powinny być łatwe w pisaniu i utrzymaniu
Powinny być proste i czytelne - testy stają się zbyt skomplikowane, gdy chcemy w pojedynczym teście zweryfikować więcej niż jedną funkcjonalność
Testy powinny wymagań niewielkiego nakładu pracy przy ich utrzymaniu w trakcie rozwoju systemu
Atrybuty testów jednostkowych - F.I.R.S.T.#
Dobre testy jednostkowe powinny spełniać pięć zasad:
Szybkie (Fast) - Testy powinny być szybkie.
Niezależne (Independent) - Testy nie powinny zależeć od siebie.
Powtarzalne (Repeatable) - Testy powinny być powtarzalne w każdym środowisku.
Samokontrolujące (Self-Validating) - Testy powinny mieć jeden parametr wyjściowy typu logicznego. Mogą się powieść albo nie.
O czasie (Timely) - Testy powinny być pisane w odpowiednim momencie. Testy jednostkowe powinny być pisane bezpośrednio przed tworzeniem kodu produkcyjnego.
Zasady testów jednostkowych#
Najpierw napisz test
Projektuj pod kątem testów
Komunikuj intencje - nazwa testu powinna wyjaśniać jego intencje
Izoluj testowany system (“System Under Test” - SUT) - jeśli testowany system odwołuje się do trudnych w kontrolowaniu zależności, takich jak baza danych, sieć, system plików, należy je zastąpić obiektami pozorującymi (Test Double)
Unikaj w kodzie produkcyjnym zależności od testów - kod produkcyjny nie powinien zawierać instrukcji warunkowych typu
if (testing) thenWeryfikuj jedną koncepcję na test
jedna asercja na test - ułatwia nazywanie testów, ale prowadzi do konieczności tworzenia wielu testów, jeśli mamy zweryfikować stan wielu pól dla SUT
wiele asercji weryfikujących jeden aspekt funkcjonalności
Organizacja testów jednostkowych#
Nie ma jednego, uniwersalnego sposobu organizacji testów. W zależności od struktury kodu i celów testowania organizacja testów w projekcie może przybierać różne formy. Możemy wyróżnić trzy podstawowe scenariusze:
Klasa testów per testowana klasa - Tests Per Class#
najprostszy przypadek - wszystkie testy danej klasy są umieszczone w jednej klasie testowej (grupie testów)
każda klasa produkcyjna ma odpowiadającą jej klasę testową
stosowana w przypadku, gdy klasa testowana jest mała i niezależna od innych klas
Klasa testów per fikstura (konfiguracja SUT pod test) - Tests Per Fixture#
testy są zorganizowane według tzw. fixture, czyli kontekstu testowego.
fikstury przygotowują środowisko testowe i dane przed każdym testem. Jest to przydatne, gdy różne testy wymagają podobnych ustawień.
Klasa testów per cecha funkcjonalności - Tests Per Feature#
klasa testowa obejmuje zbiór metod testujących kolektywnie pewną wybraną cechę funkcjonalności SUT
ułatwia dokumentowanie zachowania SUT w danym kontekście
Nazewnictwo testów#
Nazwy testów powinny być opisowe i jednoznaczne. Nazwa testu powinna wyjaśniać, co jest testowane i w jakich warunkach.
Konwencje nazewnictwa testów jednostkowych#
Konwencje nazewnictwa testów jednostkowych są kluczowe dla utrzymania czytelności i spójności. Jasne i opisowe nazwy testów pomagają programistom szybko zrozumieć, co test weryfikuje. Oto kilka powszechnych konwencji nazewnictwa testów jednostkowych:
Nazewnictwo oparte na funkcjonalności#
Nazwa testu opisuje testowaną funkcjonalność.
Przykład: TestFunctionName_StateUnderTest_ExpectedBehavior
// Functionality-Based Naming
TEST(RecentlyUsedListTest, Add_MostRecentlyUsedUpdated)
{
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
ASSERT_EQ("Item1", recently_used_list.get_recent_item());
}
TEST(RecentlyUsedListTest, Add_DuplicateItem_ListSizeIsNotChanged)
{
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
EXPECT_EQ(1, recently_used_list.size());
recently_used_list.add("Item1");
ASSERT_EQ(1, recently_used_list.size());
}
Nazewnictwo oparte na zachowaniu#
Nazwa testu opisuje oczekiwane zachowanie.
Przykład: FunctionUnderTest_ExpectedResult
// Behavior-Based Naming
TEST(RecentlyUsedListTest, GetRecentItem_ReturnsLastAddedItem)
{
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
recently_used_list.add("Item2");
EXPECT_EQ("Item2", recently_used_list.getMostRecentlyUsed());
}
TEST(RecentlyUsedListTest, GetRecentItem_ThrowsExceptionWhenListIsEmpty)
{
RecentlyUsedList recently_used_list;
EXPECT_THROW(recently_used_list.getMostRecentlyUsed(), std::out_of_range);
}
Nazewnictwo Given-When-Then#
Nazwa testu podąża za wzorcem ustawienia scenariusza (Given/Arrange), wykonania akcji (When/Act) i weryfikacji wyniku (Then/Assert).
Przykład: GivenInitialState_WhenSomethingHappens_ThenResultIsObserved
// Given-When-Then Naming
TEST(RecentlyUsedListTest, GivenEmptyList_WhenItemIsAdded_ThenListIsNotEmpty)
{
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
EXPECT_FALSE(recently_used_list.empty());
}
TEST(RecentlyUsedListTest, GivenListWithItems_WhenDuplicateIsAdded_ThenListSizeIsNotChanged)
{
// Arrange
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
// Act
EXPECT_EQ(1, recently_used_list.size());
recently_used_list.add("Item1");
// Assert
ASSERT_EQ(1, recently_used_list.size());
}
// Given-When-Then Naming
GIVEN("An empty list")
{
RecentlyUsedList recently_used_list;
WHEN("An item is added")
{
recently_used_list.add("Item1");
THEN("The list is not empty")
{
REQUIRE_FALSE(recently_used_list.empty());
}
}
}
TEST_CASE("List with items)
{
RecentlyUsedList recently_used_list;
recently_used_list.add("Item1");
SECTION("Adding duplicate")
{
recently_used_list.add("Item1");
CHECK(recently_used_list.size() == 1);
SECTION("List size is not changed")
{
REQUIRE(recently_used_list.size() == 1);
}
}
}