using JUnit5 and Mockito (coding and test)

یه مجموعه آموزش داریم تحت عنوان using JUnit5 and Mockito که اومدیم توی چنتا سرفصل تقسیمش کردیم .

این آموزش به دو بخش تقسیم شده مفاهیم کلی تست و مثال ها و انوتیشن ها mockito و junit5.

امیدوارم مبحث مفاهیم کلی تست رو خونده باشید ، توی این آموزش میایم انوتیشن ها و متدهایی که قرار توی مثال های این آموزش استفاده کنیم رو توضیح میدیم و بعد هم مثال هامون رو میزنیم .

توی این آموزش به موارد زیر میپردازیم :
1- متدها و انوتیشن ها.
2- تفاوت و موارد استفاده هر انوتیشن و متدهایی که گفتیم.
3- مثال هامون و توضیحاتش:
- یه نمونه تست ساده
- یه نمونه RestApi (میایم از mock برای دیتاهامون و کلاس هامون استفاده میکنیم)
- یه نمونه mock هزارتایی از دیتابیس



1- متد ها و انوتیشن ها

قبل از اینکه بریم مثال هامون رو بزنیم اول انوتیشن ها و متدهایی که قراره توی این آموزش استفاده کنیم رو بیاریم و بگیم چی هستن .

چون خیلی زیاد و طولانی میشه نمیشه همه متدها و انوتیشن هارو بررسی کرد بنابراین میایم چیزایی که معمولا استفاده میشه توی تست ها یا حداقل چیزایی که خودم روزمرره استفاده میکنم رو میارم .

  • برای spring:

@WebMvcTest()

  • برای JUnit:

@Test
@DisplayName
@BeforEach
@AfterEach
@BeforAll
@AfterAll
assertionMethods(assertThat,assertEqual,...)

  • برای Mockito:

@Mock
@InjectMocks
@Captor
@Spy
given()
verify()
when()
doReturn()


2- تفاوت و موارد استفاده هر متد و انوتیشن

@WebMvcTest()

یک annotation در فریم‌ورک Spring Boot است که برای تست کنترلرهای MVC (Model-View-Controller) استفاده می‌شود. این annotation به طور خاص برای تست لایه‌ی وب در برنامه‌های Spring طراحی شده است و امکان تست کنترلرها را به صورت ایزوله فراهم می‌کند.

فقط اجزای مربوط به لایه وب (مانند کنترلرها، کنترلرهای exception، فیلترهای MVC، و کانفیگ‌های مرتبط با وب) را بارگذاری کند. این کار باعث می‌شود که تست‌های شما سبک‌تر و سریع‌تر اجرا شوند، زیرا نیازی به بارگذاری کل برنامه Spring ندارید.

وقتی از @WebMvcTest استفاده می‌کنید، Spring Boot فقط اجزای ضروری برای تست کنترلرها را بارگذاری می‌کند. این شامل موارد زیر است:

  • کنترلرها (Controller)
  • کنترلرهای استثنا (ControllerAdvice)
  • فیلترهای وب
  • کانفیگ‌های مرتبط با وب (مانند WebMvcConfigurer)
  • MockMvc برای ارسال درخواست‌های HTTP و بررسی پاسخ‌ها

به طور پیش‌فرض، @WebMvcTest اجزای دیگری مانند سرویس‌ها، ریپازیتوری‌ها، و سایر لایه‌های برنامه را بارگذاری نمی‌کند. این کار ایزوله‌سازی تست‌ها را تضمین می‌کند و باعث می‌شود که تست‌های شما فقط بر رفتار کنترلر متمرکز شوند.

حالا چرا باید ازش استفاده کنیم ، البته بایدی توش نیست میتونید استفاده نکنید راهکار برای تست زیاد هست :

  • تست‌های شما فقط بر کنترلرها متمرکز می‌شوند و وابستگی به سایر لایه‌های برنامه ندارند. این کار باعث می‌شود که تست‌های شما دقیق‌تر و قابل نگهداری‌تر باشند.
  • با بارگذاری نکردن کل برنامه، تست‌ها سریع‌تر اجرا می‌شوند که باعث افزایش بهره‌وری در فرآیند توسعه می‌شود.
  • استفاده از MockMvc نوشتن تست‌ها را بسیار ساده‌تر می‌کند. شما می‌توانید به راحتی درخواست‌ها را شبیه‌سازی کنید و پاسخ‌ها را بررسی کنید.
  • این annotation به شما کمک می‌کند تا تست‌های خود را بر روی لایه وب متمرکز کنید و از صحت کارکرد کنترلرها اطمینان حاصل کنید.
@WebMvcTest(PostsController.class) // مشخص کردن کنترلر مورد نظر برای تست 
public class PostsControllerTest { 
        @Autowired 
         private MockMvc mockMvc; 
        @Test 
         public void testGetEndpoint() throws Exception { 
               mockMvc.perform(MockMvcRequestBuilders.get(&quot/api/posts&quot)) .and
         }
}


@Test

بالا سر هر متد تست باید این انوتیشن رو بزارید و گرنه به عنوان تست اجرا نمیشه.

@DisplayName

این انوتیشن به شما این امکان را می‌دهد که یک نام قابل فهم و توصیفی برای تست‌ها تعیین کنید تا گزارش‌های تست خواناتر و مفهومی‌تر باشند.

@BeforEach

این انوتیشن برای اجرای کد قبل از هر تست استفاده می‌شود. کدی که در این متد قرار می‌گیرد، پیش از هر تست اجرا می‌شود.

@AfterEach

این انوتیشن برای اجرای کد بعد از هر تست استفاده می‌شود. معمولاً برای تمیزکاری‌ها یا بازنشانی داده‌ها کاربرد دارد.

@BeforAll

این انوتیشن برای اجرای کد قبل از اجرای تمامی تست‌ها استفاده می‌شود و معمولاً برای انجام عملیات‌های سنگین (مانند راه‌اندازی پایگاه داده یا ارتباطات شبکه) به کار می‌رود.

@AfterAll

مشابه @BeforAll ، این انوتیشن برای کدهایی استفاده می‌شود که باید پس از اجرای تمام تست‌ها اجرا شوند. معمولاً برای آزادسازی منابع یا تمیزکاری‌های عمومی استفاده می‌شود.


@Mock

این انوتیشن برای Mock کردن کلاس‌ها و وابستگی‌ها استفاده می‌شود. این انوتیشن به Mockito می‌گوید که یک شی از کلاس هدف ایجاد کند، که رفتار آن قابل شبیه‌سازی باشد.

@InjectMocks

این انوتیشن به Mockito می‌گوید که وابستگی‌های Mock شده را به صورت خودکار به کلاس هدف تزریق کند.

@Captor

این انوتیشن برای تعریف یک کپی از آرگومان‌های ورودی به متدها یا مقادیر خروجی استفاده می‌شود. معمولاً برای بررسی دقیق‌تر پارامترهای ارسال شده به متدهای Mock شده کاربرد دارد.

@Spy

این انوتیشن برای ایجاد یک Spy (نمونه واقعی) از یک شی استفاده می‌شود. برخلاف Mock، Spy‌ها رفتار واقعی شی را حفظ می‌کنند اما می‌توانند رفتار خاصی را شبیه‌سازی کنند یا تغییر دهند.

given()

این متد برای شبیه‌سازی رفتار یک شی Mock شده استفاده می‌شود. شما می‌توانید با استفاده از given(...) مشخص کنید که وقتی یک متد خاص فراخوانی می‌شود، چه پاسخی باید داده شود.

متد given() بخشی از کتابخانه AssertJ است که شبیه when() عمل می‌کند، اما به خاطر شکل نوشته شدنش خیلی به روان بودن تست و قابل فهم تر شدن تست کمک میکنه .

given(myService.performAction()).willReturn(&quotExpected Value&quot);

verify()

این متد برای تایید اینکه یک متد خاص در Mock فراخوانی شده است یا نه، استفاده می‌شود.

when()

مشابه given(...) ، این متد برای تعریف رفتار یک Mock به کار می‌رود، اما در نسخه‌های قدیمی‌تر Mockito معمولاً از when(...).thenReturn(...) استفاده می‌شد. در نسخه‌های جدیدتر ماکیتو given بیشتر به کار می‌رود.

متد when یک ابزار برای شبیه‌سازی رفتار متدهای اشیاء Mock شماست.

زمانی استفاده می‌شود که می‌خواهید بگویید یک متد خاص باید چه مقدار یا چه چیزی را برگرداند.

MyService myService = mock(MyService.class); when(myService.performAction()).thenReturn(&quotExpected Value&quot);

حالا توی این مثال thenReturn چیکار میکنه :

متد thenReturn() به شما این امکان را می‌دهد که مقداری را که باید در پاسخ به یک فراخوانی متد مشخص شده برگشت داده شود، تعریف کنید.

doReturn()

متد doReturn() مشابه when().thenReturn() است، اما استفاده از آن امکان شبیه‌سازی متدهایی که عوارض جانبی دارند یا متدهایی که void هستند، را نیز فراهم می‌آورد.

زمانی استفاده می‌شود که می‌خواهید از متد spy با رفتار واقعی استفاده کنید یا می‌خواهید یک متد void را شبیه‌سازی کنید.

doReturn(&quotExpected Value&quot).when(myService).performAction();

تفاوت های بعضی از متدها و انوتیشن ها :

بعضی هارو که به ذهنم در این لحظه رسید رو میگم .

1- فرق بین given و when چیه ؟

در Mockito، بین given و when تفاوتی در استفاده وجود دارد، اما هر دو به طور کلی برای تعریف رفتار اشیاء Mock استفاده می‌شوند

when()

به این ساختار دقت کنید :

when(mock.someMethod()).thenReturn(someValue);
  • معمولا قدیمی‌تر است و بیشتر در نسخه‌های اولیه Mockito استفاده می‌شد.
  • شما از when برای تعیین آنچه که باید در هنگام فراخوانی یک متد خاص بر روی شی Mock اتفاق بیفتد استفاده می‌کنید.
  • پس از آن، با استفاده از thenReturn یا متد های دیگر موجود ، می‌توانید تعیین کنید که چه مقدار باید برگرداند.

given()

به این ساختار هم یه نیگا بندازید:

given(mock.someMethod()).willReturn(someValue);
  • ساختار جدیدتر و ترجیحی در Mockito است، به ویژه در هنگام استفاده از Mockito با JUnit 5.
  • این روش خیلی قابل فهم تر و خواناتر ، و این امکان رو میده خیلی راحت تر و ملموس تر ، رفتار Mock ها را تعریف کنیم.
  • از willReturn برای تعیین مقداری که باید هنگام فراخوانی متد برگردانده شود استفاده می‌شود.

یه سری تفاوت خیلی مهم دارن :

  1. خوانایی:
    given خیلی خواناتر و برای همه دوولوپر های یک پروژه میتونه راحت تر و قابل فهم تر باشه.
    when خیلی به given نزدیکه شکل ساختاریش اما چون توی ساختارش بین متد ها یه حالت chain وجود داره یکم پیچیدگی ایجاد میکنه.
  2. سازگاری با JUnit 5:
    given به طور خاص برای سازگاری بهتر با روش‌های مدرن نوشتن تست طراحی شده است و معمولاً در پروژه‌هایی که از JUnit 5 استفاده می‌کنند، ترجیح داده می‌شود.
  3. عملکرد:
    از نظر عملکرد، هیچ تفاوتی در کارایی وجود ندارد. هر دو روش به یک روش مشابه کار می‌کنند و تنها تفاوت در نحوه نوشتن و خوانایی کد است.

در نهایت، انتخاب بین given و when بستگی به سلیقه شخصی شما و تیم توسعه دارد. اگر می‌خواهید به کدی با خوانایی بیشتر نزدیک شوید، ممکن است given را ترجیح دهید. با این حال، برای اکثر تست‌ها هر دو روش به خوبی کار میکنه .

2- فرق بین spy و mock چیه ؟

Mock

  • تعریف:
    Mock به شما این امکان را می‌دهد که یک شی از یک کلاس را شبیه‌سازی کنید. زمانی که از Mock استفاده می‌کنید، رفتار کلاس اصلی شبیه‌سازی شده و شما می‌توانید کنترل کاملی بر روی آن داشته باشید.
    Mock تمام متدهای کلاس را به‌طور پیش‌فرض بدون پیاده‌سازی واقعی آن‌ها در نظر می‌گیرد و شما می‌توانید با استفاده از روش‌هایی مانند when یا given رفتار موردنظر خود را تعیین کنید.
  • شما با استفاده از mock چیزی شبیه‌سازی می‌کنید، یعنی وابستگی های یک کلاس یا یک شی به طور کامل پیاده سازی نشده و همه چیز فیک هست . این به معنای این است که هیچ رفتاری واقعی در کلاس وجود نداره ، مگر اینکه صراحتاً آن را تعریف کنید.
  • کاربرد:
    Mock برای تست کردن کد بدون نیاز به اجرای واقعی متدهای موجود در کلاس اصلی استفاده می‌شود. در mock، هیچ پیاده‌سازی از متدها وجود ندارد مگر اینکه آنها را شبیه‌سازی کنید.
    این به شما اجازه می‌دهد تا وابستگی‌ها را شبیه‌سازی کنید و وابستگی‌هایی که اثر جانبی دارند (مانند دیتابیس و یا شبکه) را نادیده بگیرید.
@Mock
private MyService myService;

when(myService.performAction()).thenReturn(expectedValue);

Spy

  • تعریف:
    Spy به شما این امکان را می‌دهد که یک شی واقعی از یک کلاس ایجاد کنید و بر روی آن کنترل داشته باشید. در حقیقت، Spy یک نمونه واقعی از کلاس است که می‌توانید برخی از متدهای آن را شبیه‌سازی کنید، در حالی که متدهای دیگر آن همچنان رفتار واقعی خود را حفظ می‌کنند. با استفاده از spy شما از یک شیء واقعی استفاده می‌کنید، به این معنی که بسیاری از رفتارهای واقعی آن حفظ می‌شود. شما می‌توانید متدها را شبیه‌سازی کنید یا به رفتار واقعی شیء رو داشته باشید.
  • کاربرد:
    Spy زمانی کاربرد دارد که می‌خواهید بخشی از رفتار کلاس اصلی را تغییر دهید یا شبیه‌سازی کنید، اما در عین حال می‌خواهید به عملکرد طبیعی سایر متدها دسترسی داشته باشید. در spy، می‌توانید متدها را به صورت واقعی اجرا کرده و فقط برخی از آنها را شبیه‌سازی کنید. spy ممکن است کمی کندتر باشد چون میاد متد های واقعی رو هم اجرا میکنه .
    بسیار مفید است زمانی که برخی از متدها خاص هستند و نیاز به آزمایش واقعی آن‌ها دارید.
@Spy
private MyService myService = new MyServiceImpl();

// شما می‌توانید بخشی از رفتار را شبیه‌سازی کنید
doReturn(mockedValue).when(myService).someMethod();

تفاوت‌های کلیدی بین Mock و Spy

  1. رفتار:
    Mock: تمام متدها به‌طور پیش‌فرض شبیه‌سازی شده و هیچ‌گونه رفتار واقعی ندارند تا زمانی که شما به صراحت آن‌ها را تعریف کنید.
    Spy: متدها رفتار واقعی خود را دارند، مگر اینکه شما خاصاً بخواهید که برخی از آن‌ها شبیه‌سازی (Mock) شوند.
  2. موارد استفاده:
    Mock: برای زمانی که می‌خواهید تنها بر روی رفتار یک وابستگی تمرکز کنید و به اجرا و خروجی‌های واقعی آن نیاز ندارید.
    Spy: برای زمانی که نیاز به آزمایش رفتار واقعی برخی از متدهای یک کلاس دارید، اما می‌خواهید که دیگر متدها را شبیه‌سازی کنید.
  3. ایجاد شی:
    Mock: به سادگی یک شی Mock شده ایجاد می‌شود.
    Spy: شما باید از یک شی واقعی (نمونه) استفاده کنید و برای Spy آن را به‌عنوان شی اصلی قرار دهید.
  4. تست رفتارهای پیچیده:
    اگر شما بخواهید فقط برخی از متدها را شبیه‌سازی کنید و بقیه رفتارهای واقعی را حفظ کنید، از spy استفاده کنید.
    اگر شما به طور کامل بخواهید کلاس را شبیه‌سازی کنید و به رفتار واقعی آن نیازی ندارید، mock گزینه بهتری است.

خلاصه

Mock: برای ایجاد رفتار خاص و شبیه‌سازی وابستگی‌ها استفاده می‌شود. هیچ جزئیاتی از رفتار واقعی نخواهد داشت.
Spy: از یک شی واقعی بهره می‌گیرد و می‌توانید رفتار برخی از متدها را شبیه‌سازی کرده و بقیه را به‌صورت واقعی اجرا کند.

استفاده از mock و spy به موقعیت و نیاز شما بستگی دارد. اگر نیاز دارید که یک رفتار خاص را به‌طور کامل شبیه‌سازی کنید، از mock استفاده کنید. اما اگر بخواهید در کنار شبیه‌سازی، از عملکرد واقعی شیء نیز بهره ببرید، spy گزینه مناسب‌تری است.


3- مثال ها

- یه نمونه تست ساده

اول کار بیاید این dependency هارو اضافه کنید :

<!-- already added by Spring Initializr--> 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope> 
</dependency> 

<!-- add JUnit5 (Jupiter) --> 
<dependency> 
    <groupId>org.junit.jupiter</groupId> 
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope> 
</dependency>

<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter-api</artifactId>
   <version>5.7.1</version>
   <scope>test</scope>
</dependency>

 <!-- add Mockito -->
 <dependency> 
    <groupId>org.mockito</groupId> 
    <artifactId>mockito-core</artifactId>
    <scope>test</scope> 
</dependency>

میخوایم مثل همه جاهای دیگه ما هم یه متد بیاریم که یه عمل جمع رو انجام میده و بعد تستش کنیم :

public class Calculator {   
      public int add(int a, int b) { 
            return a + b;     
       } 
}

حالا بیایم تست رو بنویسیم :

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; 
public class CalculatorTest { 
    @Test
     public void testAdd() { 
           Calculator calculator = new Calculator(); 
            int result = calculator.add(2, 3); 
            assertEquals(5, result, &quot2 + 3 should equal 5&quot); 
      } 
}

شرح کاری که کردیم :

1- انوتیشن @Test : نشان‌دهنده این است که این متد یک تست است.

2- متد assertEquals(expected, actual) : برای مقایسه‌ی مقدار مورد انتظار (expected) و مقدار واقعی (actual) استفاده می‌شود.
3- پیام "2 + 3 should equal 5" در صورتی که تست شکست بخورد، نمایش داده می‌شود.


در JUnit انواع مختلفی از assertions برای بررسی شرایط مختلف داریم:

  • assertEquals(expected, actual): بررسی می‌کند که مقادیر برابر باشند
  • assertTrue(condition): بررسی می‌کند که شرط صحیح باشد
  • assertFalse(condition): بررسی می‌کند که شرط غلط باشد
  • assertNotNull(object): بررسی می‌کند که شیء نال نباشد
  • assertNull(object): بررسی می‌کند که شیء نال باشد


حالا یکم متد رو پیچیده ترش کنیم :

public class Calculator {
    public int divide(int a, int b) {
        if (b == 0) { 
           throw new ArithmeticException(&quotCannot divide by zero&quot); 
        }
        return a / b; 
     } 
}

بریم تستش کنیم :

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*; 
public class CalculatorTest { 
    @Test 
    public void testDivide() { 
         Calculator calculator = new Calculator(); 
         int result = calculator.divide(10, 2); 
         assertEquals(5, result, &quot10 / 2 should equal 5&quot); 
    } 
    @Test 
    public void testDivideByZero() { 
         Calculator calculator = new Calculator(); 
         ArithmeticException exception = assertThrows(ArithmeticException.class, () -> { 
            calculator.divide(10, 0);
          }); 
         assertEquals(&quotCannot divide by zero&quot, exception.getMessage()); 
     } 
}

خوب توی این تست نیاز داریم exception رو در صورت اتفاق افتادن بررسی کنیم بنابراین در متد testDivideByZero از assertThrows استفاده می‌کنیم تا اطمینان حاصل کنیم که در صورتی که بخواهیم تقسیم بر صفر انجام دهیم، استثنای ArithmeticException پرتاب می‌شود.


حالا بیایم یکم تست هامون رو تمیز تر و خواناترش کنیم :

توی تست های بالا دیدید که برای ایجاد یک نمونه از Calculator اومدیم داخل متد تستمون به ازای هر متد تست یه بار instance ساختیم .
این کار مشکلی نداره اما اگر قرار باشه چندین متد تست داشته باشیم که بخوایم توی هر کدوم از متد های یک instance از این کلاس داشته باشیم اونوقت زیاد جالب نیست .

حالا باید چیکار کنیم : در تست های گروهی میشه از after و before استفاده کرد

import org.junit.jupiter.api.*; 

public class CalculatorTest { 
    private Calculator calculator; 
    
     @BeforeEach 
     public void setUp() { 
         calculator = new Calculator(); 
      } 

     @Test
      public void testAdd() { 
           int result = calculator.add(2, 3);
           assertEquals(5, result); 
       } 

      @AfterEach 
       public void tearDown() {
             // عملیات پاکسازی، اگر نیاز باشد 
        } 
}

یکم جولو تر از mock استفاده میکنیم و روال یکم پیچیده تر میشه . (برای تست‌های پیچیده‌تر که نیاز به تعامل با پایگاه‌داده‌ها، APIها یا سرویس‌های خارجی دارند. )

لطفا تست هارو خیلی سختش نکنید . موقع تست شلکس کنید .
هر تست باید مستقل از دیگر تست‌ها و تغییرات یک تست نباید بر تست‌های دیگه تأثیر بگذارد.
سعی کنید تمام قسمت‌های مهم کد تحت پوشش بیمه حضرت تست قرار بدید.

اگر اینجا میخواستیم از mock استفاده کنیم چیطور میشد :

import static org.mockito.Mockito.*; 
import org.junit.jupiter.api.Test; 

public class CalculatorServiceTest { 
     @Test 
      public void testCalculateWithMock() { 
           Calculator calculator = mock(Calculator.class); 
           when(calculator.add(2, 3)).thenReturn(5);
           int result = calculator.add(2, 3); 
           assertEquals(5, result); 
      }
}

- یه نمونه RestApi (میایم از mock برای دیتاهامون و کلاس هامون استفاده میکنیم)

میخوایم یه controller بنویسیم که اطلاعات کاربر رو توی یه api خیلی ساده دریافت و ذخیره میکنه . توی این تیکه کدی که میزنیم لایه سرویس هم داریم که طبیعتا توی لایه سرویس ریپازیتوری داریم و ...

@RestController 
@RequestMapping(&quot/user/profile&quot) 
public class UserProfileController { 
     @Autowired 
     private UserProfileService userProfileService; 
     
     @PostMapping 
     public UserProfile createUser(@RequestBody UserProfile user){ 
             return userProfileService.addUser(user); 
     } 
     
     @GetMapping 
     public List<UserProfile> findAllUsers(){ 
           return userProfileService.findAllUsers(); 
     } 
}

خوب همه چیز فکر کنم مشخص باشه حالا بریم تست رو بنویسیم :

@ExtendWith(MockitoExtension.class)
public class UserProfileControllerTest {
    @InjectMocks
    private UserProfileController userProfileController;

    @Mock
    private UserProfileService userProfileService;

    @DisplayName(&quotTest - Insert New userProfile - test without exception&quot)
    @Test
    public void insertNewUserProfile(){
        UserProfile user = new UserProfile();
        user.setId(1);
        user.setFirstName(&quotmehrdad&quot);
        user.setLastName(&quotkhanzadi&quot);
        user.phoneNumber(&quot09120909090&quot)

        given(userProfileController.createUser(user)).willReturn(user);

        UserProfile userProfile = userProfileController.createUser(user);

       assertThat(userProfile.getId()).isEqualTo(user.getId());
       assertThat(userProfile.getFirstName()).isEqualTo(user.getFirstName());
       assertThat(userProfile.getLastName()).isEqualTo(user.getLastName());
       assertThat(userProfile.getPhoneNumber()).isEqualTo(user.getPhoneNumber());
    }

    @DisplayName(&quotTest - findAll and getAll userProfiles - test without exception&quot)
    @Test
    public void findAllData(){
        UserProfile firstUser = new UserProfile();
        firstUser.setId(1);
        firstUser.setFirstName(&quotmehrdad&quot); 
        firstUser.setLastName(&quotkhanzadi&quot);  
        firstUser.phoneNumber(&quot09120909090&quot)

        UserProfile secondUser = new UserProfile();
        secondUser.setId(2);
        secondUser.setFirstName(&quotsadegh&quot); 
        secondUser.setLastName(&quotkhanzadi&quot); 
        secondUser.phoneNumber(&quot09126959493&quot)

        List<UserProfile> users = new ArrayList<>;
        users.add(firstUser);
        users.add(secondUser)

       given(userProfileController.findAllData()).willReturn(allPosts);

       List<UserProfile> results = postController.findAllUsers();
       assertThat(results).hasSize(2);

        users.stream.forEach(R -> {
           assertThat(R.getFirstName()).isEqualTo(&quotmehrdad&quot);
           assertThat(R.getPhoneNumber()).isEqualTo(&quot09030909090&quot);
        });
    }
}

@ExtendWith(MockitoExtension.class)

در نسخه‌های جدید JUnit (JUnit 5) برای اتصال Mockito به سیستم تست JUnit استفاده می‌شود. این انوتیشن به صورت خودکار mock‌ها را ایجاد کرده و تزریق می‌کند، به طوری که نیاز به انجام تنظیمات دستی نیست.

@InjectMocks

این انوتیشن یک نمونه از کلاسی که شما در حال تست آن هستید ایجاد می‌کند و mock‌هایی که با انوتیشن @Mock مشخص شده‌اند را به صورت خودکار در آن تزریق می‌کند. در واقع، @InjectMocks به Mockito می‌گوید که تمام mock‌های ساخته شده باید به این کلاس تزریق شوند تا به‌طور خودکار تست‌ها را بر اساس نیاز شما انجام دهد.

@Mock

این انوتیشن برای ایجاد پیاده‌سازی mock برای کلاس‌ها یا وابستگی‌هایی که در تست به آن‌ها نیاز دارید استفاده می‌شود. به عبارت ساده‌تر، هر زمانی که شما نیاز به شبیه‌سازی یک شیء از کلاس خاصی برای تست دارید، از این انوتیشن استفاده می‌کنید. @Mock به Mockito می‌گوید که یک نسخه جعلی از این کلاس بسازد که بتوانید رفتار آن را کنترل و بررسی کنید.

نکته : دوستان این روش تست برای restApi هامون خوبه ولی خیلی جاها میبینید که با استفاده از متد های موجود در controller میان و تست رو انجام میدن اینجا هم ما از userProfileController استفاده کردیم و یه ماک ساختیم ازش :

@InjectMocks
private UserProfileController userProfileController;

حالا میرید یه جایی میبینید به جای اینکه بیان unit by unit تست کنن میان integration test انجام میدن و این بستگی به جنس کاری شما داره ، یه جایی لازم دارید کل روال کار با صدا زدن یه سرویس تا انتهای ذخیره سازی توی دیتابیس پیش بره و جواب صحیح و یا خطا برگشت داده بشه ، میشه به صورت container بیایم با docker در زمان test دیتابیس و کش و ... رو بیاریم بالا و تست هارو بزنیم وقتی تست تمام شد container دیتابیس و ... بسته میشه و دوشواری هم نداریم . (مبحث integrationTest رو توی یه آموزش دیگه باید بررسی کنیم)

-یه نمونه mock هزارتایی از دیتابیس:

خوب اول بیایم برای مثال قبل که زدیم برای تست repository یه مدل تست بریم :

لایه repository:

public interface UserProfileRepository extend JpaRepository<User,Long> { 
       User findById(Long userId); 
}

لایه service:

@Service 
public class UserProfileService { 
       private final UserProfileRepository userRepository; 
      
        @Autowired 
        public UserService(UserProfileRepository userRepository) { 
               this.userRepository = userRepository; 
        }
 
        public UserProfile getUserById(Long userId) { 
             return userRepository.findById(userId).convertToDto(); 
        } 
}

بریم تستش رو داشته باشیم :

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {

    @Mock
     UserProfileRepository userRepository;
   
    @InjectMocks
    UserProfileService userService;
 
    @BeforeEach
    public setUp() {
      MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserById() {
      User mockUser = new User(1L, &quotSadegh&quot, &quotKhanzadi&quot, &quot09030909090&quot);
      
      when(userRepository.findById(1L)).thenReturn(mockUser);
      UserProfile user = userService.getUserById(1L);
      assertThat(user.getFirstName()).isEqualTo(&quotSadegh&quot);
    }
}

اگر دقت کنید این بالا یدونه دیتا داشتیم حالا اگر بخوایم 1000 تا user داشته باشیم چی میشه ؟؟

خودتونم میتونید راهکار داشته باشیدا این راهکار منه

public class UserServiceTest {

    @Mock
    UserProfileRepository userRepository;

    @InjectMocks
    UserProfileService userService;

    @BeforeEach
    public void setUp() {
       MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserByIdWithMultipleUsers() {
       List<User> mockUsers = TestDataGenerator.generateUserList(1000);

       when(userRepository.findById(500L)).thenReturn(mockUsers.get(499)); // کاربر شماره 500

       UserProfile user = userService.getUserById(500L);
       assertEquals(&quotUser 500&quot, user.getName());
    }

    public static List<User> generateUserList(int count) { 
          List<User> users = new ArrayList<>(); 
          for (int i = 1; i <= count; i++) {      
               users.add(new User((long) i, &quotUser &quot + i));     
           }       
           return users;   
      }
}

متد generateUserList میاد 1000 تا کاربر میسازه اول کار و بعد کار با اون 1000 کاربر پیش میره.


امیدوارم راضی بوده باشید

تلاش میکنم در یه آموزش جدا بیام InegrationTest رو توضیح بدم .

با تشکر از نگاه های زیباتون .