AAA - złoty standard unit testów.
Arrange Act Assert – to game changer dla jakości unit testów. Dzięki tym prostym zasadom testy stają się nie tylko bardziej czytelne, lecz mogą stanowić doskonałą dokumentację przypadków użycia naszego kodu. Dzielimy nazwę, jak i kod naszego unit testu, na trzy jasno określone bloki.
A czym jest to całe AAA? Zacznijmy od początku.
AAA - złoty standard
O zasadzie AAA (triple A) piszę w swojej świetnej książce: “Testy jednostkowe. Świat niezawodnych aplikacji” (The Art of Unit Testing) - Roy Osherove. Nie jest on wprawdzie jej bezpośrednim autorem, lecz wielkim fanem, zresztą nie tylko on ;). AAA wywodzi się ze środowiska .NET, niemniej, nie jest zależna od użytej technologii. Świetnie sprawdza się również w świecie C++.
No dobrze, ale czym ta zasada jest i o czym mówi?
AAA - mówi o tym, by podzielić test na trzy logiczne bloki: Arrange, Act i Assert.
Arrange
W pierwszym bloku kodu naszego testu przygotowujemy wszystko, co niezbędne, aby naszą testowaną metodę lub funkcję sprawdzić. Przygotowujemy dane wejściowe oraz oczekiwane dane wyjściowe. Tworzymy stuby i mocki oraz ustawiamy ich niezbędne zachowanie względem naszej testowanej jednostki. W tym bloku możemy także wywołać inne metody testowanej klasy, jeśli są nam potrzebne do uzyskania odpowiedniego stanu obiektu. Oczywiście te metody również powinny być zweryfikowane w osobnych testach.
Act
Drugim blokiem jest uruchomienie naszej testowanej metody/funkcji. Najczęściej będzie to pojedyncza linijka kodu, ale nie zawsze. Czasem bywa tak, że działanie naszej testowanej metody jest inne, gdy wywołamy ją kilkukrotnie. Jeśli to jest przedmiotem naszego unit testu, to wtedy jak najbardziej również umieszczamy wszystkie wywołania w bloku Act.
Assert
Trzecim i ostatnim blokiem jest weryfikacja. Tutaj sprawdzamy, czy wartości zwracane przez naszą jednostkę są zgodne z oczekiwanymi. Również tutaj weryfikujemy stan obiektu, jeśli jest to oczekiwane zachowanie testowanej metody. Można spotkać się z zasadą “jedna asercja na test”. Jeśli potraktujemy ją dosłownie, to powielimy testy tylko po to, by zachować tę zasadę i wywoływać pojedynczy ASSERT w teście. Lecz nie o to w niej chodzi, tylko o spójny kontekst asercji. Samych wywołań może być więcej, jeśli tylko są one ściśle powiązane.
Przykład użycia triple A
Spójrz na poniższy przykład testu niekorzystającego z AAA, czy potrafisz odgadnąć co jest w zasadzie testowane?
TEST_F(TemperatureSensorManagerTest, testCollectingTemperatures)
{
TemperatureSensorManager manager;
TemperatureSensorFactoryStub factory;
const auto expectedSensorName1{ "temp_core_1" };
manager.addSensor(factory.createSensor(expectedSensorName1, 47.3));
const auto expectedSensorName2{ "temp_core_2" };
manager.addSensor(factory.createSensor(expectedSensorName2, -10.0));
const auto expectedSensorName3{ "temp_board_0" };
manager.addSensor(factory.createSensor(expectedSensorName3, 65.1));
auto temps = manager.getTemps();
ASSERT_EQ(3, temps.size());
EXPECT_EQ(expectedSensorName1, temps.at(0).getName());
EXPECT_EQ(expectedSensorName2, temps.at(1).getName());
EXPECT_EQ(expectedSensorName3, temps.at(2).getName());
EXPECT_FLOAT_EQ(34.13, manager.getAvgTemp());
}
A teraz ten sam test, tylko sformatowany zgodnie z triple A(na co dzień nie dodaję takich komentarzy ;) ):
TEST_F(TemperatureSensorManagerTest, testCollectingTemperatures)
{
//Arrange
const auto expectedSensorName1{ "temp_core_1" };
const auto expectedSensorName2{ "temp_core_2" };
const auto expectedSensorName3{ "temp_board_0" };
TemperatureSensorFactoryStub factory;
TemperatureSensorManager manager;
//Act
manager.addSensor(factory.createSensor(expectedSensorName1, 47.3));
manager.addSensor(factory.createSensor(expectedSensorName2, -10.0));
manager.addSensor(factory.createSensor(expectedSensorName3, 65.1));
//Assert
auto temps = manager.getTemps();
ASSERT_EQ(3, temps.size());
EXPECT_EQ(expectedSensorName1, temps.at(0).getName());
EXPECT_EQ(expectedSensorName2, temps.at(1).getName());
EXPECT_EQ(expectedSensorName3, temps.at(2).getName());
EXPECT_FLOAT_EQ(34.13, manager.getAvgTemp());
}
Od razu widać, co jest przedmiotem testu, co jest wymagane na początku oraz jaki wynik będzie oczekiwany. Mimo iż sama nazwa testu nie mówi nam wiele, szybkie spojrzenie na kod daje jednak jasny obraz tego, co unit test sprawdza. No właśnie, nazwa testu… czy można coś z tym zrobić?
Nazwa testu
Standard AAA możemy również wykorzystać przy nadawaniu nazw unit testów. Główną zaletą AAA jest poprawa czytelności testów. Dobra nazwa to nie taka prosta sprawa, jeśli nie zna się przydatnych wytycznych. Dla przykładu zwykła lub “zła” nazwa testu:
TEST_F(TemperatureSensorTests, test_no_avg_temp)
Czy domyślasz się, co jest testowane? W jakich warunkach? Co jest wynikiem testu? Jeśli się domyślasz, ale tego nie wiesz, już po samej nazwie, to nie jest ona do końca trafiona, prawda?
Ok, spróbujmy teraz z taką nazwą:
TEST_F(TemperatureSensorTests, calculateAverageTemp_emptyTemperatureInput_ReturnZero)
Lepiej? Nie wiem jak dla Ciebie, ale dla mnie, tak! Zdecydowanie widać co jest testowane, jak i co będzie wynikiem. Tym właśnie jest AAA - trzy części nazwy unit testu, w skrócie, schemat budowy nazwy testów wygląda tak:
TEST_F(TestowanaKlasaTest, nazwaTestowanejMetody_ScenariuszTestowyZawierającyDaneWejściowe_WynikCzyliToCoMaSięStaćPoWykonaniuTestowanejMetody)
Powróćmy do naszego przykładowego unit testu i zastosujmy AAA do jego nazwy. Znając wytyczne, zamiast takiej nazwy:
TEST_F(TemperatureSensorManagerTest, testCollectingTemperatures)
Powinniśmy otrzymać coś w tym rodzaju:
TEST_F(TemperatureSensorManagerTest, addSensor_AddThreeValidSensors_StoreAllSensorsAndReturnCorrectAverageTemperature)
Teraz, nie znając ciała testu, łatwo możemy określić co on robi. A dlaczego taki format, a nie inny? Dla przykładu zróbmy listę kilku takich nazw:
TEST_F(TemperatureSensorManagerTest, addSensor_AddOneValidSensor_StoreSensorAndReturnAverageTemperatureSameAsSensorTemperature)
TEST_F(TemperatureSensorManagerTest, addSensor_AddTwoSensorsOneValid_StoreOneSensorAndReturnAverageTemperatureSameAsSensorTemperature)
TEST_F(TemperatureSensorManagerTest, addSensor_AddThreeSensorsAllNoValid_NotStoreAnySensorAndReturnZeroAsAverageTemperature)
TEST_F(TemperatureSensorManagerTest, getTemps_NoAddedSensors_ReturnsEmptyContainer)
TEST_F(TemperatureSensorManagerTest, getTemps_ThreeSensorsAdded_ReturnsThreeSensors)
TEST_F(TemperatureSensorManagerTest, getAvgTemp_NoAddedSensors_ReturnsZero)
TEST_F(TemperatureSensorManagerTest, getAvgTemp_AddedOneSensor_ReturnsSameTemperatureAsSensor)
Przeglądając taki zestaw nazw, szybko zweryfikujemy, czy i jakie metody naszej klasy są przetestowane oraz w jakich warunkach. To miałem na myśli, pisząc o przypadkach użycia. Wystarczy nam lista nazw unit testów i mamy nie tylko zakres testowania, ale także funkcjonalne i zawsze aktualne sposoby użycia naszej klasy. Nawet osoba nietechniczna będzie w stanie ogarnąć, co jest testowane i w jakim zakresie. Nie musi analizować kodu testów.
Wyjątki od AAA
Od podziału na 3 bloki są pewne wyjątki. Możemy mieć sytuację, gdy chcemy, na przykład przetestować domyślne zachowanie konstruktora i w tym przypadku akurat nie mamy nic do zainicjalizowania. Wtedy po prostu bloku Arrange nie ma w teście i to jest ok.
TEST_F(DoorsLockTests, constructor_DefaultBehavior_ShouldReturnEmptyTemps)
{
TemperatureSensorManager manager;
auto temps = manager.getTemps();
ASSERT_EQ(3, temps.size());
}
Inny przykład, gdy jedynym oczekiwanym wynikiem naszej testowanej metody jest wartość przez nią zwrócona. Wtedy Act i Assert występują razem.
TEST_F(TemperatureSensorManagerTest, getAvgTemp_NoAddedSensors_ReturnsZero)
{
TemperatureSensorManager manager;
EXPECT_FLOAT_EQ(0.0, manager.getAvgTemp());
}
Możemy również dodać zmienną wynikową, by zachować podział na 3 bloki. Takie rozwiązanie też jest dobre.
TEST_F(TemperatureSensorManagerTest, getAvgTemp_NoAddedSensors_ReturnsZero)
{
TemperatureSensorManager manager;
auto result{ manager.getAvgTemp() };
EXPECT_FLOAT_EQ(0.0, result);
}
Ostatnim przykładem może być test zawierający tylko jedną linijkę kodu.
TEST_F(TemperatureSensorManagerTest, constructor_DefaultBehavior_ShouldNoThrowAnyException)
{
EXPECT_NO_THROW(TemperatureSensorManager{});
}
Zauważ, że we wszystkich przykładach nazwa wciąż zawiera podział zgodny z AAA. Mimo, że samych bloków może brakować, to wciąż test jest czytelny i łatwy do zrozumienia.
GWT
W Internecie możesz spotkać się z nazwą GWT. To w zasadzie to samo. GWT czyli Give(Arrnage) When(Act) Then(Assert). Ja osobiście wolę triple A ;)
Podsumowanie
I to już cały opis triple A. Mam nadzieję, że znajomość złotego standardu podniesie jakość także w Twoich testach. Jak dla mnie AAA naprawdę sporo wnosi i nie widzę żadnych przeciwwskazań do jego stosowania. Po prostu spróbuj!
Autor: Tadeusz Biela
Programista C++ | Entuzjasta TDD | Fan unit testów
Zostaw komentarz