انتقل إلى المحتوى الرئيسي

الفصل 1: مقدمة في الاختبارات

مقدمة​

قبل كتابة اختبارك الأول، من الضروري فهم لماذا وكيف تختبر بفعالية. هذا الفصل يضع الأسس لاستراتيجية اختبار احترافية.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ لماذا الاختبارات ضرورية
  2. ✅ التمييز بين أنواع الاختبارات المختلفة
  3. ✅ فهم هرم الاختبارات
  4. ✅ إعداد مشروعك للاختبارات
  5. ✅ معرفة أفضل الممارسات

1. لماذا نختبر؟​

تكلفة الأخطاء​

لحظة الاكتشافالتكلفة النسبيةالتأثير
أثناء التطوير1xلا شيء
في مرحلة الاختبار10xتأخير
في الإنتاج100xعميل متأثر، حالة طارئة

فوائد الاختبارات​

✅ الثقة: الكود يقوم بما يجب عليه
✅ التراجع: يكتشف الأخطاء التي تم إدخالها بواسطة التغييرات
✅ التوثيق: تشرح الاختبارات السلوك المتوقع
✅ التصميم: كتابة الاختبارات تفرض تصميمًا جيدًا (SOLID)
✅ إعادة الهيكلة: تعديل الكود دون خوف
✅ النشر: CI/CD مؤتمت

بدون اختبارات مقابل مع اختبارات​

// بدون اختبارات 😰
public void deployToProduction() {
// "آمل أن يعمل..."
// "سنرى في الإنتاج..."
// "لنصلي..."
}

// مع اختبارات 😎
public void deployToProduction() {
// ✅ 347 اختبارات ناجحة
// ✅ التغطية: 85%
// ✅ جميع بوابات الجودة ناجحة
// → نشر بثقة!
}

2. أنواع الاختبارات​

نظرة عامة​

اختبارات وحدات​

ما هي: اختبار وحدة من الكود في عزلة (طريقة، فئة)

الخصائص:

  • سريعة (ميلي ثانية)
  • معزولة (لا قاعدة بيانات، لا شبكة)
  • عديدة (غالبية الاختبارات)
@Test
void shouldCalculatePriceWithDiscount() {
// يختبر فقط منطق الحساب
PriceCalculator calculator = new PriceCalculator();

double result = calculator.applyDiscount(100.0, 20);

assertEquals(80.0, result);
}

اختبارات تكامل​

ما هي: اختبار عدة مكونات معًا

الخصائص:

  • أبطأ (ثواني)
  • تستخدم موارد حقيقية (قاعدة بيانات، إلخ.)
  • أقل عددًا
@SpringBootTest
class OrderServiceIntegrationTest {

@Autowired
private OrderService orderService;

@Test
void shouldSaveOrderToDatabase() {
// يختبر الخدمة + المستودع + قاعدة البيانات
Order order = orderService.createOrder(/*...*/);

assertNotNull(order.getId());
}
}

اختبارات شاملة (E2E)​

ما هي: اختبار التطبيق الكامل كما يفعل المستخدم

الخصائص:

  • بطيئة (دقائق)
  • بيئة كاملة
  • قليلة العدد
// مع Selenium على سبيل المثال
@Test
void userShouldBeAbleToCompleteCheckout() {
browser.navigate("/products");
browser.click("#add-to-cart");
browser.click("#checkout");
browser.fill("#address", "123 Main St");
browser.click("#confirm");

assertThat(browser.getText("#confirmation"))
.contains("Order confirmed");
}

3. هرم الاختبارات​

الهيكل المثالي​

                    /\
/ \
/ E2E\ قليل، بطيء، هش
/ \
/────────\
/Integration\ معتدل
/ \
/────────────────\
/ Unit Tests \ كثير، سريع، مستقر
/____________________\

النسب الموصى بها​

النوعالنسبةوقت التنفيذ
وحدات70%~5 ثواني
تكامل20%~30 ثانية
شاملة10%~5 دقائق

نمط مضاد: مخروط الآيس كريم​

            ____________________
/ الكثير من E2E \ ❌ بطيء، هش
/────────────────────────\
/ قليل من التكامل \
/────────────────────────────\
/ قليل من الاختبارات \
/ الوحدات \
/──────────────────────────────────\

❌ سيء: اختبارات بطيئة، صيانة صعبة

4. TDD و BDD​

TDD (تطوير مدفوع بالاختبار)​

كتابة الاختبارات قبل الكود:

// الخطوة 1: RED - يفشل الاختبار (الطريقة غير موجودة)
@Test
void shouldReturnEmptyListWhenNoProducts() {
ProductService service = new ProductService();
List<Product> products = service.findAll();
assertTrue(products.isEmpty());
}

// الخطوة 2: GREEN - تنفيذ الحد الأدنى
public List<Product> findAll() {
return new ArrayList<>();
}

// الخطوة 3: REFACTOR - تحسين إذا لزم الأمر

BDD (تطوير مدفوع بالسلوك)​

اختبارات تصف السلوك بلغة طبيعية:

// صيغة Given-When-Then
@Test
void givenNewUser_whenRegister_thenAccountIsCreated() {
// Given (السياق)
RegistrationRequest request = new RegistrationRequest(
"john", "john@email.com", "password123"
);

// When (الإجراء)
User user = userService.register(request);

// Then (النتيجة المتوقعة)
assertNotNull(user.getId());
assertEquals("john", user.getUsername());
assertTrue(user.isEnabled());
}

5. إعداد Maven​

تبعيات الاختبار​

<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Test (يشمل JUnit 5، Mockito، AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- H2 للاختبارات -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<!-- اختبار الأمان (اختياري) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

ما هو مشمول في spring-boot-starter-test​

spring-boot-starter-test يشمل:
├── JUnit 5 (Jupiter)
├── Mockito
├── AssertJ
├── Hamcrest
├── JSONPath
├── JsonAssert
└── Spring Test

هيكل ملفات الاختبار​

src/
├── main/
│ └── java/
│ └── com/example/
│ ├── service/
│ │ └── ProductService.java
│ └── repository/
│ └── ProductRepository.java
└── test/
└── java/
└── com/example/
├── service/
│ └── ProductServiceTest.java ← اختبارات وحدات
├── repository/
│ └── ProductRepositoryTest.java ← اختبارات تكامل
└── controller/
└── ProductControllerTest.java ← اختبارات API

6. إعداد الاختبار​

application-test.properties​

# src/test/resources/application-test.properties

# قاعدة بيانات H2 في الذاكرة
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop

# تعطيل السجلات المفصلة
logging.level.org.hibernate.SQL=WARN
spring.jpa.show-sql=false

# تعطيل Flyway للاختبارات (اختياري)
spring.flyway.enabled=false

تفعيل ملف تعريف الاختبار​

@SpringBootTest
@ActiveProfiles("test") // ← يستخدم application-test.properties
class MyIntegrationTest {
// ...
}

7. تنفيذ الاختبارات​

عبر Maven​

# جميع الاختبارات
mvn test

# اختبارات مع تقرير
mvn test jacoco:report

# اختبار محدد
mvn test -Dtest=ProductServiceTest

# طريقة محددة
mvn test -Dtest=ProductServiceTest#shouldFindProductById

# تخطي الاختبارات (يجب تجنبه!)
mvn install -DskipTests

عبر IDE​

IntelliJ IDEA:
- انقر بزر الماوس الأيمن على الفئة/الطريقة → Run Test
- Ctrl+Shift+F10 (Windows)
- Ctrl+Shift+R (Mac)

VS Code:
- أيقونة "Run Test" فوق @Test
- Test Explorer في الشريط الجانبي

8. أفضل الممارسات​

تسمية الاختبارات​

// الصيغة الموصى بها: should_ExpectedBehavior_When_StateUnderTest
@Test
void should_ReturnEmpty_When_NoProductsExist() { }

// أو: given_When_Then
@Test
void given_ValidProduct_When_Save_Then_IdIsGenerated() { }

// تجنب:
@Test
void test1() { } // ❌ غير وصفي
@Test
void testProductService() { } // ❌ غامض جدًا

نمط AAA (ترتيب-تنفيذ-تحقق)​

@Test
void shouldCalculateTotalPrice() {
// ترتيب (تحضير)
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("Laptop", 1000.0));
cart.addItem(new Item("Mouse", 50.0));

// تنفيذ
double total = cart.calculateTotal();

// تحقق
assertEquals(1050.0, total);
}

اختبار واحد = مفهوم واحد​

// ❌ سيء: يختبر عدة أشياء
@Test
void testProduct() {
Product p = new Product("Test", 100.0);
assertEquals("Test", p.getName());
assertEquals(100.0, p.getPrice());
p.applyDiscount(10);
assertEquals(90.0, p.getPrice());
assertThrows(Exception.class, () -> p.applyDiscount(-5));
}

// ✅ جيد: اختبار واحد لكل سلوك
@Test
void shouldCreateProductWithNameAndPrice() {
Product p = new Product("Test", 100.0);
assertEquals("Test", p.getName());
assertEquals(100.0, p.getPrice());
}

@Test
void shouldApplyDiscountCorrectly() {
Product p = new Product("Test", 100.0);
p.applyDiscount(10);
assertEquals(90.0, p.getPrice());
}

@Test
void shouldRejectNegativeDiscount() {
Product p = new Product("Test", 100.0);
assertThrows(IllegalArgumentException.class,
() -> p.applyDiscount(-5));
}

9. ملخص​

النقاط الرئيسية​

المفهومالوصف
اختبار وحداتيختبر وحدة في عزلة
اختبار تكامليختبر عدة مكونات معًا
اختبار شامليختبر التطبيق الكامل
TDDاختبارات مكتوبة قبل الكود
AAAترتيب-تنفيذ-تحقق

هرم الاختبارات​

10% شامل (بطيء، هش)
20% تكامل (معتدل)
70% وحدات (سريع، كثير)

قائمة التحقق​

✅ spring-boot-starter-test في pom.xml
✅ application-test.properties مُعد
✅ اختبارات في src/test/java
✅ تسمية وصفية
✅ اختبار واحد = مفهوم واحد
✅ نمط AAA

🎯 الخطوة التالية​

حان وقت التطبيق! دعونا نستكشف JUnit 5 بالتفصيل.

➡️ الفصل 2: اختبارات وحدات مع JUnit 5


⬅️ العودة إلى الوحدة 9