اختبار وحدة في Laravel

من بين المناقشات في المجتمع ، كثيراً ما أسمع رأيًا مفاده أن اختبار الوحدة في Laravel خاطئ ومعقد ، وأن الاختبارات بحد ذاتها طويلة ولا تقدم أي فائدة. ولهذا السبب ، فإن القليل منهم يكتبون هذه الاختبارات ، ويقصرون أنفسهم فقط على اختبارات الميزات ، ويميل استخدام اختبارات الوحدة إلى 0.
لقد فكرت بذلك مرة واحدة ، لكن بمجرد التفكير في الأمر وسألت نفسي - ربما لا أعرف كيف أطبخها؟


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


قليلا من الفلسفة والقيود


Laravel هو نوع من الإطار في بعض الأماكن. خاصة فيما يتعلق بالواجهات والبليغة. لن أتطرق إلى المناقشات أو الإدانات لهذه النقاط ، لكنني سأظهر كيف أجمعها مع اختبارات الوحدة.
أكتب الاختبارات بعد (أو في نفس الوقت) كتابة الكود الرئيسي. ربما لن يكون مقاربي متوافقًا مع نهج TDD أو يتطلب تعديلات جزئية.


السؤال الأهم الذي أطرحه على نفسي قبل كتابة الاختبار هو "ماذا أريد بالضبط اختباره؟". هذه قضية مهمة. كانت هذه الفكرة هي التي سمحت لي بإعادة النظر في آرائي حول اختبارات وحدة الكتابة ورمز المشروع نفسه.


يجب أن تكون الاختبارات مستقرة وتعتمد بأقل قدر ممكن على البيئة. إذا فشلت اختباراتك عند إجراء الطفرات ، فمن المحتمل أنها جيدة. بالمقابل ، إذا لم يسقطوا ، فقد لا يكونون جيدًا.


من خارج الصندوق ، يدعم Laravel 3 أنواع من الاختبارات:


  • المتصفح
  • ميزة
  • وحدة

سأتحدث بشكل رئيسي عن اختبارات الوحدة.


أنا لا أختبر كل الكود من خلال اختبارات الوحدة (ربما هذا غير صحيح). أنا لا أختبر بعض الكود على الإطلاق (المزيد حول هذا أدناه).


إذا تم استخدام المسند في الاختبارات ، فلا تنس القيام بـ Mockery :: close () على tearDown.


بعض الأمثلة للاختبارات "مأخوذة من الإنترنت".


كيف يمكنني اختبار


أدناه سوف أقوم بتجميع أمثلة الاختبار حسب مجموعة الفصل وحاول تقديم أمثلة اختبار لكل مجموعة فصل. بالنسبة لمعظم مجموعات الفصل ، لن أقدم أمثلة على الكود نفسه.


الوسيطة


بالنسبة لاختبار وحدة البرامج الوسيطة ، أقوم بإنشاء كائن من فئة الطلب ، كائن من البرامج الوسيطة المطلوبة ، ثم أدعو طريقة المقبض وتنفيذ التأكيدات اللازمة. يمكن تقسيم الوسيطة وفقًا للإجراءات التي يتم تنفيذها إلى 3 مجموعات:


  • تغيير كائن الطلب (تغيير طلب النص ، أو الجلسات)
  • إعادة التوجيه (تغيير حالة الاستجابة)
  • لا تفعل شيئا مع كائن الطلب
    دعنا نحاول إعطاء مثال على اختبار لكل مجموعة:

لنفترض أن لدينا الوسيطة التالية ، التي تتمثل مهمتها في تعديل حقل العنوان:


class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } } 

قد يبدو اختبار البرنامج الوسيط المماثل كما يلي:


 public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); } 

ستكون الاختبارات للمجموعتين 2 و 3 من هذه الخطة ، على التوالي:


 $response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302); //   $this->assertEquals($response, null); //      request 

طلب فئة


المهمة الرئيسية لهذه المجموعة هي ترخيص الطلبات والتحقق من صحتها.


أنا لا أختبر هذه الفئات من خلال اختبارات الوحدة (أعترف أن هذا قد لا يكون صحيحًا) ، فقط من خلال اختبارات الميزات. في رأيي ، تعتبر اختبارات الوحدة زائدة عن الحاجة في هذه الفئات ، لكنني وجدت بعض الأمثلة المثيرة للاهتمام حول كيفية القيام بذلك. ربما سيقومون بمساعدتك إذا قررت اختبار فئة وحدة الطلب الخاصة بك مع الاختبارات:



مراقب


أنا أيضا لا اختبار وحدات التحكم من خلال اختبارات وحدة. لكن عند اختبارها ، استخدم ميزة واحدة أود التحدث عنها.


أجهزة التحكم ، في رأيي ، يجب أن تكون خفيفة. وتتمثل مهمتها في الحصول على الطلب الصحيح ، واستدعاء الخدمات والمستودعات الضرورية (بما أن كلا المصطلحين "أجنبيين" إلى Laravel ، سأشرح المصطلحات أدناه) ، وأرجع الإجابة. يؤدي في بعض الأحيان إلى حدث ، وظيفة ، إلخ.
وفقًا لذلك ، عند الاختبار من خلال اختبارات الميزات ، لا نحتاج فقط إلى الاتصال بوحدة التحكم بالمعلمات الضرورية والتحقق من الإجابة ، ولكن أيضًا قفل الخدمات الضرورية والتحقق من أنها تسمى بالفعل (أو لا يتم استدعاؤها). في بعض الأحيان - إنشاء سجل في قاعدة البيانات.


مثال على اختبار وحدة التحكم باستخدام فئة خدمة وهمية:


 public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); } 

مثال على اختبار وحدة تحكم مع واجهة وهمية (في حالتنا ، حدث ، ولكن عن طريق القياس يتم القيام به لواجهات Laravel أخرى):


 public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [ // our changed data ] ], ]); $data = json_decode($response->getContent()); $response->assertStatus(200); $this->assertEquals($user->id, $data->data->userId); // and assert other data from response Event::assertDispatched(CartChanged::class); } 

الخدمة والمستودعات


هذه الأنواع من الفصول خارج الصندوق. أحاول الحفاظ على وحدات التحكم رقيقة ، لذلك وضعت كل العمل الإضافي في واحدة من هذه المجموعات الصفية.


لقد حددت الفرق بينهما على النحو التالي:


  • إذا كنت بحاجة إلى تنفيذ بعض منطق الأعمال ، فضع هذا في طبقة الخدمة المناسبة (الفئة).
  • في جميع الحالات الأخرى ، وضعت هذا في مجموعة مستودعات التخزين. كقاعدة عامة ، يذهب الوظيفية مع Eloquent إلى هناك. أنا أفهم أن هذا ليس هو التعريف الصحيح لمستوى المخزون تمامًا. سمعت أيضًا أن البعض يتحمل كل شيء مرتبط بـ Eloquent في النموذج. في رأيي هو نوع من التسوية ، في رأيي ، على الرغم من أن "الأكاديمي" ليس صحيحًا بالكامل.

بالنسبة لفئات المستودع ، لا أكتب اختبارات تقريبًا.


مثال على اختبار فئة الخدمة أدناه:


 public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user); // set data $data = [ ]; $newCart = $cartService->updateForUser($user, $data); $this->assertEquals($data, $newCart->toArray()); Event::assertDispatched(CartChanged::class, 1); } 

الحدث المستمع ، وظائف


يتم اختبار هذه الفئات وفقًا للمبدأ العام تقريبًا - نقوم بإعداد البيانات اللازمة للاختبار ؛ نحن نسمي الطبقة المطلوبة من الإطار ونتحقق من النتيجة.
مثال للمستمع:


 public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id, // other needed data ]); $listener = new CreateTaskForSyncCart(); $listener->handle(new CartChanged($cart)); $job = // get our job $this->assertSame(json_encode($cart->products), $job->payload); $this->assertSame($user->id, $job->user_id); // some additional asserts. Work with this data simplest for example $this->assertTrue($updatedAt->equalTo($job->last_updated_at)); } 

وحدة التحكم


أنا أعتبر أوامر وحدة التحكم كنوع من أنواع أجهزة التحكم التي يمكنها بالإضافة إلى ذلك (وتنفيذ عمليات معالجة أكثر تعقيدًا مع إدخال مدخلات وحدة التحكم الموضحة في الوثائق). وفقًا لذلك ، يتم الحصول على الاختبارات على غرار وحدة التحكم: نحن نتحقق من أن أساليب الخدمة الضرورية تسمى ، يتم تشغيل الأحداث (أو لا) ، ونقوم أيضًا بالتحقق من التفاعل مع وحدة التحكم (الإخراج أو طلب البيانات).


مثال على اختبار مماثل:


 public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); } 

منفصلة المكتبات الخارجية


كقاعدة عامة ، إذا كانت المكتبات المنفصلة تحتوي على ميزات لاختبارات الوحدات ، فسيتم وصفها في الوثائق. في حالات أخرى ، يتم اختبار العمل باستخدام هذا الرمز بشكل مشابه لطبقة الخدمة. ليس من المنطقي تغطية المكتبات نفسها باختبارات (فقط إذا كنت ترغب في إرسال العلاقات العامة إلى هذه المكتبة) وعليك اعتبارها بمثابة نوع من الصندوق الأسود.


في العديد من المشاريع ، يجب أن أتفاعل من خلال API مع خدمات أخرى. غالبًا ما تستخدم Laravel مكتبة Guzzle لهذه الأغراض. بدا لي مناسبًا لوضع كل العمل مع الخدمات الأخرى في فئة منفصلة من خدمة NetworkService. جعل ذلك الأمر أسهل بالنسبة لي في كتابة واختبار الكود الرئيسي ، وساعد على توحيد الإجابات ومعالجة الأخطاء.


أعطي أمثلة على العديد من الاختبارات لفئة NetworkService الخاصة بي:


 public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); } 

النتائج


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


مع إعادة المعالجة النشطة عند توسيع أو تغيير الوظيفة ، نرى على الفور ما يحدث بالضبط ويمكننا تصحيح الأخطاء بسرعة وبدقة دون إطلاقها من البيئة المحلية. هذا يجعل تصحيح الخطأ رخيصة قدر الإمكان.


آمل أن تساعدك المبادئ والمناهج التي وصفتها في التعامل مع اختبار الوحدة في Laravel وإجراء اختبارات الوحدة على مساعدينك في تطوير التعليمات البرمجية.


اكتب الإضافات والتعليقات.

Source: https://habr.com/ru/post/ar457866/


All Articles