اختبار في Java: من الاختبارات الوحدوية إلى الاختبارات الشاملة
جودة الكود هي أحد المؤشرات الرئيسية لاحترافية المطور. في عالم Java، لم يعد الاختبار مجرد ممارسة جيدة، بل أصبح معيارًا صناعيًا. بدون اختبارات موثوقة، يصعب صيانة المشاريع الكبيرة، وإضافة ميزات جديدة، وإعادة هيكلة الكود بثقة. في هذه المقالة، سنستعرض ثلاثة مستويات رئيسية للاختبار: unit (الاختبار الوحدوي)، integration (اختبار التكامل)، و e2e (الاختبار الشامل). ستتعرف على الاختلافات بينها، وكيفية كتابتها بشكل صحيح في Java، والأدوات المستخدمة.
هرم الاختبار ولماذا هو مهم
قبل التعمق في التفاصيل، يجدر بنا تذكر مفهوم هرم الاختبار (Mike Cohn). إنه يصور النسبة المثالية للاختبارات على مستويات مختلفة:
- اختبارات Unit (قاعدة الهرم) — الأسرع، الأقل تكلفة، والأكثر عددًا. يجب أن تشكل حوالي 70%.
- اختبارات Integration (الوسط) — تتحقق من تفاعل المكونات. تشكل حوالي 20%.
- اختبارات E2E (القمة) — الأبطأ والأغلى، ولكنها حاسمة للتحقق من سيناريوهات المستخدم. تشكل حوالي 10%.
الالتزام بهذه النسبة يتيح الحصول على أقصى ثقة في الكود بأقل تكلفة زمنية لتشغيل الاختبارات.
1. الاختبار الوحدوي (Unit-testing) في Java
اختبارات Unit تتحقق بشكل منعزل من "وحدة" واحدة من الكود — عادةً ما تكون دالة أو كلاس. هدفها الرئيسي هو التأكد من أن منطق الدالة يعمل بشكل صحيح، بغض النظر عن التبعيات الخارجية (قاعدة بيانات، API، نظام ملفات). للعزل، يتم استخدام mock-كائنات.
الأدوات الرئيسية
- JUnit 5 — الإطار القياسي لكتابة الاختبارات.
- Mockito — مكتبة لإنشاء mock-كائنات والتحقق من التفاعلات.
- AssertJ — مكتبة للتأكيدات السلسة (fluent-assertions) (تزيد من قابلية القراءة).
مثال لاختبار unit مع JUnit 5 و Mockito
لنفترض أن لدينا خدمة UserService تعتمد على UserRepository:
public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public User findById(Long id) { return repository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found")); }}اختبار لهذه الدالة:
import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.junit.jupiter.MockitoExtension;import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)class UserServiceTest {
@Mock private UserRepository repository;
@InjectMocks private UserService userService;
@Test void testFindById_UserExists() { // Given Long userId = 1L; User expectedUser = new User(userId, "Alice"); when(repository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When User actualUser = userService.findById(userId);
// Then assertNotNull(actualUser); assertEquals("Alice", actualUser.getName()); verify(repository).findById(userId); }
@Test void testFindById_UserNotFound() { // Given Long userId = 99L; when(repository.findById(userId)).thenReturn(Optional.empty());
// When & Then assertThrows(UserNotFoundException.class, () -> userService.findById(userId)); }}أفضل الممارسات:
- اختبر و