Test-driven development (TDD) to metodyka programowania, w której testy jednostkowe powstają przed implementacją kodu, dzięki czemu błędy wykrywane są szybciej, a stabilność oprogramowania rośnie.
W robotyce i elektronice, gdzie kod działa w świecie fizycznym i musi reagować przewidywalnie na sygnały z sensorów oraz sterować aktuatorami, TDD minimalizuje awarie i przyspiesza rozwój złożonych systemów.
Czym jest test-driven development?
TDD, znana również jako rozwój sterowany testami, to technika tworzenia oprogramowania zaliczana do metodyk zwinnych, wywodząca się z Extreme Programming. Kent Beck spopularyzował to podejście, podkreślając, że przed napisaniem funkcjonalności programista tworzy automatyczny test, który na początku musi się nie powieść.
W przeciwieństwie do tradycyjnego kodowania, gdzie testy pisze się po fakcie, TDD odwraca kolejność: testy prowadzą rozwój kodu. Pisząc testy przed kodem, uzyskujesz wysokie pokrycie kodu testami i istotnie zmniejszasz liczbę defektów.
Korzyści TDD w robotyce i elektronice
Stosowanie TDD w projektach robotycznych i embedded przynosi konkretne zalety:
- szybkie wykrywanie błędów – testy jednostkowe symulują warunki pracy robota, np. odczyt z akcelerometru, zanim kod trafi na fizyczne urządzenie;
- łatwiejsza konserwacja – dobrze przetestowany kod ułatwia modyfikacje, np. dodanie nowego modułu sterowania LED-ami w robocie;
- lepsza dokumentacja – testy działają jak specyfikacja – pokazują, jak sensor odległości powinien reagować na przeszkody;
- zmniejszona liczba defektów – badania wskazują, że TDD redukuje błędy i poprawia utrzymanie kodu w złożonych systemach, kluczowych dla robotyki;
- elastyczność – umożliwia małe kroki rozwoju, istotne przy iteracyjnym projektowaniu robotów, gdzie wymagania zmieniają się po testach poligonowych.
W elektronice TDD chroni firmware i sprzęt – błędy wychwycone w testach rzadziej kończą się uszkodzeniem hardware’u.
Cykl życia TDD – czerwony–zielony–refaktoryzacja
Podstawą TDD jest iteracyjny cykl czerwony–zielony–refaktoryzacja, powtarzany dla każdej nowej funkcjonalności. To krótka, zdyscyplinowana pętla, która długofalowo daje czystszy i stabilniejszy kod.
- Czerwony – napisz test jednostkowy dla nieistniejącej jeszcze funkcji; test musi się nie powieść (brak implementacji) i potwierdzić, że narzędzie testowe działa;
- Zielony – napisz minimalny kod, by test przeszedł; nie optymalizuj, skup się wyłącznie na działaniu;
- Refaktoryzacja – popraw czytelność i wydajność, usuń duplikaty i zachowaj zielone testy. Uruchom komplet testów dla potwierdzenia stabilności.
W TDD wyróżnia się testy jednostkowe (sprawdzające moduły od środka) oraz testy funkcjonalne (z perspektywy użytkownika/robota).
Przykłady TDD w robotyce i elektronice
Aby zilustrować TDD, posłużymy się Pythonem (popularnym w robotyce dzięki bibliotekom jak RPi.GPIO czy PyRobot). Załóżmy rozwój softu dla robota mobilnego z sensorem ultradźwiękowym HC-SR04 do unikania przeszkód oraz sterowaniem silnikami DC.
Przykład 1 – kalkulator odległości z sensora (podstawowy TDD)
Chcemy funkcji obliczającej odległość na podstawie czasu echa. Użyjemy frameworka unittest. Najpierw tworzymy test dla funkcji oblicz_odleglosc(czas_echa, predkosc_dzwieku):
import unittest
class TestSensor(unittest.TestCase):
def test_oblicz_odleglosc(self):
self.assertEqual(oblicz_odleglosc(0.01, 343), 171.5) # Oczekiwana odległość: 171,5 cm
Test nie przejdzie – funkcja nie istnieje (czerwony). Następnie dodajemy minimalną implementację:
def oblicz_odleglosc(czas_echa, predkosc_dzwieku=343):
return (predkosc_dzwieku * czas_echa) / 2 * 100 # Odległość w cm
Test przechodzi (zielony). Na koniec refaktoryzujemy – walidujemy wejście i usuwamy magiczne liczby:
def oblicz_odleglosc(czas_echa, predkosc_dzwieku=343):
if czas_echa <= 0:
raise ValueError("Czas echa musi być dodatni")
return (predkosc_dzwieku * czas_echa / 2) * 100
Testy przechodzą, a funkcja jest gotowa do integracji z robotem. W robotyce taki test symuluje rzeczywisty odczyt sensora i zapobiega błędom, np. dzieleniu przez zero.
Przykład 2 – sterowanie silnikiem z unikaniem przeszkód (zaawansowany)
Wymaganie: jeśli odległość spadnie poniżej 20 cm, zatrzymaj silniki. Użyjemy mocków do symulacji hardware’u i napiszemy test dla klasy KontrolerRobota:
class TestKontrolerRobota(unittest.TestCase):
def test_zatrzymaj_przy_przeszkodzie(self):
sensor = MockSensor(15) # Symulacja odległości 15 cm
silnik = MockSilnik()
kontroler = KontrolerRobota(sensor, silnik)
kontroler.aktualizuj()
self.assertTrue(silnik.zatrzymany)
Implementujemy minimalne zachowanie, by test przeszedł:
class KontrolerRobota:
def __init__(self, sensor, silnik):
self.sensor = sensor
self.silnik = silnik
def aktualizuj(self):
odleglosc = self.sensor.odczytaj()
if odleglosc < 20:
self.silnik.zatrzymaj()
Po przejściu testu refaktoryzujemy – dodajemy jazdę do przodu, gdy droga wolna, oraz obsługę wyjątków:
class KontrolerRobota:
PROG_PRZESZKODY = 20 # cm
def __init__(self, sensor, silnik):
self.sensor = sensor
self.silnik = silnik
def aktualizuj(self):
try:
odleglosc = self.sensor.odczytaj()
if odleglosc < self.PROG_PRZESZKODY:
self.silnik.zatrzymaj()
else:
self.silnik.jedz_do_przodu()
except ValueError:
self.silnik.zatrzymaj() # Bezpieczeństwo na wypadek błędu sensora
Dzięki TDD zyskujesz zestaw testów regresyjnych, który chroni logikę sterowania – nawet po podmianie mocków na GPIO na Raspberry Pi robot nie powinien „wjechać w ścianę”.
Przykład 3 – symulacja PID dla regulatora prędkości
W elektronice robotycznej regulator PID (proporcjonalno–całkująco–różniczkujący) stabilizuje prędkość silników. Cykl czerwony–zielony–refaktoryzacja dla funkcji regulator_pid(blad, kp=1, ki=0, kd=0) pozwala testować stabilność dla zadanej prędkości, np. 10 cm/s, bez fizycznego robota.
Testy weryfikują brak oscylacji i przeregulowań, co jest krytyczne dla autonomicznej jazdy.
Jak wdrożyć TDD w projekcie robotycznym?
- Wybierz framework testowy – dobór zależy od języka i platformy. Najczęstsze zestawy:
| Język/Platforma | Framework testowy | Uwagi |
|---|---|---|
| Python | unittest, pytest |
łatwa parametryzacja, bogaty ekosystem |
| C++ (Arduino/STM32) | Google Test | dobry do testów logiki niezależnej od HAL |
| Rust | cargo test |
wbudowane wsparcie i szybka kompilacja |
- Zacznij od małych funkcji – testuj sensory i aktory (IO), a dopiero potem logikę integrującą;
- Włącz testy w CI/CD – uruchamiaj je automatycznie przed wdrożeniem na mikrokontroler lub SBC;
- Mierz pokrycie testami – celuj w >80%, używając narzędzi takich jak
coverage.py; - Radź sobie z hardware’em – symuluj urządzenia mockami i emulatorami; utrzymuj dyscyplinę TDD od pierwszego dnia projektu.