امروز برنامهای را پیدا نمیکنید که با آزمایش پوشیده نشده باشد، بنابراین این موضوع برای توسعهدهندگان مبتدی بیش از همیشه مرتبط خواهد بود: بدون آزمایش نمیتوانید موفق شوید. بیایید در نظر بگیریم که اصولاً چه نوع آزمایشی مورد استفاده قرار می گیرد و سپس همه چیزهایی را که در مورد تست واحد باید بدانیم با جزئیات مطالعه خواهیم کرد.
انواع تست
تست چیست؟ طبق ویکی پدیا: "آزمایش نرم افزار شامل اجرای یک جزء نرم افزار یا جزء سیستم برای ارزیابی یک یا چند ویژگی مورد علاقه است." به عبارت دیگر، بررسی صحت سیستم ما در شرایط خاص است. خوب، بیایید ببینیم به طور کلی چه نوع آزمایشی وجود دارد:- تست واحد - تست هایی که هدف آن بررسی هر یک از ماژول های سیستم به طور جداگانه است. این آزمایشها باید برای کوچکترین بخشهای اتمی سیستم، به عنوان مثال، ماژولها اعمال شوند.
- تست سیستم - تست سطح بالا برای بررسی عملکرد یک قطعه بزرگتر از برنامه یا سیستم به عنوان یک کل.
- تست رگرسیون - آزمایشی که برای بررسی اینکه آیا ویژگی های جدید یا رفع اشکال بر عملکرد موجود برنامه تأثیر می گذارد یا اشکالات قدیمی را معرفی می کند، استفاده می شود.
- تست عملکرد - بررسی اینکه آیا بخشی از برنامه الزامات ذکر شده در مشخصات، داستان های کاربر و غیره را برآورده می کند یا خیر.
انواع تست عملکردی:
- تست جعبه سفید - بررسی اینکه آیا بخشی از برنامه الزامات را برآورده می کند در حالی که از پیاده سازی داخلی سیستم اطلاع دارید.
- تست جعبه سیاه - بررسی اینکه آیا بخشی از برنامه بدون اطلاع از اجرای داخلی سیستم، الزامات را برآورده می کند یا خیر.
- تست عملکرد - تست هایی که برای تعیین نحوه عملکرد سیستم یا بخشی از سیستم تحت یک بار مشخص نوشته می شوند.
- تست بار - آزمایشهایی که برای بررسی پایداری سیستم تحت بارهای استاندارد و یافتن حداکثر باری که برنامه هنوز در آن به درستی کار میکند طراحی شده است.
- تست استرس - تست طراحی شده برای بررسی عملکرد برنامه تحت بارهای غیر استاندارد و تعیین حداکثر بار قبل از خرابی سیستم.
- تست امنیتی - تستهایی که برای بررسی امنیت سیستم (از هکرها، ویروسها، دسترسی غیرمجاز به دادههای محرمانه و سایر حملات لذتبخش) استفاده میشوند.
- تست محلی سازی - تست های محلی سازی برنامه.
- تست قابلیت استفاده - آزمایشی با هدف بررسی قابلیت استفاده، قابل فهم بودن، جذابیت و یادگیری پذیری.
- واحد - این بخش به تست های واحد اشاره دارد که در لایه های مختلف برنامه اعمال می شود. آنها کوچکترین واحد قابل تقسیم منطق کاربردی را آزمایش می کنند. به عنوان مثال، کلاس ها، اما اغلب متدها. این تستها معمولاً سعی میکنند تا حد امکان آنچه را که آزمایش میشود از هر منطق بیرونی جدا کنند. یعنی سعی می کنند این توهم را ایجاد کنند که بقیه برنامه طبق انتظار اجرا می شود.
همیشه باید تعداد زیادی از این تست ها (بیشتر از هر نوع دیگری) وجود داشته باشد، زیرا آنها قطعات کوچک را آزمایش می کنند و بسیار سبک وزن هستند و منابع زیادی را مصرف نمی کنند (منظور رم و زمان است).
- ادغام - این بخش به تست ادغام اشاره دارد. این آزمایش قطعات بزرگتر سیستم را بررسی می کند. یعنی یا چندین قطعه منطق (چند روش یا کلاس) را ترکیب می کند، یا صحت تعامل با یک جزء خارجی را بررسی می کند. این تست ها معمولا کوچکتر از تست های واحد هستند زیرا سنگین تر هستند.
نمونه ای از تست یکپارچه سازی می تواند اتصال به پایگاه داده و بررسی صحت عملکرد روش های کار با آن باشد.
- UI - این بخش به تست هایی اشاره دارد که عملکرد رابط کاربری را بررسی می کنند. آنها منطق را در تمام سطوح برنامه در بر می گیرند، به همین دلیل است که آنها را تست های پایان به پایان نیز می نامند. به عنوان یک قاعده، تعداد آنها بسیار کمتر است، زیرا آنها دست و پا گیر ترین هستند و باید مسیرهای ضروری (استفاده شده) را بررسی کنند.
در تصویر بالا، میبینیم که اندازههای قسمتهای مختلف مثلث متفاوت است: نسبتهای تقریباً یکسانی در تعداد انواع مختلف آزمایش در کار واقعی وجود دارد.
امروز نگاهی دقیقتر به رایجترین تستها، تستهای واحد خواهیم داشت، زیرا همه توسعهدهندگان جاوا که به خود احترام میگذارند باید بتوانند از آنها در سطح پایه استفاده کنند.
مفاهیم کلیدی در تست واحد
پوشش تست (پوشش کد) یکی از معیارهای اصلی میزان تست یک برنامه کاربردی است. این درصد کدی است که تحت پوشش تست ها قرار می گیرد (0-100%). در عمل، بسیاری این درصد را به عنوان هدف خود دنبال می کنند. این چیزی است که من با آن مخالفم، زیرا به این معنی است که آزمایشها در جایی که نیازی به آنها نیست، شروع میشوند. به عنوان مثال، فرض کنید ما عملیات استاندارد CRUD (ایجاد/دریافت/به روز رسانی/حذف) را بدون منطق اضافی در سرویس خود داریم. این روش ها صرفاً واسطه هایی هستند که کار را به لایه ای که با مخزن کار می کند واگذار می کنند. در این شرایط، ما چیزی برای آزمایش نداریم، به جز اینکه آیا روش داده شده یک متد DAO را فراخوانی می کند، اما این یک شوخی است. معمولاً از ابزارهای اضافی برای ارزیابی پوشش آزمایشی استفاده می شود: JaCoCo، Cobertura، Clover، Emma، و غیره. برای مطالعه دقیق تر این موضوع، در اینجا چند مقاله مرتبط وجود دارد: TDD مخفف عبارت test-driven development است. در این روش، قبل از انجام هر کار دیگری، تستی می نویسید که کد خاصی را بررسی می کند. معلوم می شود که این آزمایش جعبه سیاه است: ما می دانیم ورودی است و می دانیم که خروجی باید چه باشد. این امکان جلوگیری از تکرار کد را فراهم می کند. توسعه مبتنی بر تست با طراحی و توسعه تستها برای هر بیت از عملکرد برنامه شما شروع میشود. در رویکرد TDD، ابتدا تستی ایجاد می کنیم که رفتار کد را تعریف و آزمایش می کند. هدف اصلی TDD این است که کد شما قابل فهم تر، ساده تر و بدون خطا باشد. رویکرد شامل موارد زیر است:- ما آزمون خود را می نویسیم.
- ما تست را اجرا می کنیم. جای تعجب نیست، زیرا ما هنوز منطق مورد نیاز را اجرا نکرده ایم، شکست می خورد.
- کدی را که باعث گذراندن آزمون می شود اضافه کنید (آزمون را دوباره اجرا می کنیم).
- ما کد را بازسازی می کنیم.
مراحل تست
یک آزمون شامل سه مرحله است:- داده های تست (لوازم) را مشخص کنید.
- کد مورد آزمایش را اعمال کنید (روش تست شده را فراخوانی کنید).
- نتایج را بررسی کنید و با نتایج مورد انتظار مقایسه کنید.
محیط های تست
بنابراین، در حال حاضر به نقطه. چندین محیط تست (فریم ورک) برای جاوا موجود است. محبوب ترین آنها JUnit و TestNG هستند. برای بررسی ما در اینجا، ما از این استفاده می کنیم: آزمون JUnit روشی در کلاس است که فقط برای آزمایش استفاده می شود. نام کلاس معمولاً همان کلاسی است که تست میکند و "Test" به انتهای آن اضافه میشود. به عنوان مثال، CarService -> CarServiceTest. سیستم ساخت Maven به طور خودکار چنین کلاس هایی را در محدوده آزمایش قرار می دهد. در واقع به این کلاس کلاس تست می گویند. اجازه دهید به طور خلاصه به حاشیه نویسی های اساسی بپردازیم:- @Test نشان می دهد که روش یک تست است (در اصل، روشی که با این حاشیه مشخص شده است یک تست واحد است).
- @Before نشان دهنده روشی است که قبل از هر تست اجرا می شود. به عنوان مثال، برای پر کردن یک کلاس با داده های آزمایشی، خواندن داده های ورودی و غیره.
- @After برای علامت گذاری روشی استفاده می شود که پس از هر آزمایش فراخوانی می شود (مثلاً برای پاک کردن داده ها یا بازیابی مقادیر پیش فرض).
- @BeforeClass در بالای یک متد، مشابه @Before قرار می گیرد. اما چنین متدی فقط یک بار قبل از تمام تست های کلاس داده شده فراخوانی می شود و بنابراین باید ثابت باشد. از آن برای انجام عملیات با منابع فشرده تر، مانند چرخش یک پایگاه داده آزمایشی استفاده می شود.
- @AfterClass برعکس @BeforeClass است: یک بار برای کلاس داده شده اجرا می شود، اما فقط پس از تمام تست ها. برای مثال برای پاک کردن منابع پایدار یا قطع ارتباط با پایگاه داده استفاده می شود.
- @Ignore نشان می دهد که یک روش غیرفعال است و در طول اجرای آزمایشی کلی نادیده گرفته می شود. این در شرایط مختلف استفاده می شود، به عنوان مثال، اگر روش پایه تغییر کرده باشد و تست هنوز برای تطبیق با تغییرات دوباره کار نشده باشد. در چنین مواردی، اضافه کردن یک توضیحات، یعنی @Ignore ("Some description") نیز مطلوب است.
- @Test(expected = Exception.class) برای تست های منفی استفاده می شود. اینها تست هایی هستند که بررسی می کنند روش در صورت بروز خطا چگونه رفتار می کند، یعنی آزمون انتظار دارد که متد نوعی استثنا ایجاد کند. چنین روشی با حاشیه نویسی @Test نشان داده می شود، اما با نشان دادن اینکه کدام خطا را باید گرفت.
- @Test(timeout = 100) بررسی می کند که روش در حداکثر 100 میلی ثانیه اجرا شود.
- @Mock در بالای یک فیلد برای اختصاص یک شی ساختگی استفاده می شود (این حاشیه نویسی JUnit نیست، بلکه از Mockito می آید). در صورت نیاز، رفتار ماکت را برای یک موقعیت خاص مستقیماً در روش تست تنظیم می کنیم.
- @RunWith(MockitoJUnitRunner.class) بالای یک کلاس قرار می گیرد. این حاشیه نویسی به JUnit می گوید که تست های کلاس را فراخوانی کند. رانرهای مختلفی وجود دارد، از جمله این موارد: MockitoJUnitRunner، JUnitPlatform و SpringRunner. در JUnit 5، حاشیه نویسی @RunWith با حاشیه نویسی قدرتمندتر @ExtendWith جایگزین شده است.
- assertEquals (Object مورد انتظار، Object actuals) - بررسی می کند که آیا اشیاء ارسال شده برابر هستند یا خیر.
- assertTrue (پرچم بولی) - بررسی می کند که آیا مقدار ارسال شده درست است یا خیر.
- assertFalse (پرچم بولی) - بررسی می کند که آیا مقدار ارسال شده نادرست است یا خیر.
- assertNull (شیء شی) - بررسی می کند که آیا شیء ارسال شده تهی است یا خیر.
- assertSame (Object firstObject، Object secondObject) - بررسی می کند که آیا مقادیر ارسال شده به یک شی اشاره دارد یا خیر.
- ادعا می کند که (T t, Matcher
همسان) - بررسی می کند که آیا t شرایط مشخص شده در matcher را برآورده می کند یا خیر.
تست در عمل
حال بیایید در یک مثال خاص به مطالب بالا نگاه کنیم. ما روش به روز رسانی یک سرویس را آزمایش خواهیم کرد. ما لایه DAO را در نظر نخواهیم گرفت، زیرا از پیش فرض استفاده می کنیم. بیایید یک شروع کننده برای تست ها اضافه کنیم:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
و در اینجا ما کلاس خدمات را داریم:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
خط 8 - شی به روز شده را از پایگاه داده بیرون بکشید. خطوط 9-14 - یک شی از طریق سازنده ایجاد کنید. اگر شی ورودی دارای یک فیلد است، آن را تنظیم کنید. اگر نه، آنچه را که در پایگاه داده است می گذاریم. حالا به تست ما نگاه کنید:
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
خط 1 - دونده ما. خط 4 - با جایگزین کردن یک ماک، سرویس را از لایه DAO جدا می کنیم. خط 11 - ما یک موجودیت آزمایشی (آنی که به عنوان خوکچه هندی استفاده خواهیم کرد) را برای کلاس تنظیم کردیم. خط 22 - ما شیء سرویس را تنظیم می کنیم که همان چیزی است که آزمایش خواهیم کرد.
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
در اینجا می بینیم که آزمون دارای سه تقسیم بندی واضح است: خطوط 3-9 - مشخص کردن وسایل. خط 11 - اجرای کد تحت آزمایش. خطوط 13-17 - بررسی نتایج. با جزئیات بیشتر: خطوط 3-4 - رفتار را برای ساختگی DAO تنظیم کنید. خط 5 - نمونه ای را تنظیم کنید که در بالای استاندارد خود به روز می کنیم. خط 11 - از روش استفاده کنید و نمونه حاصل را بگیرید. خط 13 - بررسی کنید که پوچ نباشد. خط 14 - شناسه نتیجه و آرگومان های متد داده شده را با هم مقایسه کنید. خط 15 - بررسی کنید که آیا نام به روز شده است یا خیر. خط 16 - نتیجه CPU را ببینید. خط 17 - ما این فیلد را در نمونه مشخص نکردیم، بنابراین باید به همان صورت باقی بماند. ما آن شرایط را در اینجا بررسی می کنیم. بیایید آن را اجرا کنیم: تست سبز است! ما می توانیم نفس راحتی بکشیم :) به طور خلاصه، آزمایش کیفیت کد را بهبود می بخشد و روند توسعه را انعطاف پذیرتر و قابل اعتمادتر می کند. تصور کنید چقدر تلاش برای طراحی مجدد نرم افزار شامل صدها فایل کلاسی لازم است. وقتی برای همه این کلاسها تستهای واحد نوشته شده باشد، میتوانیم با اطمینان اصلاح کنیم. و مهمتر از همه، به ما کمک می کند تا به راحتی باگ ها را در طول توسعه پیدا کنیم. بچه ها و دخترها، این تمام چیزی است که امروز دارم. لایک کن و نظر بده :)
GO TO FULL VERSION