piątek, 16 października 2026

Testy programów w C w Python

W tym poście skupimy się na uruchomieniu testów przykładowych funkcji w języku C. Do tego celu wykorzystamy Pythona oraz czysty język C. 



Funkcja do testów:


Jako przykład wykorzystamy prosty kod do sterowania przekaźnikami. Jego zadaniem jest załączenie przekaźnika w zależności od stanu na wejściu. 

Czas określony jest przez deklarację define RELAY_ON_DELAY_SEC 5. Po zmianie wejścia z 0 na 1, moduł zapamięta aktualny czas. Przez kolejne wejścia do funkcji będzie sprawdzał czas jaki został do zakończenia odliczania. Jeśli w tym czasie stan na pinie się zmieni na niski to odliczanie zostaje przerwane. Stan przekaźnika pozostanie bez zmian. 

  1. #include <stdint.h>
  2. #include <time.h>
  3.  
  4. #define RELAY_ON_DELAY_SEC 5
  5.  
  6. typedef struct
  7. {
  8.     uint8_t previousInputState;
  9.     uint8_t relayState;
  10.     time_t inputActiveSince;
  11.  
  12. } RelayControl_t;
  13.  
  14.  
  15. void RelayControlInit(RelayControl_t* ctrl)
  16. {
  17.     if (ctrl == NULL)
  18.         return;
  19.  
  20.     ctrl->previousInputState = 0;
  21.     ctrl->relayState = 0;
  22.     ctrl->inputActiveSince = 0;
  23. }
  24.  
  25.  
  26. int RelayControlUpdateWithTime(
  27.     RelayControl_t* ctrl,
  28.     uint8_t currentInputState,
  29.     time_t now)
  30. {
  31.     if (ctrl == NULL)
  32.         return -1;
  33.  
  34.     if (currentInputState == 0)
  35.     {
  36.         ctrl->previousInputState = 0;
  37.         ctrl->relayState = 0;
  38.         ctrl->inputActiveSince = 0;
  39.  
  40.         return ctrl->relayState;
  41.     }
  42.  
  43.     if (ctrl->previousInputState == 0)
  44.     {
  45.         ctrl->previousInputState = 1;
  46.         ctrl->inputActiveSince = now;
  47.  
  48.         return ctrl->relayState;
  49.     }
  50.  
  51.     if ((now - ctrl->inputActiveSince) >= RELAY_ON_DELAY_SEC)
  52.     {
  53.         ctrl->relayState = 1;
  54.     }
  55.  
  56.     return ctrl->relayState;
  57. }

Czas przekazujemy jako argument do funkcji RelayControlUpdateWithTime. Takie rozwiązanie jest bardzo wygodne podczas testowania. Nie musimy czekać tych 5 sekund. Tylko po po prostu wywołujemy funkcję z kolejnymi wartościami czasu. 

Testowanie kodu za pomocą Pythona:

Na początku wykorzystamy Pythona. 

Aby program w C udało się przetestować w Pythonie należy przygotować plik .dll:

  1. gcc -shared -o relay_controler.dll relay_controler.c

Teraz program testowy:

  1. import ctypes
  2. import unittest
  3.  
  4. # ładujemy bibliotekę DLL
  5. dll = ctypes.CDLL("./relay_controler.dll")

  6. # Struktura RelayControl_t. Trzymamy kolejność i typy danych
  7. class RelayControl(ctypes.Structure):
  8.     _fields_ = [
  9.         ("previousInputState", ctypes.c_uint8),  
  10.         ("relayState", ctypes.c_uint8),        
  11.         ("inputActiveSince", ctypes.c_longlong),
  12.     ]
  13.  
  14. # Funkcja dostaje wskaźnik do struktury RelayControl
  15. dll.RelayControlInit.argtypes = [
  16.     ctypes.POINTER(RelayControl)
  17. ]

  18. dll.RelayControlInit.restype = None
  19.  
  20.  
  21. # - Wskaźnik do struktury RelayControl
  22. # - Aktualny stan wejścia (0 lub 1)
  23. # - Aktualny czas w sekundach
  24. dll.RelayControlUpdateWithTime.argtypes = [
  25.     ctypes.POINTER(RelayControl),
  26.     ctypes.c_uint8,
  27.     ctypes.c_longlong
  28. ]
  29.  
  30. # Zwracany stan przekaźnika jako int
  31. dll.RelayControlUpdateWithTime.restype = ctypes.c_int
  32.  
  33.  
  34. # Klasa z testami
  35. class TestRelayControl(unittest.TestCase):
  36.  
  37.     # Wykonywanie pierwszej metody tzw. setUP. Startuje przed każdym testem
  38.     # Tworzy nową strukturę i ustawia stan początkowy
  39.     def setUp(self):
  40.         self.ctrl = RelayControl()
  41.         dll.RelayControlInit(ctypes.byref(self.ctrl))
  42.  
  43.     # Test: Sprawdzenie stanu początkowego przekaźnika
  44.     # Ustawione: Wejście nieaktywne
  45.   # Oczekiwane: Przekaźnik wyłączony
  46.     def test_initial_state(self):
  47.         result = dll.RelayControlUpdateWithTime(
  48.             ctypes.byref(self.ctrl),
  49.             0,
  50.             1000
  51.         )
  52.  
  53.         # Oczekiwany stan przekaźnika: 0 (OFF)
  54.         self.assertEqual(result, 0)
  55.  
  56.     # Test: Aktywacja wejścia w chwili 1000
  57.     # Oczekiwanie: Przekaźnik nie aktywny
  58.     def test_input_start(self):
  59.         result = dll.RelayControlUpdateWithTime(
  60.             ctypes.byref(self.ctrl),
  61.             1,
  62.             1000
  63.         )
  64.  
  65.         # Oczekiwany stan: 0 (OFF)
  66.         self.assertEqual(result, 0)
  67.  
  68.     # TEST 3
  69.     # Test: Sprawdzenie stanu po 4 sekundach aktywnego wejścia
  70.     # Oczekiwanie: Przekaźnik nie aktywny
  71.     def test_after_4_seconds(self):
  72.  
  73.         # Aktywacja wejścia w chwili 1000
  74.         dll.RelayControlUpdateWithTime(
  75.             ctypes.byref(self.ctrl),
  76.             1,
  77.             1000
  78.         )
  79.  
  80.         # Sprawdzenie stanu w chwili 1004
  81.         result = dll.RelayControlUpdateWithTime(
  82.             ctypes.byref(self.ctrl),
  83.             1,
  84.             1004
  85.         )
  86.  
  87.         # Przekaźnik powinien pozostać wyłączony
  88.         self.assertEqual(result, 0)
  89.  
  90.     # Test: Sprawdzenie stanu po 5 sekundach aktywnego wejścia
  91.     # Oczekiwanie: Przekaźnik ON po 5 sekundach
  92.     def test_after_5_seconds(self):
  93.  
  94.         # Aktywacja wejścia w chwili 1000
  95.         dll.RelayControlUpdateWithTime(
  96.             ctypes.byref(self.ctrl),
  97.             1,
  98.             1000
  99.         )
  100.  
  101.         # Sprawdzenie stanu w chwili 1005
  102.         result = dll.RelayControlUpdateWithTime(
  103.             ctypes.byref(self.ctrl),
  104.             1,
  105.             1005
  106.         )
  107.  
  108.         # Oczekiwany stan: 1 (ON)
  109.         self.assertEqual(result, 1)
  110.  
  111.     # Test: Sprawdzenie wyłączenia przekaźnika po zaniku sygnału
  112.     def test_input_off(self):
  113.  
  114.         # Aktywacja wejścia
  115.         dll.RelayControlUpdateWithTime(
  116.             ctypes.byref(self.ctrl),
  117.             1,
  118.             1000
  119.         )
  120.  
  121.         # Upływ 5 sekund - włączenie przekaźnika
  122.         dll.RelayControlUpdateWithTime(
  123.             ctypes.byref(self.ctrl),
  124.             1,
  125.             1005
  126.         )
  127.  
  128.         # Wyłączenie wejścia w chwili 1006
  129.         result = dll.RelayControlUpdateWithTime(
  130.             ctypes.byref(self.ctrl),
  131.             0,
  132.             1006
  133.         )
  134.  
  135.         # Przekaźnik powinien zostać wyłączony
  136.         self.assertEqual(result, 0)
  137.  
  138.  
  139. # Main uruchomienie wszystkich testów
  140. if __name__ == "__main__":
  141.     unittest.main()

Bibliotekę zewnętrzną ładujemy do Pythona przez moduł ctypes. Pozwala on na wywoływanie funkcji znajdujących się w bibliotece napisanej w C. 

Do przygotowania testów wykorzystamy standardowy moduł unittest. Przed każdym testem uruchamiana jest funkcja setUp. Dzięki odpowiedniej inicjalizacja wewnątrz tej funkcji każde testy rozpoczynamy z odpowiednim stanem początkowym. 

Testy uruchamiamy z konsoli:
  1. py relay_controler_test.py
Przykładowy wynik działania programu:


Jeśli test zakończy się niepowodzeniem, to w konsoli dostaniemy nazwę testu oraz różnicę pomiędzy wartością oczekiwaną a otrzymaną. 

Testowanie kodu za pomocą C:

Teraz zmienimy język funkcji testowych. Wszystkie testy wykonamy bezpośrednio w C. 

  1. #include <stdio.h>
  2. #include <time.h>
  3.  
  4. static int TestsPassed = 0;
  5. static int TestsFailed = 0;
  6.  
  7. #define CHECK_EQ(name, expression, expected)                     \
  8.     do {                                                         \
  9.         int actual = (expression);                               \
  10.         int exp = (expected);                                    \
  11.         if (actual == exp) {                                     \
  12.             printf("[PASS] %-50s actual=%d expected=%d\n",       \
  13.                    name, actual, exp);                            \
  14.             TestsPassed++;                                       \
  15.         } else {                                                 \
  16.             printf("[FAIL] %-50s actual=%d expected=%d\n",       \
  17.                    name, actual, exp);                            \
  18.             TestsFailed++;                                       \
  19.         }                                                        \
  20.     } while (0)
  21.  
  22. //Dodanie odnośnika do testowanego modułu
  23. #include "relay_controler.c"
  24.  
  25. //MAIN
  26. int main(void)
  27. {
  28.     RelayControl_t ctrl;
  29.  
  30.     printf("============================================================\n");
  31.     printf(" TEST relay_controler.c\n");
  32.     printf("============================================================\n\n");
  33.  
  34.     //Test1 - sprawdzenie stanu początkowego
  35.     printf("--- TEST 1: Inicjalizacja ---\n");
  36.     RelayControlInit(&ctrl);
  37.     CHECK_EQ(
  38.         "wejscie=0 -> przekaznik OFF",
  39.         RelayControlUpdateWithTime(&ctrl, 0, 1000),
  40.         0
  41.     );
  42.  
  43.      //Test2 - Pojawienie sie sygnalu
  44.     printf("\n--- TEST 2: Wejscie przechodzi na 1 ---\n");
  45.     CHECK_EQ(
  46.         "t=0s wejscie=1 -> przekaznik OFF",
  47.         RelayControlUpdateWithTime(&ctrl, 1, 1000),
  48.         0
  49.     );
  50.    
  51.      //Test3 - sprawdzenie stanu po sekundzie
  52.     printf("\n--- TEST 3: Po 1 sekundzie ---\n");
  53.     CHECK_EQ(
  54.         "t=1s wejscie=1 -> przekaznik OFF",
  55.         RelayControlUpdateWithTime(&ctrl, 1, 1001),
  56.         0
  57.     );
  58.    
  59.     //Test4 - sprawdzenie przed wartością graniczną
  60.     printf("\n--- TEST 4: Po 4 sekundach ---\n");
  61.     CHECK_EQ(
  62.         "t=4s wejscie=1 -> przekaznik OFF",
  63.         RelayControlUpdateWithTime(&ctrl, 1, 1004),
  64.         0
  65.     );
  66.  
  67.     //Test5 - Dokladnie 5 sekund
  68.     printf("\n--- TEST 5: Dokladnie 5 sekund ---\n");
  69.     CHECK_EQ(
  70.         "t=5s wejscie=1 -> przekaznik ON",
  71.         RelayControlUpdateWithTime(&ctrl, 1, 1005),
  72.         1
  73.     );
  74.  
  75.     //Test6 - sprawdzenie czy nadal aktywny
  76.     printf("\n--- TEST 6: Wejscie nadal aktywne ---\n");
  77.     CHECK_EQ(
  78.         "t=10s wejscie=1 -> przekaznik ON",
  79.         RelayControlUpdateWithTime(&ctrl, 1, 1010),
  80.         1
  81.     );
  82.  
  83.      //Test7 - zanik wejścia
  84.     printf("\n--- TEST 7: Zanik wejscia ---\n");
  85.     CHECK_EQ(
  86.         "wejscie=0 -> natychmiast OFF",
  87.         RelayControlUpdateWithTime(&ctrl, 0, 1011),
  88.         0
  89.     );
  90.  
  91.     //Test8 - ponowne włączenie czy licznik ruszy ponownie
  92.     printf("\n--- TEST 8: Ponowne zalaczenie wejscia ---\n");
  93.     CHECK_EQ(
  94.         "nowe wejscie=1 -> OFF",
  95.         RelayControlUpdateWithTime(&ctrl, 1, 1020),
  96.         0
  97.     );
  98.  
  99.     CHECK_EQ(
  100.         "po 4s -> nadal OFF",
  101.         RelayControlUpdateWithTime(&ctrl, 1, 1024),
  102.         0
  103.     );
  104.  
  105.     CHECK_EQ(
  106.         "po 5s -> ON",
  107.         RelayControlUpdateWithTime(&ctrl, 1, 1025),
  108.         1
  109.     );
  110.  
  111.     //Test9 - krótki impuls
  112.     printf("\n--- TEST 9: Krotki impuls wejscia ---\n");
  113.     RelayControlInit(&ctrl);
  114.     CHECK_EQ(
  115.         "impuls START",
  116.         RelayControlUpdateWithTime(&ctrl, 1, 2000),
  117.         0
  118.     );
  119.  
  120.     CHECK_EQ(
  121.         "impuls trwa 2s",
  122.         RelayControlUpdateWithTime(&ctrl, 1, 2002),
  123.         0
  124.     );
  125.  
  126.     CHECK_EQ(
  127.         "impuls znika przed 5s",
  128.         RelayControlUpdateWithTime(&ctrl, 0, 2003),
  129.         0
  130.     );
  131.  
  132.     //Test10 - po 5 sekundach od startu
  133.     printf("\n--- TEST 10: Ponowny start po krotkim impulsie ---\n");
  134.  
  135.     CHECK_EQ(
  136.         "nowy impuls START",
  137.         RelayControlUpdateWithTime(&ctrl, 1, 3000),
  138.         0
  139.     );
  140.  
  141.     CHECK_EQ(
  142.         "po 4s nadal OFF",
  143.         RelayControlUpdateWithTime(&ctrl, 1, 3004),
  144.         0
  145.     );
  146.  
  147.     CHECK_EQ(
  148.         "po 5s ON",
  149.         RelayControlUpdateWithTime(&ctrl, 1, 3005),
  150.         1
  151.     );
  152.  
  153.     //Test11 - puste
  154.     printf("\n--- TEST 11: NULL ---\n");
  155.  
  156.     CHECK_EQ(
  157.         "ctrl=NULL",
  158.         RelayControlUpdateWithTime(NULL, 1, 1000),
  159.         -1
  160.     );
  161.  
  162.     printf("\n");
  163.     printf("############################################################\n");
  164.     printf(" PODSUMOWANIE TESTOW\n");
  165.     printf("############################################################\n");
  166.  
  167.     printf(" PASS : %d\n", TestsPassed);
  168.     printf(" FAIL : %d\n", TestsFailed);
  169.     printf(" TOTAL: %d\n", TestsPassed + TestsFailed);
  170.  
  171.     printf("############################################################\n");
  172.  
  173.     return TestsFailed == 0 ? 0 : 1;
  174. }

Tutaj tworzymy osobny program w C. Wywołuje on testowane funkcje i porównuje ich wynik z oczekiwaną wartością za pomocą makra CHECK_EQ.

Wykonuje ono wskazane wyrażenie, a następnie porównuje jego wynik z oczekiwaną wartością. 

Przed uruchomieniem dobrze do skompilować do formatu .exe. Wykonywałem to z poziomu konsoli mingw (MSYS2 UCRT64):

  1. gcc -Wall -Wextra relay_controler_test.c -o relay_controler_test.exe

  1. ============================================================
  2.  TEST relay_controler.c
  3. ============================================================
  4.  
  5. --- TEST 1: Inicjalizacja ---
  6. [PASS] wejscie=0 -> przekaznik OFF                        actual=0 expected=0
  7.  
  8. --- TEST 2: Wejscie przechodzi na 1 ---
  9. [PASS] t=0s wejscie=1 -> przekaznik OFF                   actual=0 expected=0
  10.  
  11. --- TEST 3: Po 1 sekundzie ---
  12. [PASS] t=1s wejscie=1 -> przekaznik OFF                   actual=0 expected=0
  13.  
  14. --- TEST 4: Po 4 sekundach ---
  15. [PASS] t=4s wejscie=1 -> przekaznik OFF                   actual=0 expected=0
  16.  
  17. --- TEST 5: Dokladnie 5 sekund ---
  18. [PASS] t=5s wejscie=1 -> przekaznik ON                    actual=1 expected=1
  19.  
  20. --- TEST 6: Wejscie nadal aktywne ---
  21. [PASS] t=10s wejscie=1 -> przekaznik ON                   actual=1 expected=1
  22.  
  23. --- TEST 7: Zanik wejscia ---
  24. [PASS] wejscie=0 -> natychmiast OFF                       actual=0 expected=0
  25.  
  26. --- TEST 8: Ponowne zalaczenie wejscia ---
  27. [PASS] nowe wejscie=1 -> OFF                              actual=0 expected=0
  28. [PASS] po 4s -> nadal OFF                                 actual=0 expected=0
  29. [PASS] po 5s -> ON                                        actual=1 expected=1
  30.  
  31. --- TEST 9: Krotki impuls wejscia ---
  32. [PASS] impuls START                                       actual=0 expected=0
  33. [PASS] impuls trwa 2s                                     actual=0 expected=0
  34. [PASS] impuls znika przed 5s                              actual=0 expected=0
  35.  
  36. --- TEST 10: Ponowny start po krotkim impulsie ---
  37. [PASS] nowy impuls START                                  actual=0 expected=0
  38. [PASS] po 4s nadal OFF                                    actual=0 expected=0
  39. [PASS] po 5s ON                                           actual=1 expected=1
  40.  
  41. --- TEST 11: NULL ---
  42. [PASS] ctrl=NULL                                          actual=-1 expected=-1
  43.  
  44. ############################################################
  45.  PODSUMOWANIE TESTOW
  46. ############################################################
  47.  PASS : 17
  48.  FAIL : 0
  49.  TOTAL: 17
  50. ############################################################

Python czy C?


No to jak te testy w końcu wykonywać. W obu podejściach testujemy ten sam moduł. Różnica polega głównie na sposobie ich pisania. 

Testy w Pythonie wydają się bardziej czytelne. Dodawane są wygodniejsze narzędzia do ich tworzenia, grupowania itp. Dzięki modułowi unittest otrzymujemy gotowe mechanizmy do tworzenia przypadków testowych. Moduł ctypes nie ma konieczności przepisywania logiki do Pythona. Natoamist przed rozpoczęciem testów należy wcześniej przygotować odpowiednią bibliotekę oraz poprawnie przygotować dane do wzorców funkcji w Pythonie. 

W C przykład testów był dosyć okrojony, dalej możemy używać bardziej skomplikowanych narzędzi jak Unity, CMock czy CUnit.. Oferują one gotowe mechanizmy asercji, organizacji testów itp. 

Należy oczywiście pamiętać, że uruchomienie testów na Windows pozwala szybko zweryfikować algorytmy i reakcje na określone dane wejściowe. Nie zastąpi sprawdzenia działania programu na rzeczywistym sprzęcie, szczególnie gdy kod korzysta z przerwań, timerów, rejestrów sprzętowych czy systemu RTOS.

Tak więc samo środowisko ma drugorzędne znaczenie. Najlepiej jak wybierzemy jedno i się nauczymy go poprawnie używać. Skupimy się na poprawnym przygotowaniu scenariuszy testowych, które będą uwzględniały poprawne działanie programu, sytuacje graniczne czy błędne dane. Jakość testów na pierwszym miejscu. Wybór język zostawmy własnym preferencją.