اختبار الوحدة هو طريقة للتحقق من البرمجيات يتم فيها اختبار صحة عمل الوحدات الفردية أو دوال الكود بشكل منعزل عن بقية النظام. وفقاً لـ Martin Fowler, 2026، تعد اختبارات الوحدة أساس CI/CD وإعادة الهيكلة، حيث توفر تغذية راجعة سريعة حول صحة الكود. الاختبار النمطي يساعد في اكتشاف الأخطاء في المراحل المبكرة من التطوير، مما يقلل تكلفة إصلاحها بعشرات المرات.
النقاط الرئيسية
اختبار الوحدة هو عملية التحقق من الوحدات الفردية من الكود المصدري — الدوال، الطرق، الأصناف — بشكل منعزل عن بقية البرنامج. يقوم كل اختبار بتشغيل سيناريو استخدام محدد للوحدة ويتحقق من أن النتيجة تطابق المتوقع. تُكتب اختبارات الوحدة بنفس لغة البرمجة المستخدمة في الكود الرئيسي ويتم تشغيلها تلقائياً في بيئة التطوير أو في خط أنابيب CI/CD. على عكس اختبارات التكامل، لا تتفاعل اختبارات الوحدة مع قواعد البيانات الحقيقية أو نظام الملفات أو خدمات الشبكة.
الهدف الرئيسي هو التغذية الراجعة السريعة حول صحة الكود بعد التغييرات. إذا قام المطور بإعادة هيكلة دالة ما، فإن مجموعة اختبارات الوحدة تؤكد أن السلوك لم يتعطل. وفقاً لـ Google Testing Blog (2025)، المشاريع التي تغطي اختبارات الوحدة فيها أكثر من 60% لديها حوادث إنتاج أقل بمقدار 2.5 مرة. فوائد إضافية: توثيق الكود (تظهر الاختبارات كيفية استخدام API)، تبسيط إعادة الهيكلة (يمكن تغيير التنفيذ مع الحفاظ على السلوك)، والتشخيص السريع للانتكاسات.
ليس كل اختبار آلي هو اختبار وحدة. المعايير: يتم اختبار وحدة واحدة (صنف أو دالة)، يتم استبدال التبعيات الخارجية بالموكس أو الستب، يتم تنفيذ الاختبار بالمللي ثانية، ولا يتطلب تشغيل خادم أو قاعدة بيانات. الاختبار الذي يصل إلى قاعدة بيانات حقيقية هو اختبار تكامل. الاختبار الذي يفتح متصفحاً هو اختبار E2E. فهم الحدود بين أنواع الاختبارات مهم لتوزيع الجهود بشكل صحيح في هرم الاختبارات.
تتبع اختبارات الوحدة عالية الجودة مبادئ FIRST التي صاغها Robert C. Martin. يجب أن يكون كل اختبار Fast (سريع — ملي ثانية)، Isolated (منعزل — لا يعتمد على اختبارات أخرى)، Repeatable (قابل للتكرار — نفس النتيجة على أي جهاز)، Self-validating (ذاتي التحقق — النتيجة "نجاح" أو "فشل"، بدون فحص يدوي)، و Timely (في الوقت المناسب — يُكتب قبل الكود أو بالتزامن معه). انتهاك أي مبدأ يقلل من قيمة الاختبار.
قالب قياسي لكتابة اختبارات الوحدة. Arrange — تحضير البيانات والتبعيات: إنشاء الكائنات، تهيئة الموكس، تعيين معاملات الإدخال. Act — تنفيذ الإجراء المختبر: استدعاء دالة أو طريقة. Assert — التحقق من النتيجة: مقارنة القيمة الفعلية بالقيمة المتوقعة. تقسيم الاختبار إلى ثلاث كتل يجعله قابلاً للقراءة والفهم. إذا كانت كتلة Assert تتطلب منطقاً معقداً، فمن المحتمل أن الاختبار يتحقق من الكثير في وقت واحد.
// مثال لاختبار وحدة بنمط AAA في Kotlin مع JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — إنشاء الكائن المراد اختباره
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — تنفيذ الإجراء
val result = calculator.add(2, 3)
// ASSERT — التحقق من النتيجة
Assertions.assertEquals(5, result)
}
}
يجب أن يصف اسم الاختبار ما يتم اختباره والنتيجة المتوقعة. التنسيق: [methodName]_[scenario]_[expectedResult]. مثال: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. الاسم الجيد للاختبار يحل محل التعليق ويشير فوراً عند الفشل إلى أي وظيفة تعطلت. تجنب أسماء مثل test1 أو checkSomething أو verify — فهي لا تحمل معلومات وتعقد التشخيص.
لعزل الوحدة المختبرة عن التبعيات الخارجية، يتم استخدام مضاعفات الاختبار (test doubles). الأنواع الرئيسية: الموكس (mocks) — تتحقق من أن طريقة معينة تم استدعاؤها بالمعاملات المتوقعة؛ الستب (stubs) — تُرجع قيماً محددة مسبقاً عند استدعاء طريقة؛ الفيك (fakes) — تطبيقات مبسطة لمكونات حقيقية (مثل InMemoryUserRepository بدلاً من UserRepository الذي يعمل مع قاعدة البيانات). يعتمد الاختيار على ما يجب التحقق منه: الحالة (stub) أو التفاعل (mock).
| المضاعف | ما يتحقق منه | مثال |
|---|---|---|
| موك | استدعاء الطريقة بالمعاملات الصحيحة | userRepository.save(user) تم استدعاؤه مرة واحدة بالضبط |
| ستب | القيمة المُعادة | repository.findById(1) يُرجع User(id=1, name="Test") |
| فيك | المنطق عبر تطبيق مبسط | InMemoryMapUserRepository مع HashMap بدلاً من قاعدة البيانات |
| سباي | Mocking جزئي لكائن حقيقي | spy(repo).when(findById).thenReturn(user) |
Mockito هو إطار الموكينغ الأكثر شعبية لجافا وكوتلن. يتيح إنشاء الموكس عبر mock()، وتهيئة قيم الإرجاع عبر when().thenReturn()، والتحقق من الاستدعاءات عبر verify(). تدعم الإصدارات الحديثة من Mockito (5.x) الموكس الثابت (mockStatic) والصيغة المبسطة عبر BDDMockito (given-willReturn). قاعدة مهمة: لا تموكس ما لا تملكه — لا تنشئ موكس لكائنات القيمة والمكتبات القياسية.
// مثال لاختبار وحدة مع Mockito في Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (التطوير الموجه بالاختبارات) هو منهجية يتم فيها كتابة الاختبار قبل تنفيذ الكود. دورة "أحمر-أخضر-إعادة هيكلة": كتابة اختبار يفشل (أحمر)، كتابة الحد الأدنى من الكود لاجتياز الاختبار (أخضر)، تحسين الكود دون تغيير السلوك (إعادة هيكلة). يضمن TDD أن كل الكود مغطى بالاختبارات (تغطية = 100% للوظائف المنفذة) وأن الكود قابل للاختبار — إذا كان الكود صعب الاختبار، فهذا يعني أن البنية تحتاج إلى تحسين.
وفقاً لبحث IBM (2006-2026، دراسة طولية)، الفرق التي تستخدم TDD لديها عيوب أقل بنسبة 40-80% في الإنتاج مقارنة بالفرق التي تكتب الاختبارات بعد الكود. كما يحسن TDD البنية: يُجبر المطور على التفكير في تصميم API قبل التنفيذ، مما يؤدي إلى اقتران ضعيف (loose coupling) وتماسك عالٍ (high cohesion). تأثير إضافي هو التوثيق بكود حي: تعمل الاختبارات كمواصفات لسلوك الوحدة، وهي محدثة دائماً.
TDD ليس مثالياً دائماً. مكونات واجهة المستخدم يصعب اختبارها بشكل منعزل — فاختبارات اللقطات (snapshot tests) أو اختبارات الانحدار البصري (Percy, Chromatic) أكثر فعالية. النمذجة الأولية والبحث (spike solutions) لا تتطلب اختبارات. الكود القديم (legacy) بدون اختبارات يصعب تغطيته عبر TDD — هنا نحتاج أولاً إلى اختبارات التوصيف (characterization tests) (اختبارات تلتقط السلوك الحالي قبل إعادة الهيكلة). في هذه الحالات، لا يتم التخلي عن TDD بالكامل بل يتم تكييفه — تُكتب اختبارات للوظائف المعدلة، وليس لكل الكود القديم.
التطوير المحمول له خصوصيته: غالباً ما يختلط منطق الأعمال بكود واجهة المستخدم (Activity, ViewController, ViewModel)، مما يعقد اختبار الوحدة. أفضل ممارسة هي عروض رقيقة، ViewModels سميكة: استخرج كل المنطق من مكونات واجهة المستخدم إلى أصناف منفصلة (UseCase, Repository, ViewModel) يسهل اختبارها دون محاكي. أندرويد و iOS لديهما أطر اختبار وحدة أصلية تعمل على JVM/Native دون تشغيل جهاز.
تعمل اختبارات وحدة أندرويد على JVM محلية دون محاكي، مما يوفر سرعة تنفيذ — يستغرق الاختبار النموذجي أقل من 100 مللي ثانية. JUnit 5 هو المشغل الرئيسي. لاختبارات ViewModel، استخدم kotlinx-coroutines-test لاختبار الكوروتينات و Turbine لاختبار StateFlow. يتيح Robolectric اختبار المكونات المعتمدة على أندرويد (Context, Resources) دون محاكي عن طريق تحميل shadow-classes. لاختبارات Compose، استخدم Compose UI Test — ولكن هذه اختبارات واجهة مستخدم، وليست اختبارات وحدة.
تُكتب اختبارات وحدة iOS بلغة Swift مع XCTest (مدمج في Xcode). Quick + Nimble هما إطارا BDD لاختبارات أكثر قابلية للقراءة (describe/context/it). للموكينغ، استخدم Cuckoo (توليد الموكس) أو SwiftyMocky. تدعم Swift البروتوكولات وحقن التبعية، مما يسهل استبدال التبعيات. نقطة رئيسية: تعمل اختبارات وحدة iOS على محاكي macOS، وليس على جهاز حقيقي. الاختبارات التي تتطلب وظائف أجهزة (كاميرا، Bluetooth) هي اختبارات تكامل.
تستخدم اختبارات وحدة Flutter حزمة flutter_test وتعمل على Dart VM دون محاكي. للموكينغ، استخدم حزمة mockito مع توليد الكود (build_runner). تختبر اختبارات الودجت (في نفس الحزمة) ودجتات فردية ولكنها تتطلب عرضاً وتعمل ببطء أكثر — استخدمها فقط للتحقق من منطق واجهة المستخدم. يتم اختبار منطق Dart الخالص (النماذج، المستودعات، blocs) كاختبارات Dart عادية دون استيراد flutter_test.
// مثال لاختبار وحدة في Flutter مع mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
تتطلب اختبارات الوحدة الفعالة انضباطاً. القاعدة الرئيسية: اختبر السلوك، وليس التنفيذ. لا يجب أن يعرف الاختبار كيف تم تنفيذ الوحدة داخلياً (أي الطرق الخاصة يتم استدعاؤها، وبأي ترتيب). إذا كان الاختبار مرتبطاً بالتنفيذ، فإنه ينكسر عند كل إعادة هيكلة ويفقد قيمته. يتحقق الاختبار من العقد: عند إدخال X، يجب أن يكون الإخراج Y. الاستثناء هو اختبارات الخوارزميات ذات الأداء الحرج، حيث يكون تسلسل الاستدعاءات مهماً.
تغطية 100% هدف غير قابل للتحقيق وغير ضروري. وفقاً لـ Google Testing Blog (2025)، المستوى الأمثل للتغطية لاختبارات الوحدة هو 70-80% من أسطر الكود. غالباً ما تتحقق تغطية 100% عن طريق اختبار getters و setters والمنشئات، وهو ما لا يضيف قيمة. ركز على منطق الأعمال الحرج: الحسابات المعقدة، التحقق، معالجة الأخطاء، الحالات الحدودية. استخدم JaCoCo (Java) و Coverage.py (Python) و Istanbul (JS) للقياس واضبط عتبة في CI — فشل البناء عند تغطية أقل من 60%.
اختبارات الوحدة هي المرحلة الأولى من أي خط أنابيب CI/CD. يتم تشغيلها عند كل push إلى المستودع، قبل البناء والنشر. يجب ألا يتجاوز متوسط وقت تشغيل مجموعة اختبارات الوحدة 5 دقائق — إذا زاد، تتوقف الاختبارات عن كونها "سريعة" ويتوقف المطورون عن تشغيلها محلياً. افصل الاختبارات إلى سريعة (وحدة) وبطيئة (تكامل) وشغّلها في مراحل مختلفة من خط الأنابيب. استخدم التنفيذ المتوازي و fail-fast لتسريع العملية.
الأسئلة الشائعة
يختبر اختبار الوحدة وحدة واحدة بشكل منعزل، مستبدلاً التبعيات الخارجية بالموكس. يختبر اختبار التكامل التفاعل بين عدة مكونات حقيقية (قاعدة بيانات، API، نظام ملفات). تُنفذ اختبارات الوحدة بالمللي ثانية، بينما تُنفذ اختبارات التكامل بالثواني. في هرم الاختبارات، تشكل اختبارات الوحدة 70%.
الاختيار يعتمد على المنصة: JUnit 5 لجافا/كوتلن، XCTest لـ iOS/Swift، pytest لبايثون، Jest/Vitest لجافاسكريبت/TypeScript، flutter_test لـ Flutter. للموكينغ، استخدم Mockito (جافا)، Cuckoo (iOS)، unittest.mock (بايثون) أو vitest.mock (JS). جميع الأطر الحديثة تدعم الاختبارات المعلمة والتحقق المدمج والتنفيذ المتوازي.
Fast — سريع، يُنفذ الاختبار بالمللي ثانية. Isolated — منعزل، لا يعتمد على اختبارات أو أنظمة خارجية. Repeatable — قابل للتكرار، يعطي نفس النتيجة على أي جهاز. Self-validating — يتحقق من النتيجة تلقائياً. Timely — في الوقت المناسب، يُكتب قبل الكود أو بالتزامن معه. انتهاك أي مبدأ يقلل من فعالية الاختبار.
نعم، بالتأكيد. يحتوي ViewModel على منطق الأعمال — معالجة الأحداث، تحويل البيانات، إدارة الحالة. على أندرويد، استخدم kotlinx-coroutines-test للكوروتينات و Turbine لاختبار StateFlow. على iOS، اختبر Combine Publishers أو async/await في ViewModel. اختبارات ViewModel هي اختبارات وحدة نقية تعمل على JVM/macOS دون محاكي.
طلبات الشبكة في اختبارات الوحدة لا تُنفذ — يتم استبدالها بموكس لعميل HTTP. على أندرويد، استخدم MockWebServer (OkHttp) — يشغل خادم HTTP محلي، وهو أفضل من الموكس لأنه يحاكي التفاعل الشبكي الحقيقي. يوفر MockWebServer عزلاً دون فقدان الواقعية. لـ iOS — استخدم OHHTTPStubs أو URLProtocol لاعتراض واستبدال الردود.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا