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.
- #include <stdint.h>
- #include <time.h>
- #define RELAY_ON_DELAY_SEC 5
- typedef struct
- {
- uint8_t previousInputState;
- uint8_t relayState;
- time_t inputActiveSince;
- } RelayControl_t;
- void RelayControlInit(RelayControl_t* ctrl)
- {
- if (ctrl == NULL)
- return;
- ctrl->previousInputState = 0;
- ctrl->relayState = 0;
- ctrl->inputActiveSince = 0;
- }
- int RelayControlUpdateWithTime(
- RelayControl_t* ctrl,
- uint8_t currentInputState,
- time_t now)
- {
- if (ctrl == NULL)
- return -1;
- if (currentInputState == 0)
- {
- ctrl->previousInputState = 0;
- ctrl->relayState = 0;
- ctrl->inputActiveSince = 0;
- return ctrl->relayState;
- }
- if (ctrl->previousInputState == 0)
- {
- ctrl->previousInputState = 1;
- ctrl->inputActiveSince = now;
- return ctrl->relayState;
- }
- if ((now - ctrl->inputActiveSince) >= RELAY_ON_DELAY_SEC)
- {
- ctrl->relayState = 1;
- }
- return ctrl->relayState;
- }
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:
- gcc -shared -o relay_controler.dll relay_controler.c
Teraz program testowy:
- import ctypes
- import unittest
- # ładujemy bibliotekę DLL
- dll = ctypes.CDLL("./relay_controler.dll")
- # Struktura RelayControl_t. Trzymamy kolejność i typy danych
- class RelayControl(ctypes.Structure):
- _fields_ = [
- ("previousInputState", ctypes.c_uint8),
- ("relayState", ctypes.c_uint8),
- ("inputActiveSince", ctypes.c_longlong),
- ]
- # Funkcja dostaje wskaźnik do struktury RelayControl
- dll.RelayControlInit.argtypes = [
- ctypes.POINTER(RelayControl)
- ]
- dll.RelayControlInit.restype = None
- # - Wskaźnik do struktury RelayControl
- # - Aktualny stan wejścia (0 lub 1)
- # - Aktualny czas w sekundach
- dll.RelayControlUpdateWithTime.argtypes = [
- ctypes.POINTER(RelayControl),
- ctypes.c_uint8,
- ctypes.c_longlong
- ]
- # Zwracany stan przekaźnika jako int
- dll.RelayControlUpdateWithTime.restype = ctypes.c_int
- # Klasa z testami
- class TestRelayControl(unittest.TestCase):
- # Wykonywanie pierwszej metody tzw. setUP. Startuje przed każdym testem
- # Tworzy nową strukturę i ustawia stan początkowy
- def setUp(self):
- self.ctrl = RelayControl()
- dll.RelayControlInit(ctypes.byref(self.ctrl))
- # Test: Sprawdzenie stanu początkowego przekaźnika
- # Ustawione: Wejście nieaktywne
- # Oczekiwane: Przekaźnik wyłączony
- def test_initial_state(self):
- result = dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 0,
- 1000
- )
- # Oczekiwany stan przekaźnika: 0 (OFF)
- self.assertEqual(result, 0)
- # Test: Aktywacja wejścia w chwili 1000
- # Oczekiwanie: Przekaźnik nie aktywny
- def test_input_start(self):
- result = dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1000
- )
- # Oczekiwany stan: 0 (OFF)
- self.assertEqual(result, 0)
- # TEST 3
- # Test: Sprawdzenie stanu po 4 sekundach aktywnego wejścia
- # Oczekiwanie: Przekaźnik nie aktywny
- def test_after_4_seconds(self):
- # Aktywacja wejścia w chwili 1000
- dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1000
- )
- # Sprawdzenie stanu w chwili 1004
- result = dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1004
- )
- # Przekaźnik powinien pozostać wyłączony
- self.assertEqual(result, 0)
- # Test: Sprawdzenie stanu po 5 sekundach aktywnego wejścia
- # Oczekiwanie: Przekaźnik ON po 5 sekundach
- def test_after_5_seconds(self):
- # Aktywacja wejścia w chwili 1000
- dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1000
- )
- # Sprawdzenie stanu w chwili 1005
- result = dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1005
- )
- # Oczekiwany stan: 1 (ON)
- self.assertEqual(result, 1)
- # Test: Sprawdzenie wyłączenia przekaźnika po zaniku sygnału
- def test_input_off(self):
- # Aktywacja wejścia
- dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1000
- )
- # Upływ 5 sekund - włączenie przekaźnika
- dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 1,
- 1005
- )
- # Wyłączenie wejścia w chwili 1006
- result = dll.RelayControlUpdateWithTime(
- ctypes.byref(self.ctrl),
- 0,
- 1006
- )
- # Przekaźnik powinien zostać wyłączony
- self.assertEqual(result, 0)
- # Main uruchomienie wszystkich testów
- if __name__ == "__main__":
- 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:
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.
- #include <stdio.h>
- #include <time.h>
- static int TestsPassed = 0;
- static int TestsFailed = 0;
- #define CHECK_EQ(name, expression, expected) \
- do { \
- int actual = (expression); \
- int exp = (expected); \
- if (actual == exp) { \
- printf("[PASS] %-50s actual=%d expected=%d\n", \
- name, actual, exp); \
- TestsPassed++; \
- } else { \
- printf("[FAIL] %-50s actual=%d expected=%d\n", \
- name, actual, exp); \
- TestsFailed++; \
- } \
- } while (0)
- //Dodanie odnośnika do testowanego modułu
- #include "relay_controler.c"
- //MAIN
- int main(void)
- {
- RelayControl_t ctrl;
- printf("============================================================\n");
- printf(" TEST relay_controler.c\n");
- printf("============================================================\n\n");
- //Test1 - sprawdzenie stanu początkowego
- printf("--- TEST 1: Inicjalizacja ---\n");
- RelayControlInit(&ctrl);
- CHECK_EQ(
- "wejscie=0 -> przekaznik OFF",
- RelayControlUpdateWithTime(&ctrl, 0, 1000),
- 0
- );
- //Test2 - Pojawienie sie sygnalu
- printf("\n--- TEST 2: Wejscie przechodzi na 1 ---\n");
- CHECK_EQ(
- "t=0s wejscie=1 -> przekaznik OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 1000),
- 0
- );
- //Test3 - sprawdzenie stanu po sekundzie
- printf("\n--- TEST 3: Po 1 sekundzie ---\n");
- CHECK_EQ(
- "t=1s wejscie=1 -> przekaznik OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 1001),
- 0
- );
- //Test4 - sprawdzenie przed wartością graniczną
- printf("\n--- TEST 4: Po 4 sekundach ---\n");
- CHECK_EQ(
- "t=4s wejscie=1 -> przekaznik OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 1004),
- 0
- );
- //Test5 - Dokladnie 5 sekund
- printf("\n--- TEST 5: Dokladnie 5 sekund ---\n");
- CHECK_EQ(
- "t=5s wejscie=1 -> przekaznik ON",
- RelayControlUpdateWithTime(&ctrl, 1, 1005),
- 1
- );
- //Test6 - sprawdzenie czy nadal aktywny
- printf("\n--- TEST 6: Wejscie nadal aktywne ---\n");
- CHECK_EQ(
- "t=10s wejscie=1 -> przekaznik ON",
- RelayControlUpdateWithTime(&ctrl, 1, 1010),
- 1
- );
- //Test7 - zanik wejścia
- printf("\n--- TEST 7: Zanik wejscia ---\n");
- CHECK_EQ(
- "wejscie=0 -> natychmiast OFF",
- RelayControlUpdateWithTime(&ctrl, 0, 1011),
- 0
- );
- //Test8 - ponowne włączenie czy licznik ruszy ponownie
- printf("\n--- TEST 8: Ponowne zalaczenie wejscia ---\n");
- CHECK_EQ(
- "nowe wejscie=1 -> OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 1020),
- 0
- );
- CHECK_EQ(
- "po 4s -> nadal OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 1024),
- 0
- );
- CHECK_EQ(
- "po 5s -> ON",
- RelayControlUpdateWithTime(&ctrl, 1, 1025),
- 1
- );
- //Test9 - krótki impuls
- printf("\n--- TEST 9: Krotki impuls wejscia ---\n");
- RelayControlInit(&ctrl);
- CHECK_EQ(
- "impuls START",
- RelayControlUpdateWithTime(&ctrl, 1, 2000),
- 0
- );
- CHECK_EQ(
- "impuls trwa 2s",
- RelayControlUpdateWithTime(&ctrl, 1, 2002),
- 0
- );
- CHECK_EQ(
- "impuls znika przed 5s",
- RelayControlUpdateWithTime(&ctrl, 0, 2003),
- 0
- );
- //Test10 - po 5 sekundach od startu
- printf("\n--- TEST 10: Ponowny start po krotkim impulsie ---\n");
- CHECK_EQ(
- "nowy impuls START",
- RelayControlUpdateWithTime(&ctrl, 1, 3000),
- 0
- );
- CHECK_EQ(
- "po 4s nadal OFF",
- RelayControlUpdateWithTime(&ctrl, 1, 3004),
- 0
- );
- CHECK_EQ(
- "po 5s ON",
- RelayControlUpdateWithTime(&ctrl, 1, 3005),
- 1
- );
- //Test11 - puste
- printf("\n--- TEST 11: NULL ---\n");
- CHECK_EQ(
- "ctrl=NULL",
- RelayControlUpdateWithTime(NULL, 1, 1000),
- -1
- );
- printf("\n");
- printf("############################################################\n");
- printf(" PODSUMOWANIE TESTOW\n");
- printf("############################################################\n");
- printf(" PASS : %d\n", TestsPassed);
- printf(" FAIL : %d\n", TestsFailed);
- printf(" TOTAL: %d\n", TestsPassed + TestsFailed);
- printf("############################################################\n");
- return TestsFailed == 0 ? 0 : 1;
- }
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):
- gcc -Wall -Wextra relay_controler_test.c -o relay_controler_test.exe
- ============================================================
- TEST relay_controler.c
- ============================================================
- --- TEST 1: Inicjalizacja ---
- [PASS] wejscie=0 -> przekaznik OFF actual=0 expected=0
- --- TEST 2: Wejscie przechodzi na 1 ---
- [PASS] t=0s wejscie=1 -> przekaznik OFF actual=0 expected=0
- --- TEST 3: Po 1 sekundzie ---
- [PASS] t=1s wejscie=1 -> przekaznik OFF actual=0 expected=0
- --- TEST 4: Po 4 sekundach ---
- [PASS] t=4s wejscie=1 -> przekaznik OFF actual=0 expected=0
- --- TEST 5: Dokladnie 5 sekund ---
- [PASS] t=5s wejscie=1 -> przekaznik ON actual=1 expected=1
- --- TEST 6: Wejscie nadal aktywne ---
- [PASS] t=10s wejscie=1 -> przekaznik ON actual=1 expected=1
- --- TEST 7: Zanik wejscia ---
- [PASS] wejscie=0 -> natychmiast OFF actual=0 expected=0
- --- TEST 8: Ponowne zalaczenie wejscia ---
- [PASS] nowe wejscie=1 -> OFF actual=0 expected=0
- [PASS] po 4s -> nadal OFF actual=0 expected=0
- [PASS] po 5s -> ON actual=1 expected=1
- --- TEST 9: Krotki impuls wejscia ---
- [PASS] impuls START actual=0 expected=0
- [PASS] impuls trwa 2s actual=0 expected=0
- [PASS] impuls znika przed 5s actual=0 expected=0
- --- TEST 10: Ponowny start po krotkim impulsie ---
- [PASS] nowy impuls START actual=0 expected=0
- [PASS] po 4s nadal OFF actual=0 expected=0
- [PASS] po 5s ON actual=1 expected=1
- --- TEST 11: NULL ---
- [PASS] ctrl=NULL actual=-1 expected=-1
- ############################################################
- PODSUMOWANIE TESTOW
- ############################################################
- PASS : 17
- FAIL : 0
- TOTAL: 17
- ############################################################
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ą.

