Sobes.tech
Middle

Jak testy i metodologia TDD wpływają na organizację kodu?

sobes.tech AI

Odpowiedź od AI

Testy i TDD mają istotny wpływ na organizację kodu:

  1. Dezaktywacja i modularność: Pisanie testów przed lub w trakcie rozwoju zachęca do tworzenia małych, łatwych do przetestowania funkcji i komponentów, co poprawia modularność i zmniejsza powiązania w kodzie.

  2. Czystość i czytelność: Testy służą jako żywa dokumentacja i przykłady użycia kodu. Trudno jest napisać testy dla źle zaprojektowanego, zagmatwanego (kod spaghetti) kodu. To motywuje do pisania czystszego i bardziej zrozumiałego kodu.

  3. Ulepszanie API: Proces pisania testów zmusza do korzystania z API tworzonych modułów z zewnątrz. Pomaga to wykryć niewygodne lub nielogiczne części API na wczesnym etapie i poprawić je przed szerokim użyciem.

  4. Wsparcie refaktoryzacji: Posiadanie pełnego zestawu testów daje pewność podczas refaktoryzacji. Testy szybko wykrywają regresje, umożliwiając bezpieczne zmiany wewnętrznej struktury kodu bez naruszania jego zewnętrznego zachowania.

  5. Szybkie wykrywanie błędów: Testy pozwalają wykrywać błędy na wczesnych etapach rozwoju, co znacznie obniża koszty ich naprawy w porównaniu do wykrywania ich w produkcji.

Przykład organizacji kodu inspirowanej TDD (w Golang):

package calculator // Pakiet dla konkretnego funkcjonalności

import "errors" // Zależności

// Add dodaje dwa całkowite liczby.
func Add(a, b int) int {
	return a + b // Prosta logika
}

// Divide dzieli num przez den. Zwraca błąd, jeśli den jest 0.
func Divide(num, den int) (int, error) {
	if den == 0 {
		// Jawne obsługiwanie przypadków brzegowych, łatwe do przetestowania
		return 0, errors.New("dzielenie przez zero niedozwolone")
	}
	return num / den, nil
}

Odpowiednie testy:

package calculator // Testy w tym samym pakiecie, ale w osobnym pliku (_test.go)

import (
	"testing"
)

// TestAdd sprawdza funkcję Add.
func TestAdd(t *testing.T) {
	// Przypadki testowe dla różnych danych wejściowych
	testCases := []struct {
		name   string
		a, b   int
		expected int
	}{
		{"Dodatnie liczby", 1, 2, 3},
		{"Ujemne liczby", -1, -2, -3},
		{"Mieszane liczby", -1, 2, 1},
		{"Zero i dodatnie", 0, 5, 5},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) { // Użycie t.Run do strukturyzacji testów
			result := Add(tc.a, tc.b) // Wywołanie testowanej funkcji
			if result != tc.expected {
				// Jasny komunikat o błędzie
				t.Errorf("Add(%d, %d): oczekiwano %d, otrzymano %d", tc.a, tc.b, tc.expected, result)
			}
		})
	}
}

// TestDivide sprawdza funkcję Divide.
func TestDivide(t *testing.T) {
	testCases := []struct {
		name        string
		num, den    int
		expected int
		expectError bool
	}{
		{"Pozytywne dzielenie", 10, 2, 5, false},
		{"Negatywny wynik", 10, -2, -5, false},
		{"Dzielenie przez jeden", 7, 1, 7, false},
		{"Dzielenie przez zero", 5, 0, 0, true}, // przypadek błędu
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			result, err := Divide(tc.num, tc.den)

			if tc.expectError {
				if err == nil {
					// Sprawdzenie oczekiwanego błędu
					t.Errorf("Divide(%d, %d): oczekiwano błędu, ale go nie było", tc.num, tc.den)
				}
			} else {
				if err != nil {
					// Sprawdzenie braku błędu
					t.Errorf("Divide(%d, %d): nieoczekiwany błąd: %v", tc.num, tc.den, err)
				}
				if result != tc.expected {
					// Sprawdzenie wyniku w przypadku sukcesu
					t.Errorf("Divide(%d, %d): oczekiwano %d, otrzymano %d", tc.num, tc.den, tc.expected, result)
				}
			}
		})
	}
}