JUnit 4 – pisanie pierwszych testów jednostkowych

TL;DR: JUnit 4 to najpopularniejszy framework do testów jednostkowych w Javie. Pozwala automatycznie sprawdzać czy kod działa poprawnie. Używasz adnotacji @Test, asercji like assertEquals(), oraz setup/teardown z @Before/@After. Testy to podstawa profesjonalnego rozwoju oprogramowania.

Dlaczego testy jednostkowe to podstawa każdej aplikacji

**Bez testów Twoja aplikacja to dom z kart – jeden błąd może zburzyć wszystko.** Testy jednostkowe to automatyczna siatka bezpieczeństwa, która wykrywa błędy od razu, nie po tygodniach w produkcji. **Profesjonalni programiści nie przedstawiają kodu bez testów** – to jak lekarz operujący bez rękawiczek.

Co się nauczysz:

  • Czym są testy jednostkowe i dlaczego są ważne
  • Jak skonfigurować JUnit 4 w projekcie Java
  • Pisanie pierwszych testów z adnotacjami @Test, @Before, @After
  • Używanie asercji – assertEquals, assertTrue, assertNull
  • Testowanie wyjątków i edge cases
  • Najlepsze praktyki nazewnictwa i organizacji testów
Wymagania wstępne: Podstawowa znajomość Java (klasy, metody, wyjątki), umiejętność korzystania z IDE jak IntelliJ czy Eclipse. Znajomość Maven będzie pomocna.

Czym są testy jednostkowe?

Test jednostkowy to automatyczny test sprawdzający czy pojedyncza jednostka kodu (najczęściej metoda) działa zgodnie z oczekiwaniami.
Test jednostkowy to jak kontrola jakości w fabryce – każdy wyprodukowany element jest sprawdzany czy spełnia standardy przed wysłaniem do klienta.

**Zalety testów jednostkowych:**
– **Wczesne wykrywanie błędów** – łatwiej naprawić błąd od razu niż po tygodniu
– **Dokumentacja kodu** – testy pokazują jak kod powinien być używany
– **Bezpieczny refactoring** – możesz zmieniać kod z pewnością że nic nie zepsujesz
– **Szybsza implementacja** – nie musisz ręcznie testować każdej zmiany

Konfiguracja JUnit 4 w projekcie

Maven dependency (pom.xml)



    
    4.0.0
    com.example
    junit-example
    1.0-SNAPSHOT
    
    
        1.7
        1.7
    
    
    
        
            junit
            junit
            4.12
            test
        
    
    

Struktura katalogów

src/
├── main/
│   └── java/
│       └── com/example/
│           ├── Calculator.java
│           └── BankAccount.java
└── test/
    └── java/
        └── com/example/
            ├── CalculatorTest.java
            └── BankAccountTest.java
Pro tip: Zawsze używaj tej samej struktury pakietów w src/test/java jak w src/main/java. To standard i ułatwia nawigację.

Pierwszy test JUnit 4

Zacznijmy od prostej klasy do testowania:

// src/main/java/com/example/Calculator.java
public class Calculator {
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public int subtract(int a, int b) {
        return a - b;
    }
    
    public int multiply(int a, int b) {
        return a * b;
    }
    
    public double divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Nie można dzielić przez zero");
        }
        return (double) a / b;
    }
    
    public boolean isEven(int number) {
        return number % 2 == 0;
    }
}

A teraz pierwszy test:

// src/test/java/com/example/CalculatorTest.java
import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {
    
    @Test
    public void shouldAddTwoPositiveNumbers() {
        // Given - przygotowanie danych testowych
        Calculator calculator = new Calculator();
        
        // When - wykonanie testowanej metody
        int result = calculator.add(5, 3);
        
        // Then - sprawdzenie rezultatu
        assertEquals(8, result);
    }
    
    @Test
    public void shouldAddNegativeNumbers() {
        Calculator calculator = new Calculator();
        
        int result = calculator.add(-5, -3);
        
        assertEquals(-8, result);
    }
    
    @Test
    public void shouldSubtractNumbers() {
        Calculator calculator = new Calculator();
        
        int result = calculator.subtract(10, 4);
        
        assertEquals(6, result);
    }
}
Given-When-Then pattern: Popularna struktura testów – Given (przygotuj dane), When (wykonaj akcję), Then (sprawdź rezultat). Nie jest obowiązkowa, ale czyni testy bardziej czytelnymi.

Podstawowe asercje JUnit 4

import org.junit.Test;
import static org.junit.Assert.*;

public class AssertionsExampleTest {
    
    @Test
    public void demonstrateBasicAssertions() {
        Calculator calc = new Calculator();
        
        // Równość obiektów/prymitywów
        assertEquals("Dodawanie nie działa", 8, calc.add(5, 3));
        assertEquals(8, calc.add(5, 3)); // Bez niestandardowej wiadomości
        
        // Równość z tolerancją dla double/float
        assertEquals(2.5, calc.divide(5, 2), 0.001);
        
        // Boolean assertions
        assertTrue("5 powinno być nieparzyste", calc.isEven(4));
        assertFalse(calc.isEven(5));
        
        // Null assertions
        String text = null;
        assertNull("Text powinien być null", text);
        
        text = "Hello";
        assertNotNull("Text nie powinien być null", text);
        
        // Same/Not Same (referencje obiektów)
        Calculator calc1 = new Calculator();
        Calculator calc2 = new Calculator();
        Calculator calc3 = calc1;
        
        assertSame(calc1, calc3); // Ta sama referencja
        assertNotSame(calc1, calc2); // Różne obiekty
        
        // Arrays
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};
        assertArrayEquals(expected, actual);
    }
    
    @Test
    public void demonstrateStringAssertions() {
        String greeting = "Hello World";
        
        assertEquals("Hello World", greeting);
        assertTrue(greeting.contains("World"));
        assertTrue(greeting.startsWith("Hello"));
        assertTrue(greeting.endsWith("World"));
    }
}

Testowanie wyjątków

public class ExceptionTestingTest {
    
    // Sposób 1: expected parameter (najprostrzy)
    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionWhenDividingByZero() {
        Calculator calculator = new Calculator();
        
        calculator.divide(10, 0); // Powinno rzucić wyjątek
    }
    
    // Sposób 2: try-catch (więcej kontroli)
    @Test
    public void shouldThrowExceptionWithCorrectMessage() {
        Calculator calculator = new Calculator();
        
        try {
            calculator.divide(10, 0);
            fail("Powinien rzucić IllegalArgumentException");
        } catch (IllegalArgumentException e) {
            assertEquals("Nie można dzielić przez zero", e.getMessage());
        }
    }
    
    // Test że metoda NIE rzuca wyjątku
    @Test
    public void shouldNotThrowExceptionForValidDivision() {
        Calculator calculator = new Calculator();
        
        // Jeśli nie rzuci wyjątku, test przejdzie
        double result = calculator.divide(10, 2);
        assertEquals(5.0, result, 0.001);
    }
}

Setup i Teardown – @Before i @After

Setup/Teardown to kod wykonywany przed/po każdym teście. Używany do przygotowania danych testowych i czyszczenia po testach.
import org.junit.*;
import static org.junit.Assert.*;

public class BankAccountTest {
    
    private BankAccount account;
    
    // Wykonywane PRZED każdym testem
    @Before
    public void setUp() {
        System.out.println("Przygotowuję dane testowe...");
        account = new BankAccount("Jan Kowalski", 1000.0);
    }
    
    // Wykonywane PO każdym teście
    @After
    public void tearDown() {
        System.out.println("Sprzątam po teście...");
        account = null;
    }
    
    // Wykonywane RAZ przed wszystkimi testami w klasie
    @BeforeClass
    public static void setUpClass() {
        System.out.println("Inicjalizacja całej klasy testowej");
        // np. połączenie z bazą danych, konfiguracja logowania
    }
    
    // Wykonywane RAZ po wszystkich testach w klasie
    @AfterClass
    public static void tearDownClass() {
        System.out.println("Sprzątanie po całej klasie testowej");
        // np. zamknięcie połączenia z bazą danych
    }
    
    @Test
    public void shouldDepositMoney() {
        // account jest już przygotowane w @Before
        account.deposit(500.0);
        
        assertEquals(1500.0, account.getBalance(), 0.01);
    }
    
    @Test
    public void shouldWithdrawMoney() {
        account.withdraw(300.0);
        
        assertEquals(700.0, account.getBalance(), 0.01);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotAllowNegativeDeposit() {
        account.deposit(-100.0);
    }
}

Praktyczny przykład – testowanie klasy BankAccount

Najpierw klasa do testowania:

// src/main/java/com/example/BankAccount.java
public class BankAccount {
    private String ownerName;
    private double balance;
    private boolean isActive;
    
    public BankAccount(String ownerName, double initialBalance) {
        if (ownerName == null || ownerName.trim().isEmpty()) {
            throw new IllegalArgumentException("Nazwa właściciela nie może być pusta");
        }
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Saldo początkowe nie może być ujemne");
        }
        
        this.ownerName = ownerName;
        this.balance = initialBalance;
        this.isActive = true;
    }
    
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Kwota musi być dodatnia");
        }
        if (!isActive) {
            throw new IllegalStateException("Konto jest nieaktywne");
        }
        
        balance += amount;
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Kwota musi być dodatnia");
        }
        if (!isActive) {
            throw new IllegalStateException("Konto jest nieaktywne");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Niewystarczające środki");
        }
        
        balance -= amount;
    }
    
    public void deactivate() {
        isActive = false;
    }
    
    public void activate() {
        isActive = true;
    }
    
    // Gettery
    public String getOwnerName() { return ownerName; }
    public double getBalance() { return balance; }
    public boolean isActive() { return isActive; }
}

Kompleksowe testy:

// src/test/java/com/example/BankAccountTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class BankAccountTest {
    
    private BankAccount account;
    private static final double DELTA = 0.001; // Tolerancja dla porównań double
    
    @Before
    public void setUp() {
        account = new BankAccount("Jan Kowalski", 1000.0);
    }
    
    // =================
    // TESTY KONSTRUKTORA
    // =================
    
    @Test
    public void shouldCreateAccountWithValidData() {
        BankAccount newAccount = new BankAccount("Anna Nowak", 500.0);
        
        assertEquals("Anna Nowak", newAccount.getOwnerName());
        assertEquals(500.0, newAccount.getBalance(), DELTA);
        assertTrue(newAccount.isActive());
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotCreateAccountWithNullName() {
        new BankAccount(null, 1000.0);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotCreateAccountWithEmptyName() {
        new BankAccount("   ", 1000.0);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotCreateAccountWithNegativeBalance() {
        new BankAccount("Jan Kowalski", -100.0);
    }
    
    @Test
    public void shouldCreateAccountWithZeroBalance() {
        BankAccount zeroAccount = new BankAccount("Piotr Wiśniewski", 0.0);
        assertEquals(0.0, zeroAccount.getBalance(), DELTA);
    }
    
    // =================
    // TESTY DEPOZYTÓW
    // =================
    
    @Test
    public void shouldDepositValidAmount() {
        account.deposit(500.0);
        
        assertEquals(1500.0, account.getBalance(), DELTA);
    }
    
    @Test
    public void shouldDepositMultipleTimes() {
        account.deposit(200.0);
        account.deposit(300.0);
        
        assertEquals(1500.0, account.getBalance(), DELTA);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotDepositZero() {
        account.deposit(0.0);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotDepositNegativeAmount() {
        account.deposit(-100.0);
    }
    
    @Test(expected = IllegalStateException.class)
    public void shouldNotDepositToInactiveAccount() {
        account.deactivate();
        account.deposit(100.0);
    }
    
    // =================
    // TESTY WYPŁAT
    // =================
    
    @Test
    public void shouldWithdrawValidAmount() {
        account.withdraw(300.0);
        
        assertEquals(700.0, account.getBalance(), DELTA);
    }
    
    @Test
    public void shouldWithdrawEntireBalance() {
        account.withdraw(1000.0);
        
        assertEquals(0.0, account.getBalance(), DELTA);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotWithdrawMoreThanBalance() {
        account.withdraw(1100.0);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotWithdrawZero() {
        account.withdraw(0.0);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldNotWithdrawNegativeAmount() {
        account.withdraw(-50.0);
    }
    
    @Test(expected = IllegalStateException.class)
    public void shouldNotWithdrawFromInactiveAccount() {
        account.deactivate();
        account.withdraw(100.0);
    }
    
    // =================
    // TESTY AKTYWACJI/DEAKTYWACJI
    // =================
    
    @Test
    public void shouldDeactivateAccount() {
        account.deactivate();
        
        assertFalse(account.isActive());
    }
    
    @Test
    public void shouldActivateAccount() {
        account.deactivate();
        account.activate();
        
        assertTrue(account.isActive());
    }
    
    @Test
    public void shouldAllowOperationsAfterReactivation() {
        account.deactivate();
        account.activate();
        
        // Powinno działać normalnie
        account.deposit(100.0);
        account.withdraw(50.0);
        
        assertEquals(1050.0, account.getBalance(), DELTA);
    }
    
    // =================
    // TESTY EDGE CASES
    // =================
    
    @Test
    public void shouldHandleVerySmallAmounts() {
        account.deposit(0.01);
        
        assertEquals(1000.01, account.getBalance(), DELTA);
    }
    
    @Test
    public void shouldHandleLargeAmounts() {
        account.deposit(999999.99);
        
        assertEquals(1000999.99, account.getBalance(), DELTA);
    }
}

Uruchom testowanie i interpretacja wyników

Uruchamianie testów

**W IntelliJ IDEA:**
– Kliknij prawym na klasę testową → Run 'CalculatorTest’
– Lub użyj skrótu `Ctrl+Shift+F10`
– Uruchom wszystkie testy: `Ctrl+Shift+F10` na folderze test

**W Eclipse:**
– Kliknij prawym na klasę → Run As → JUnit Test
– Lub `Alt+Shift+X, T`

**Z linii komend (Maven):**
„`bash
mvn test # Wszystkie testy
mvn test -Dtest=CalculatorTest # Konkretna klasa
„`

Interpretacja wyników

Zielony pasek = wszystkie testy przeszły ✅
Czerwony pasek = co najmniej jeden test nie przeszedł ❌
Żółty pasek = testy zostały zignorowane (@Ignore)
Running com.example.BankAccountTest
Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.123 sec

Results :

Tests run: 15, Failures: 0, Errors: 0, Skipped: 0

[INFO] BUILD SUCCESS

Najlepsze praktyki testowania

Pro tip: Jedna asercja na test. Każdy test powinien sprawdzać jedną konkretną rzecz – łatwiej debugować błędy.

Nazewnictwo testów

// DOBRE nazwy testów - opisują co i kiedy
@Test
public void shouldReturnTrueWhenNumberIsEven() { }

@Test 
public void shouldThrowExceptionWhenDividingByZero() { }

@Test
public void shouldCalculateCorrectTaxForHighIncome() { }

// ZŁTE nazwy testów
@Test
public void test1() { } // Co testuje?

@Test
public void testCalculator() { } // Która metoda? Jaki scenariusz?

@Test
public void divideTest() { } // Za ogólne

Wzorce nazewnictwa

**Popularne konwencje:**
– `should[ExpectedBehavior]When[StateUnderTest]`
– `given[Precondition]When[Action]Then[ExpectedResult]`
– `test[MethodName][Scenario]`

Organizacja testów

public class BankAccountTest {
    
    // ===============================
    // HAPPY PATH TESTS (pozytywne scenariusze)
    // ===============================
    
    @Test
    public void shouldDepositValidAmount() { }
    
    @Test  
    public void shouldWithdrawValidAmount() { }
    
    // ===============================
    // ERROR CASES (błędne scenariusze)
    // ===============================
    
    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionForNegativeDeposit() { }
    
    // ===============================
    // EDGE CASES (przypadki graniczne)
    // ===============================
    
    @Test
    public void shouldHandleZeroBalance() { }
    
    @Test
    public void shouldHandleMaximumAmount() { }
}

Częste błędy w testach

Błąd #1: Testowanie implementacji zamiast zachowania – testy nie powinny wiedzieć JAK kod działa, tylko CO robi.
// ŹLE - test zna implementację
@Test
public void shouldUseArrayListInternally() {
    UserService service = new UserService();
    // Test sprawdza typ wewnętrznej kolekcji - źle!
    assertTrue(service.getUsers() instanceof ArrayList);
}

// DOBRZE - test sprawdza zachowanie
@Test
public void shouldReturnAllAddedUsers() {
    UserService service = new UserService();
    service.addUser(new User("Jan"));
    service.addUser(new User("Anna"));
    
    assertEquals(2, service.getUsers().size());
}
Pułapka: Ignorowanie testów które „czasem nie przechodzą” – niestabilne testy to czerwona flaga! Napraw je lub usuń.
Błąd #2: Testy zależne od siebie – każdy test powinien być niezależny i móc być uruchomiony osobno.
// BŁĄD - testy zależne od kolejności
private static int counter = 0;

@Test
public void testFirst() {
    counter++;
    assertEquals(1, counter);
}

@Test  
public void testSecond() {
    // Zależy od testFirst() - źle!
    assertEquals(1, counter);
}

// POPRAWNIE - każdy test niezależny
@Before
public void setUp() {
    counter = 0; // Reset przed każdym testem
}

Test Coverage – pokrycie kodu testami

Code Coverage to metryka pokazująca jaki procent kodu jest wykonywany przez testy. Nie gwarantuje jakości testów, ale pomaga znajdować niepokryte obszary.

**W IntelliJ IDEA:**
– Run → Run with Coverage
– Zobaczysz kolorowanie kodu: zielone = pokryte, czerwone = niepokryte

**Typy pokrycia:**
– **Line Coverage** – jaki % linii jest wykonywany
– **Branch Coverage** – jaki % ścieżek w if/switch jest testowany
– **Method Coverage** – jaki % metod jest wywoływany

Uwaga: 100% pokrycia nie oznacza 100% jakości testów! Ważniejsze są dobre scenariusze testowe niż wysoki procent pokrycia.
Ile testów powinienem napisać dla jednej metody?

Przynajmniej jeden test dla happy path, po jednym dla każdego błędnego scenariusza, i testy dla edge cases (wartości graniczne jak 0, null, maksymalne wartości).

Czy powinienem testować gettery i settery?

Zazwyczaj nie – to proste metody bez logiki biznesowej. Testuj je tylko jeśli zawierają walidację lub złożoną logikę.

Co zrobić gdy test „czasami” nie przechodzi?

To flaky test – trzeba go naprawić! Często przyczyną są problemy z czasem, wielowątkowością, lub zewnętrznymi zależnościami. Użyj @Before/@After do ustabilizowania warunków testowych.

Jak testować metody private?

Nie testuj metod private bezpośrednio! Testuj public metody które je wywołują. Jeśli private metoda jest na tyle złożona że wymaga testów, może powinna być public w osobnej klasie.

Czy testy mogą mieć logikę (if, for, try-catch)?

Unikaj logiki w testach – mają być proste i przewidywalne. Jeśli potrzebujesz if/for, rozważ napisanie kilku prostszych testów zamiast jednego złożonego.

Jaki powinien być docelowy procent pokrycia testami?

Nie ma magicznej liczby! 70-80% to dobry cel, ale ważniejsza jest jakość testów. Lepiej mieć 50% dobrych testów niż 90% testów które nic nie sprawdzają.

Czy mogę używać System.out.println w testach?

Tylko do debugowania! Usuń przed commitem. Testy powinny być „ciche” – komunikują wyniki przez asercje, nie przez wypisywanie na konsolę.

Następne kroki:

  • Naucz się mockowania z biblioteką Mockito
  • Poznaj JUnit 5 – najnowszą wersję frameworka
  • Dowiedz się o testach integracyjnych i Test-Driven Development (TDD)

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz klasę „ShoppingCart” z metodami: addItem(String name, double price), removeItem(String name), getTotalPrice(), getItemCount(), clear(). Napisz kompletne testy JUnit 4 pokrywające wszystkie scenariusze – pozytywne, błędne i graniczne. Zadbaj o dobre nazwy testów i użyj @Before do przygotowania danych testowych!

Czy już piszesz testy do swojego kodu? Jakie są Twoje największe wyzwania z testowaniem? Podziel się swoimi doświadczeniami!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry