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.

  1. Czerwony – napisz test jednostkowy dla nieistniejącej jeszcze funkcji; test musi się nie powieść (brak implementacji) i potwierdzić, że narzędzie testowe działa;
  2. Zielony – napisz minimalny kod, by test przeszedł; nie optymalizuj, skup się wyłącznie na działaniu;
  3. 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?

  1. 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
  1. Zacznij od małych funkcji – testuj sensory i aktory (IO), a dopiero potem logikę integrującą;
  2. Włącz testy w CI/CD – uruchamiaj je automatycznie przed wdrożeniem na mikrokontroler lub SBC;
  3. Mierz pokrycie testami – celuj w >80%, używając narzędzi takich jak coverage.py;
  4. Radź sobie z hardware’em – symuluj urządzenia mockami i emulatorami; utrzymuj dyscyplinę TDD od pierwszego dnia projektu.