## თავი 7: ვებ-აპლიკაციების ტესტირება --- ## 7.1 რატომ არის ტესტირება მნიშვნელოვანი წინა თავებში ჩვენ ავაგეთ კონტროლერები, შაბლონები (templates), ენთითები (entities), რეპოზიტორიები, REST API-ები და კონფიგურაციის პროფილები. ჩვენ ვამოწმებდით მათ მუშაობას აპლიკაციის გაშვებით და ბრაუზერში "დაკლიკვით", ან curl-ის მეშვეობით მოთხოვნების გაგზავნით. ეს მიდგომა — **მექანიკური ტესტირება** — მუშაობს მაშინ, როცა აპლიკაცია პატარაა. თუმცა, ის წყვეტს ეფექტურობას იმ მომენტში, როცა აპლიკაცია რამდენიმე ფუნქციაზე მეტად იზრდება. მექანიკური ტესტირება არის ნელი, არასანდო და არამასშტაბირებადი. შეუძლებელია ყოველ ჯერზე, როცა კოდის ერთ ხაზს შეცვლით, ხელით გადაამოწმოთ ყველა ფუნქცია. **ავტომატიზებულ ტესტებს** კი ეს შეუძლიათ. ისინი წამებში სრულდებიან, მომენტალურად აფიქსირებენ რეგრესიებს (ახალი ცვლილებით გამოწვეულ შეცდომებს) და გაძლევენ თავდაჯერებულობას, რომ ჩაატაროთ რეფაქტორინგი რაიმეს გაფუჭების შიშის გარეშე. ეს თავი გაგაცნობთ ტესტირების იმ ინსტრუმენტებს, რომლებსაც Spring Boot გვთავაზობს. ჩვენ შევისწავლით ტესტირების პირამიდას და ტესტირების სხვადასხვა დონეებს; როგორ დავწეროთ იუნიტ (unit) ტესტები JUnit 5-ისა და Mockito-ს გამოყენებით; როგორ დავტესტოთ Spring-ის კომპონენტები იზოლირებულად "slice" ანოტაციების მეშვეობით; როგორ შევამოწმოთ REST API-ები და ფორმების გაგზავნა MockMvc-ით; და როგორ დავაკონფიგურიროთ ტესტ-სპეციფიკური პროფილები და მონაცემთა ბაზები. თავის ბოლოს თქვენ გექნებათ პრაქტიკული ტესტირების სტრატეგია თქვენი აპლიკაციისთვის. Spring Boot მოიცავს ყველა საჭირო დამოკიდებულებას (dependencies) — JUnit 5, Mockito, AssertJ, MockMvc და სხვა — `spring-boot-starter-test` ბიბლიოთეკის მეშვეობით. --- ## 7.2 ტესტირების პირამიდა ყველა ტესტი ერთსა და იმავე მიზანს არ ემსახურება. ტესტირების პირამიდა აღწერს სამ დონეს, რომლებიც დალაგებულია სიჩქარის, მასშტაბისა და რაოდენობის მიხედვით: **იუნიტ ტესტები (Unit tests)** პირამიდის საფუძველშია. ისინი ტესტავენ ერთ კლასს ან მეთოდს აპლიკაციის სხვა ნაწილებისგან სრულ იზოლაციაში. დამოკიდებულებები (dependencies) ჩანაცვლებულია "მოკებით" (mocks/იმიტაციებით). იუნიტ ტესტები არის სწრაფი (თითოეული სრულდება მილიწამებში), მრავალრიცხოვანი და მათი დაწერა თუ შენახვა იაფია. **ინტეგრაციული ტესტები (Integration tests)** შუა დონეზეა. ისინი ამოწმებენ, როგორ მუშაობს რამდენიმე კომპონენტი ერთად — მაგალითად, სერვისის კავშირი რეალურ რეპოზიტორიასთან ან კონტროლერის კავშირი Spring-ის კონტექსტთან. ისინი იუნიტ ტესტებზე ნელია, რადგან შესაძლოა დასჭირდეთ Spring კონტექსტის გაშვება ან ბაზასთან ურთიერთქმედება, მაგრამ ისინი პოულობენ იმ პრობლემებს, რომლებსაც იუნიტ ტესტები ვერ ამჩნევენ — კავშირების (wiring) შეცდომებს, კონფიგურაციის ხარვეზებს ან ბაზის მოთხოვნების (query) ბაგებს. **ბოლო-ბოლო ტესტები (End-to-end tests / E2E)** პირამიდის მწვერვალზეა. ისინი ტესტავენ მთლიან აპლიკაციას მომხმარებლის პერსპექტივიდან — ბრაუზერის გაშვება, ფორმების შევსება, ღილაკებზე დაჭერა. ისინი ყველაზე ნელი და რთულად შესანახია. ისინი გვაძლევენ მაქსიმალურ თავდაჯერებულობას, თუმცა მათი გამოყენება ზომიერად არის საჭირო. პირამიდის ფორმა განზრახულია: დაწერეთ ბევრი იუნიტ ტესტი, საშუალო რაოდენობის ინტეგრაციული ტესტი და მცირე რაოდენობის E2E ტესტი. ეს თავი ფოკუსირებულია პირველ ორ დონეზე. --- ## 7.3 იუნიტ ტესტირება JUnit 5-ით JUnit 5 არის სტანდარტული ტესტირების ფრეიმვორკი Java-სთვის. Spring Boot მას ავტომატურად მოაყოლებს `spring-boot-starter-test`-ს, ამიტომ დამატებითი ბიბლიოთეკები საჭირო არ არის. ### ტესტის ბაზისური სტრუქტურა ტესტ-კლასი არის ჩვეულებრივი Java კლასი. თითოეული ტესტ-მეთოდი ინიშნება `@Test` ანოტაციით: ```java import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @Test void shouldAddTwoNumbers() { Calculator calculator = new Calculator(); int result = calculator.add(2, 3); assertEquals(5, result); } @Test void shouldThrowExceptionWhenDividingByZero() { Calculator calculator = new Calculator(); assertThrows(ArithmeticException.class, () -> calculator.divide(10, 0)); } } ``` თითოეული ტესტ-მეთოდი მიჰყვება სამნაწილიან სტრუქტურას, რომელსაც ხშირად **Arrange-Act-Assert** (მომზადება-მოქმედება-შემოწმება) ეწოდება: მოამზადეთ საჭირო ობიექტები, შეასრულეთ ოპერაცია, რომელსაც ტესტავთ და დაადასტურეთ შედეგი. ტესტ-მეთოდის სახელი უნდა აღწერდეს ქცევას — `shouldAddTwoNumbers` ბევრად უფრო ინფორმაციულია, ვიდრე უბრალოდ `testAdd`. ### გავრცელებული ასერციები (Assertions) * `assertEquals(expected, actual)` — მნიშვნელობები ტოლია. * `assertNotEquals(unexpected, actual)` — მნიშვნელობები განსხვავდება. * `assertTrue(condition)` — პირობა ჭეშმარიტია. * `assertFalse(condition)` — პირობა მცდარია. * `assertNull(value)` — მნიშვნელობა არის null. * `assertNotNull(value)` — მნიშვნელობა არ არის null. * `assertThrows(ExceptionType.class, () -> {})` — ხდება ერორის (exception) გამოტყორცნა. * `assertAll(...)` — რამდენიმე შემოწმების დაჯგუფება. `assertAll` განსაკუთრებით სასარგებლოა — ის უშვებს ყველა შემოწმებას მაშინაც კი, თუ ერთ-ერთი ჩაიჭრა, რაც საშუალებას გაძლევთ დაინახოთ ყველა ხარვეზი ერთდროულად. ### სასიცოცხლო ციკლის ანოტაციები JUnit 5 გვთავაზობს ანოტაციებს setup და teardown კოდისთვის, რომელიც ტესტის სხვადასხვა ეტაპზე ეშვება: | ანოტაცია | როდის ეშვება | |:--- |:--- | | `@BeforeEach` | ყოველი ტესტ-მეთოდის წინ. გამოიყენება ახალი ობიექტების შესაქმნელად. | | `@AfterEach` | ყოველი ტესტ-მეთოდის შემდეგ. გამოიყენება გასასუფთავებლად. | | `@BeforeAll` | ერთხელ, ყველა ტესტის დაწყებამდე (უნდა იყოს `static`). | | `@AfterAll` | ერთხელ, ყველა ტესტის დასრულების შემდეგ (უნდა იყოს `static`). | | `@DisplayName` | ტესტისთვის ადამიანისთვის გასაგები სახელის მინიჭება. | | `@Disabled` | ტესტის გამოტოვება. | --- ## 7.4 დამოკიდებულებების იმიტაცია (Mocking) Mockito-სთან ერთად იუნიტ ტესტი ამოწმებს ერთ კლასს იზოლაციაში. თუმცა, კლასების უმეტესობა დამოკიდებულია სხვა კლასებზე — მაგალითად, `ProductService` დამოკიდებულია `ProductRepository`-ზე. თუ რეალურ დამოკიდებულებებს გამოიყენებთ, ეს აღარ იქნება იზოლირებული ტესტი — რეპოზიტორიაში არსებულმა ბაგმა შეიძლება სერვისის ტესტის ჩაჭრა გამოიწვიოს. **Mockito** ამ პრობლემას ჭრის ყალბი ობიექტების — **მოკების (mocks)** — შექმნით. თქვენ ეუბნებით მოკს, რა დააბრუნოს კონკრეტული მეთოდის გამოძახებისას და ამოწმებთ, სწორად დაუკავშირდა თუ არა დასატესტი კლასი ამ მოკს. ### მოკების შექმნა ანოტაციებით ```java @ExtendWith(MockitoExtension.class) class ProductServiceTest { @Mock private ProductRepository productRepository; @InjectMocks private ProductService productService; @Test void shouldReturnProductWhenFound() { // Arrange (მომზადება) Product mockProduct = new Product(1L, "Laptop", 999.99); when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct)); // Act (მოქმედება) Product result = productService.findById(1L); // Assert (შემოწმება) assertNotNull(result); assertEquals("Laptop", result.getName()); // Verify (ვერიფიკაცია) verify(productRepository, times(1)).findById(1L); } } ``` * `@ExtendWith(MockitoExtension.class)` ააქტიურებს Mockito-ს ინტეგრაციას. * `@Mock` ქმნის `ProductRepository`-ს იმიტაციას. * `@InjectMocks` ქმნის `ProductService`-ს რეალურ ინსტანსს და მასში "უშვებს" (inject) შექმნილ მოკებს. * `when(...).thenReturn(...)` აპროგრამებს მოკს: "როცა გამოიძახებენ ამას, დააბრუნე ეს". --- ## 7.5 ტესტირებადი კოდის წერა ტესტირება უფრო ადვილია, როცა კოდი ამისთვისაა შექმნილი: 1. **გამოიყენეთ კონსტრუქტორის ინექცია (Constructor Injection):** ეს საშუალებას გაძლევთ მარტივად ჩაანაცვლოთ რეალური ობიექტები მოკებით ტესტებში. 2. **ენდეთ ინტერფეისებს და არა კონკრეტულ კლასებს.** 3. **შეინარჩუნეთ მეთოდები პატარა და ფოკუსირებული.** 4. **მოერიდეთ სტატიკურ მეთოდებს ბიზნეს ლოგიკაში.** --- ## 7.6 ინტეგრაციული ტესტირება @SpringBootTest-ით იუნიტ ტესტებისგან განსხვავებით, ინტეგრაციული ტესტები ამოწმებენ, როგორ მუშაობს ნაწილები ერთად. `@SpringBootTest` ტვირთავს Spring-ის სრულ კონტექსტს, ქმნის ყველა Bean-ს და აკავშირებს მათ, ზუსტად ისე, როგორც რეალურ აპლიკაციაში. ეს ტესტი ამოწმებს მთელ ჯაჭვს: სერვისი იძახებს რეპოზიტორიას, რეპოზიტორია უკავშირდება ბაზას (ტესტებში ხშირად H2-ს) და მონაცემები ინახება. მთავარი მინუსი სიჩქარეა — კონტექსტის ჩატვირთვას წამები სჭირდება. --- ## 7.7 Slice ტესტები (ფენობრივი ტესტირება) Spring Boot გვთავაზობს სპეციალურ ანოტაციებს, რომლებიც ტვირთავენ აპლიკაციის კონტექსტის მხოლოდ ნაწილს. ისინი უფრო სწრაფებია, რადგან ტოვებენ ყველაფერს, რაც კონკრეტული ფენისთვის საჭირო არ არის. * **@WebMvcTest:** ტვირთავს მხოლოდ ვებ-ფენას (კონტროლერებს). სერვისები უნდა მოვნიშნოთ `@MockitoBean`-ით (Spring Boot 4-ში `@MockBean`-ს ნაცვლად). * **@DataJpaTest:** ტვირთავს მხოლოდ JPA კომპონენტებს (ენთითებს, რეპოზიტორიებს) და იყენებს ჩაშენებულ (embedded) მონაცემთა ბაზას. --- ## 7.8 REST API-ების ტესტირება MockMvc-ით `MockMvc` ახდენს HTTP მოთხოვნების სიმულაციას რეალური სერვერის გაშვების გარეშე. მისი მეშვეობით შეგიძლიათ შეამოწმოთ სტატუსის კოდები, Header-ები და JSON პასუხები. მაგალითად, JSON-ის შესამოწმებლად გამოიყენება `jsonPath`: ```java mockMvc.perform(get("/api/products")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].name").value("Laptop")); ``` --- ## 7.9 ტესტ პროფილები და ჩაშენებული ბაზები თქვენ არ გინდათ, რომ ტესტებმა თქვენი დეველოპმენტის ბაზა დააზიანონ. ამისთვის იქმნება ტესტ-სპეციფიკური ფაილი `src/test/resources/application-test.properties`, სადაც მიეთითება H2 (in-memory) ბაზა. ტესტ კლასს კი ემატება `@ActiveProfiles("test")`. --- ## შეჯამება ამ თავში ჩვენ ვისწავლეთ Spring Boot აპლიკაციების მრავალდონიანი ტესტირება. **ტესტირების პირამიდა** გვკარნახობს სტრატეგიას: ბევრი სწრაფი იუნიტ ტესტი, საშუალო რაოდენობის ინტეგრაციული ტესტი და ცოტა E2E ტესტი. **JUnit 5** და **Mockito** არის ჩვენი მთავარი იარაღები კოდის ხარისხის უზრუნველსაყოფად, ხოლო **Slice ტესტები** გვეხმარება სწრაფ და ფოკუსირებულ შემოწმებაში.