Məzmuna keç
Educora
İrəli22 dəq14 / 18

JUnit 5 ilə testləşdirmə

JUnit 5 ilə avtomatik unit testlər yaz: yoxlamalar, parametrli testlər və Mockito ilə asılılıqların əvəz edilməsi.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • @Test və assertEquals, assertThrows, assertAll ilə test sinfi yazmaq
  • Bir testi @ParameterizedTest ilə çoxlu verilənlər üçün işə salmaq
  • Asılılığı Mockito ilə əvəz etmək və çağırışları verify ilə yoxlamaq

Metodda kiçik bir dəyişiklik etdin və proqram yenə də işləyir. Bəs başqa on yerdə nəsə pozulmadı ki? Hər dəfə hər şeyi əl ilə yoxlamaq mümkün deyil. Peşəkar komandalar bunun üçün avtomatik testlər yazır: kodu çağıran və nəticəni gözlənilən qiymətlə müqayisə edən kiçik metodlar. Yüzlərlə test bir neçə saniyəyə işləyir və səhvi istifadəçidən əvvəl tutur.

Test nədir?

Unit test (vahid test) kiçik bir kod parçasını — adətən bir metodu — digərlərindən ayrılıqda yoxlayır. İdeyası sadədir: gözlənilən nəticəni bilirsən, metodu çağırırsan və ikisini müqayisə edirsən. Aşağıda heç bir kitabxana olmadan yazılmış mini test «freymvorku» var. Üçüncü yoxlamada gözlənti qəsdən səhv yazılıb ki, uğursuz testin necə göründüyünü görəsən.

Java
import java.util.List;

public class Main {
    static int passed = 0, failed = 0;

    static void check(String name, Object expected, Object actual) {
        if (expected.equals(actual)) {
            passed++;
            System.out.println("PASS " + name);
        } else {
            failed++;
            System.out.println("FAIL " + name + ": expected " + expected + " but was " + actual);
        }
    }

    public static void main(String[] args) {
        check("average of 85, 90, 95", 90.0, Grades.average(List.of(85, 90, 95)));
        check("95 is an A", "A", Grades.letter(95));
        check("79 is a B", "B", Grades.letter(79));
        System.out.println(passed + " passed, " + failed + " failed");
    }
}

class Grades {
    static double average(List<Integer> scores) {
        if (scores.isEmpty()) throw new IllegalArgumentException("No scores");
        return scores.stream().mapToInt(Integer::intValue).average().getAsDouble();
    }

    static String letter(int score) {
        if (score < 0 || score > 100) throw new IllegalArgumentException("Score out of range: " + score);
        return score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 50 ? "D" : "F";
    }
}
Gözlənilən nəticə
PASS average of 85, 90, 95
PASS 95 is an A
FAIL 79 is a B: expected B but was C
2 passed, 1 failed
Testin mahiyyəti: gözlənilən və faktiki nəticəni müqayisə etmək

JUnit eyni işi peşəkar səviyyədə görür: test metodlarını özü tapır, hər birini ayrıca işə salır, səhvləri toplayır və IDE-də, Maven və ya Gradle-də səliqəli hesabat verir. Əvvəlki dərsdəki org.junit.jupiter:junit-jupiter asılılığı (test scope-u ilə) kifayətdir. JUnit 5-in əsas hissəsi Jupiter adlanır, ona görə paketləri org.junit.jupiter ilə başlayır.

İlk JUnit 5 testləri

Java
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class GradesTest {

    @Test
    void averageOfThreeScores() {
        List<Integer> scores = List.of(85, 90, 95);   // Arrange
        double result = Grades.average(scores);        // Act
        assertEquals(90.0, result, 0.001);             // Assert
    }

    @Test
    @DisplayName("An empty list is rejected")
    void emptyListThrows() {
        var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
        assertEquals("No scores", ex.getMessage());
    }

    @Test
    void lettersAtBoundaries() {
        assertAll(
            () -> assertEquals("A", Grades.letter(90)),
            () -> assertEquals("B", Grades.letter(80)),
            () -> assertEquals("F", Grades.letter(49)));
    }
}
src/test/java/GradesTest.java — Grades sinfi yuxarıdakı kimidir

Hər test metodunun üstündə @Test annotasiyası var; metod public olmaya bilər və heç nə qaytarmır. Birinci test AAA qaydasına əməl edir: Arrange (verilənləri hazırla), Act (metodu çağır), Assert (nəticəni yoxla). assertEquals(expected, actual, delta) həqiqi ədədləri 0.001 dəqiqliklə müqayisə edir, çünki Java-da 0.1 + 0.2 tam 0.3 deyil, 0.30000000000000004-dür. assertThrows istisnanın atıldığını yoxlayır və onu qaytarır ki, mesajını da yoxlaya biləsən. assertAll isə bütün yoxlamaları icra edir və birinci səhvdə dayanmır.

AnnotasiyaNə edir
@Testmetodu test kimi işarələyir
@DisplayName("...")hesabatda oxunaqlı ad verir
@BeforeEach / @AfterEachhər testdən əvvəl / sonra işləyir
@BeforeAll / @AfterAllbütün sinif üçün bir dəfə işləyir (static metod)
@Disabledtesti müvəqqəti söndürür
@ParameterizedTesttesti hər verilənlər sətri üçün ayrıca işə salır

JUnit hər test üçün yeni obyekt yaradır, ona görə testlər bir-birinə təsir etmir və istənilən ardıcıllıqla işləyə bilər. Testləri IDE-də yaşıl ▶ düyməsi ilə, terminalda isə mvn test və ya ./gradlew test ilə işə salırsan. Təsəvvür et ki, kimsə letter metodunda score >= 80 şərtini səhvən score > 80 ilə əvəz edib:

Terminal
mvn test
Gözlənilən nəticə
[INFO] Running GradesTest
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[ERROR] Failures:
[ERROR]   GradesTest.lettersAtBoundaries:25 Multiple Failures (1 failure)
	org.opentest4j.AssertionFailedError: expected: <B> but was: <C>
[INFO] BUILD FAILURE
Nümunə nəticə (qısaldılıb): test səhvi dərhal tutdu — 80 bal artıq «B» deyil
JUnit 4 (köhnə)
import org.junit.Test;                          // JUnit 4
import static org.junit.Assert.assertEquals;

public class GradesTest {
    @Test(expected = IllegalArgumentException.class)
    public void emptyListThrows() {
        Grades.average(List.of());
    }
}
JUnit 5 (müasir)
import org.junit.jupiter.api.Test;              // JUnit 5
import static org.junit.jupiter.api.Assertions.*;

class GradesTest {
    @Test
    void emptyListThrows() {
        var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
        assertEquals("No scores", ex.getMessage());
    }
}
JUnit 5-də sinif və metodlar public olmaya bilər, istisnanın mesajını da yoxlamaq olur

Parametrli testlər

Eyni yoxlamanı müxtəlif verilənlərlə təkrarlamaq üçün testi kopyalamağa ehtiyac yoxdur. @ParameterizedTest testi hər verilənlər dəsti üçün ayrıca işə salır: @CsvSource vergüllə ayrılmış hər sətri metodun parametrlərinə ötürür, @ValueSource isə sadə qiymətlər siyahısı üçündür. Hesabatda hər hal ayrıca görünür, məsələn, 85 -> B. Bu annotasiyalar junit-jupiter-params modulundadır və artıq junit-jupiter asılılığına daxildir.

Kopyala-yapışdır testlər
@Test void letterFor95() { assertEquals("A", Grades.letter(95)); }
@Test void letterFor85() { assertEquals("B", Grades.letter(85)); }
@Test void letterFor72() { assertEquals("C", Grades.letter(72)); }
@Test void letterFor49() { assertEquals("F", Grades.letter(49)); }
// one more case = one more copy-pasted method...
Parametrli testlər: yeni hal = yeni sətir
@ParameterizedTest(name = "{0} -> {1}")
@CsvSource({ "95, A", "85, B", "72, C", "49, F" })
void letterForScore(int score, String expected) {
    assertEquals(expected, Grades.letter(score));
}

@ParameterizedTest
@ValueSource(ints = { -1, 101, 150 })
void outOfRangeScoresAreRejected(int score) {
    assertThrows(IllegalArgumentException.class, () -> Grades.letter(score));
}

Mockito: asılılıqları əvəz etmək

Real sinif çox vaxt başqa obyektlərdən asılıdır: verilənlər bazasından, e-poçt serverindən, xarici API-dən. Unit testdə onlara həqiqətən müraciət etmək yavaş və etibarsızdır. Həll yolu asılılığı konstruktor vasitəsilə vermək və testdə onu mock (saxta obyekt) ilə əvəz etməkdir. Aşağıdakı ReportService balları ScoreRepository interfeysindən alır və onun bazanı oxuduğunu, yoxsa saxta olduğunu bilmir.

Java
interface ScoreRepository {
    List<Integer> findScores(String student);   // e.g. reads a database
}

class ReportService {
    private final ScoreRepository repository;

    ReportService(ScoreRepository repository) {
        this.repository = repository;
    }

    String report(String student) {
        return student + ": " + Grades.average(repository.findScores(student));
    }
}
Asılılıq konstruktordan gəlir — bu, sinfi test etməyi asanlaşdırır
Java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

import java.util.List;
import org.junit.jupiter.api.Test;

class ReportServiceTest {

    @Test
    void reportUsesScoresFromRepository() {
        ScoreRepository repo = mock(ScoreRepository.class);
        when(repo.findScores("Aysel")).thenReturn(List.of(90, 80));

        ReportService service = new ReportService(repo);
        String report = service.report("Aysel");

        assertEquals("Aysel: 85.0", report);
        verify(repo).findScores("Aysel");
        verifyNoMoreInteractions(repo);
    }
}
Mockito üçün org.mockito:mockito-junit-jupiter asılılığı (test scope) lazımdır

mock(...) interfeysin boş saxta nüsxəsini yaradır. when(...).thenReturn(...) ona «Aysel soruşulsa, 90 və 80 qaytar» deyə öyrədir — buna stub deyilir. verify servisin repozitorini məhz bir dəfə və düzgün arqumentlə çağırdığını yoxlayır, verifyNoMoreInteractions isə artıq çağırış olmadığını təsdiqləyir. Böyük test siniflərində eyni işi annotasiyalarla görürlər: sinfin üstünə @ExtendWith(MockitoExtension.class), sahənin üstünə @Mock yazılır.

Əsas fikirlər

  • Unit test kodun kiçik hissəsini ayrılıqda yoxlayır: gözlənilən nəticəni faktiki nəticə ilə müqayisə edir.
  • JUnit 5: @Test, assertEquals(expected, actual), assertThrows, assertAll; testlər src/test/java-da yerləşir.
  • AAA qaydası: Arrange → Act → Assert; hər test yeni obyektdə işləyir və başqalarından asılı deyil.
  • @ParameterizedTest + @CsvSource / @ValueSource bir testi çoxlu verilənlərlə işə salır.
  • Mockito asılılıqları əvəz edir: mock, when(...).thenReturn(...), verify; asılılığı konstruktorla vermək testi asanlaşdırır.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
assertEquals(0.3, 0.1 + 0.2) testi niyə uğursuz olur?