wtorek, 27 listopada 2018

[2] CPPUnit Testing - Wstrzykiwanie zależności

W tym poście chciałbym opisać sposób testowania funkcji której wynik zależy od innej funkcji zewnętrznej.
[Źródło: http://academy.trendonix.com]

Opis:


Post ten dotyczy symulowania obiektów znajdujących się wewnątrz funkcji przez wykonanie podmiany funkcji wykonującej operacje wewnątrz innej funkcji. Stosuje się to w celu określenia konkretnej ścieżki wykonywania testu, tak aby określić konkretną drogę wykonania programu.

Poniżej krótki przykład funkcji której wynik będzie zależny od innej funkcji:

  1. uint8_t test_function(uint8_t val1, uint8_t val2)
  2. {
  3.     uint8_t retData = readSendedData();
  4.     if(retData == 1)
  5.     {
  6.         return ((val1 + val2) / 2);
  7.     }
  8.     else if(retData == 2)
  9.     {
  10.         return ((val1 + val2) * 2);
  11.     }
  12.     else if(retData == 3)
  13.     {
  14.         return (((val1 + val2) + 3) * 2);
  15.     }
  16.     return (val1 + val2);
  17. }


Tutaj jej wynik będzie zależny od teoretycznych odebranych danych:

  1. uint8_t readSendedData()
  2. {
  3.     if(sendedData == 0x1A)
  4.     {
  5.         return 1;
  6.     }
  7.     else if(sendedData == 0xA5)
  8.     {
  9.         return 2;
  10.     }
  11.     else if(sendedData == 0xC6)
  12.     {
  13.         return 3;
  14.     }
  15.     else
  16.     {
  17.         return 4;
  18.     }
  19. }

Aby przetestować funkcje test_function() tak aby nie ograniczać się do rezultatu funkcji  readSendedData().

Zmienna globalna:

Rozwiązanie pierwsze polega na ustawianiu wartości zmiennej globalnej tak aby wywołać odpowiednie elementy funkcji:

  1. void UnitTest::checkTestFunction_SendDataDecodeToOne()
  2. {
  3.     uint8_t expectedValue = 2;
  4.    
  5.     sendedData = 0x1A;
  6.     uint8_t returnData = test_function(2, 2);
  7.     CPPUNIT_ASSERT(returnData == expectedValue);
  8. }

Niestety to rozwiązanie nie do końca będzie dobre ponieważ, w którymś przypadku przez modyfikację zmiennych globalnych możemy zmodyfikować sobie wartość tak, że w kolejnych testach możemy otrzymywać inne wyniki niż te jakie sobie zażyczyliśmy. Spowodowane jest to tym że w pewnym momencie inna funkcja nam niespodziewane zmodyfikuje zmienną globalną, bądź zapomnimy jej ustawić przed wywołaniem. To spowoduje występowanie błędów podczas testów jednostkowych, które mogą zająć trochę czasu, za nim dogrzebiemy się do ich przyczyny.

Zastosowanie predefiniowanego makra:

Drugie rozwiązanie polega na zastosowanie makra, które zastąpi nam definicję funkcji na inną. W niej pozwoli nam to na wywołanie funkcji zastępczej, która pozwoli na wywołanie wymaganej definicji oraz nie spowoduje zmian w globalnej części programu:

  1. uint8_t test_function(uint8_t val1, uint8_t val2)
  2. {
  3.     #ifndef UNIT_TEST
  4.     uint8_t retData = readSendedData();
  5.     #else /* USE UNIT TEST*/
  6.     uint8_t retData = val1;
  7.     #endif
  8.     if(retData == 1)
  9.     {
  10.         return ((val1 + val2) / 2);
  11.     }
  12.     else if(retData == 2)
  13.     {
  14.         return ((val1 + val2) * 2);
  15.     }
  16.     else if(retData == 3)
  17.     {
  18.         return (((val1 + val2) + 3) * 2);
  19.     }
  20.     return (val1 + val2);
  21. }

Ta część automatycznie przypisuje parametr val1 do części testów jako argument sterujący. Przykładowy test wygląda następująco:

  1. void UnitTest::checkTestFunction_SendDataDecodeToOne()
  2. {
  3.     uint8_t expectedValue = 2;
  4.     uint8_t returnData = test_function(1, 3);
  5.     CPPUNIT_ASSERT(returnData == expectedValue);
  6. }

Można też wywołać osobną funkcję wywołującą odpowiedni parametr:

  1. uint8_t test_function(uint8_t val1, uint8_t val2)
  2. {
  3.     #ifndef UNIT_TEST
  4.     uint8_t retData = readSendedData();
  5.     #else /* USE UNIT TEST*/
  6.     uint8_t retData = readSendedData_testImplem();
  7.     #endif
  8.     if(retData == 1)
  9.     {
  10.         return ((val1 + val2) / 2);
  11.     }
  12.     else if(retData == 2)
  13.     {
  14.         return ((val1 + val2) * 2);
  15.     }
  16.     else if(retData == 3)
  17.     {
  18.         return (((val1 + val2) + 3) * 2);
  19.     }
  20.     return (val1 + val2);
  21. }

Zastosowanie wskaźnika do funkcji:

Kolejny sposób polega na zastosowaniu wskaźnika do funkcji.

  1. void UnitTest::checkTestFunction_SendDataDecodeToOne()
  2. {
  3.     extern uint8_t (*readSendedDataPtr)();
  4.     readSendedDataPtr = readSendedData_testImplem;
  5.     uint8_t expectedValue = 3;
  6.     uint8_t returnData = test_function(2, 4);
  7.     CPPUNIT_ASSERT(returnData == expectedValue);
  8. }

Funkcja testowa wygląda wtedy następująco:

  1. uint8_t test_function(uint8_t val1, uint8_t val2)
  2. {
  3.     uint8_t retData = readSendedDataPtr();
  4.     if(retData == 1)
  5.     {
  6.         return ((val1 + val2) / 2);
  7.     }
  8.     else if(retData == 2)
  9.     {
  10.         return ((val1 + val2) * 2);
  11.     }
  12.     else if(retData == 3)
  13.     {
  14.         return (((val1 + val2) + 3) * 2);
  15.     }
  16.     return (val1 + val2);
  17. }