@TestvəassertEquals,assertThrows,assertAllilə test sinfi yazmaq- Bir testi
@ParameterizedTestilə çoxlu verilənlər üçün işə salmaq - Asılılığı Mockito ilə əvəz etmək və çağırışları
verifyilə 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.
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";
}
}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
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
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ı kimidirHə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.
| Annotasiya | Nə edir |
|---|---|
@Test | metodu test kimi işarələyir |
@DisplayName("...") | hesabatda oxunaqlı ad verir |
@BeforeEach / @AfterEach | hər testdən əvvəl / sonra işləyir |
@BeforeAll / @AfterAll | bütün sinif üçün bir dəfə işləyir (static metod) |
@Disabled | testi müvəqqəti söndürür |
@ParameterizedTest | testi 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:
mvn test[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
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());
}
}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());
}
}public olmaya bilər, istisnanın mesajını da yoxlamaq olurParametrli 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.
@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...@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.
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));
}
}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);
}
}org.mockito:mockito-junit-jupiter asılılığı (test scope) lazımdırmock(...) 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ərsrc/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/@ValueSourcebir 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.
assertEquals(0.3, 0.1 + 0.2) testi niyə uğursuz olur?