اجراء الاختبارات لـ Room و DAO و LiveData و Paging

Mohammad Laifمنذ سنة

بسم الله الرحمن الرحيم
السلام عليكم ورحمة الله وبركاته

في هذا الدرس سنتعلم كيفية القيام بعمل الاختبارات باستخدام الـ JUnit على كل من الـ Database و Dao و Paging لإختبار ان قاعدة البيانات لدينا خاليه من المشاكل قبل ان نكمل في انشاء بقية الـ Components للـ Android Architecture. يجب ان تكون ملماً بكتابة الاختبارات بعض الشئ حتى لايكون هذا الدرس غامضاً لديك (لدي مقالات تشرح ذلك مفصلاً ساقوم بوضع روابط لها او يمكنك الاطلاع عليها من خلال زيارة ملفي الشخصي - قسم المقالات).

 

ماذا سوف تقرئ في هذا الدرس؟

  • طريقة اختبار الـ Dao مع الـ LiveData.
  • طريقة اختبار الـ Dao مع الـ Paging و LiveData للـ Card.

 

ماذا سننشئ؟

  • كلاس اختبار للـ Dao.
  • اختبار الـ SubjectDao فقط LiveData.
  • اختبارات الـ CardDao الـ Paging.

 

كلاس اختبارات الـ Dao و الـ Database

ننشئ كلاس جديده باسم DatabaseDaoTest في مجلد الخاص بالاختبارات المخصصه للـ androidTest ونقوم بتعليمها بنوتيشن RunWith واختيار كلاس المشغل AndroidJUnit4 (انظر قسم ماهو المشغل في هذه المقاله: استخدام الـ Suites و Categories في الـ JUnit للتحكم في تشغيل الاختبارات) لها:

package com.mzdhr.flashcards;
import android.support.test.runner.AndroidJUnit4;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class DatabaseDaoTest {}

وهكذا اصبحت الكلاس مشغل لإختبارات من نوع الـ AndroidTest اي Instrumented test (راجع المصدر Test your app).

 

نقوم بأنشاء حقول لكل قاعدة البيانات والـ DAOs لدينا:

private AppDatabase mAppDatabase;
private SubjectDao mSubjectDao;
private CardDao mCardDao;

 

نقوم بكتابة دالة setUp تقوم بتجهيز كل من هذه الحقول وايضاً تجهيز لنا Context وهميه حتى نستطيع تشغيل الاختبارات عليها. ولاننسى تعليمها بالنوتيشن Before (للقرائة حول النوتيشنات الخاصة بالاختبارات تفضل بزيارة مقالتي: طريقة كتابة اختبارات الـ JUnit في لغة الجافا):

@Before
public void setUp() {
    Context context = InstrumentationRegistry.getTargetContext();
    mAppDatabase = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
    mSubjectDao = mAppDatabase.subjectDao();
    mCardDao = mAppDatabase.cardDao();
}

لاحظ طريقة عمل init لكل من قاعدة البيانات باستخدام الـBuilder الخاص بالـ Room. و كيفية عمل init لكل من الـ DAOs.

 

نقوم بكتابة دالة cleanUp تقوم بتنظيف مانريد بعد انتهاء اي من الاختبارات. ولاننسى تعليمها بالنوتيشن After (للقرائة حول النوتيشنات الخاصة بالاختبارات تفضل بزيارة مقالتي: طريقة كتابة اختبارات الـ JUnit في لغة الجافا):

@After
public void closeDatabase() {
    mAppDatabase.close();
}

لاحظ اننا نريد فقط اغلاق قاعدة البيانات, واذا كان هناك اي شئ اخر نريده ان يحذف او يرجع الى ماكان عليه نستطيع كتابته هنا ايضاً.

 

اختبار الـ SubjectDao فقط LiveData

نقوم بكتابة اول دالة اختبار لدينا وهي لتجربة دالة الـ insert هل تعمل بالشكل المطلوب ام لا؟

    @Test
    public void testInsertSubject() throws InterruptedException {
        // Arrange
        SubjectEntity subjectEntity = new SubjectEntity("Math", new Date(), 1);

        // Action
        mSubjectDao.insertSubject(subjectEntity);

        // Assertion
        final LiveData<List<SubjectEntity>> retrieveSubjects = mSubjectDao.getAllSubject();

        // Assert not empty
        // create a count down
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        // Setting an observer for our LiveData object
        retrieveSubjects.observeForever(new Observer<List<SubjectEntity>>() {
            @Override
            public void onChanged(@Nullable List<SubjectEntity> subjectEntities) {
                // Assert that our LiveData not empty
                assertThat("Assert subjectEntity not Empty", subjectEntities.size(), not(0));
                // Assert that our first object is inserted and exist in the database
                assertThat("Assert subjectEntity inserted", subjectEntities.get(0).getTitle(), is("Mathematics"));
                countDownLatch.countDown();     // start the count down
                retrieveSubjects.removeObserver(this);  // remove the observer
            }
        });
        // tell the count down to wait for 2 seconds
        countDownLatch.await(2, TimeUnit.SECONDS);
    }

تعقيب:

  • في الـ Arrange قمنا بانشاء عنصر جديد من SubjectEntity.
  • في الـ Action قمنا باستخدام SubjectDao والدالة insertSubject لحفظ هذا العنصر الجديد في قاعدة البيانات.
  • في الـ Assertion سوف نتحقق هل تم حفظ ذلك العنصر ام لا.

ملاحظة: لو اننا لم نستخدم LiveData لكان كتابة هذا القسم اسهل بكثير هكذا (وهذا يعتبر من اختبار الـ Dao):

List<SubjectEntity> retrieveSubjects = mSubjectDao.getAllSubjectNoLiveData();
assertThat("Assert subjectEntity not Empty", retrieveSubjects.size(), not(0));
assertThat("Assert subjectEntity inserted", retrieveSubjects.get(0).getTitle(), is("Mathematics"));
  • ولكن نحن نريد استخدام LiveData اليس كذلك!
  • فقبل كل شئ عندما تريد التعامل مع الـ LiveData يجب عليك ان توفر شيئين مهمين والاول هو Object يمتلك Lifecycle كـ Activity او Fragment واما الشئ الثاني فهو Observer مراقب وظيفته يراقب تغير البيانات بشكل تلقائي. ولكننا هنا استخدمنا دالة observeForever لان لايوجد لدينا Lifecycle Object. وسوف نستخدم عنصر من الـ CountDownLatch حتى يتأنى للـ LiveData ان تأخد وقتها لتحديث المراقب بالبيانات الجديدة (تستطيع عمل encapsulating لهذا الكود كما هو موجود في مثال جوجل بهذه الكلاس في كلاس خاصه, ثم تقوم باستخدامه كما هو مبين في هذا الكلاس). 
  • مايهم هو دوال الـ assertThat ومن خلالهم قمنا باختبار سعة المصفوفه وهل تم تخزين العنصر الاول ام لا وذلك بمقارنة عنوانه (سيفشل الاختبار لان العنوان خاطئ).
  • لقرائة المزيد حول طريقة تنظيم كتابة الاختبارات كـ Arrange و Action و Assert وكتابة التعاليق اطلع على مقالتي: كتابة اختبارات الـ JUnit بشكل مستحسن.

 

نقوم بكتابة دالة اختبار لدينا وهي لتجربة دالة الـ update هل تعمل بالشكل المطلوب ام لا؟

    @Test
    public void testUpdateSubject() throws InterruptedException {
        // Arrange
        final LiveData<List<SubjectEntity>> retrieveSubjects = mSubjectDao.getAllSubject();
        final SubjectEntity subjectEntity = new SubjectEntity("Math", new Date(), 1);

        // Action
        mSubjectDao.insertSubject(subjectEntity);   // insert a subject
        // update subject (You need to use the id when you want to updated also use the room constructor)
        // We are creating a new subject with the same id of the old, Room gonna figure it out and updated.
        mSubjectDao.updateSubject(new SubjectEntity(1, "Mathematics", subjectEntity.getDate(), subjectEntity.getColor()));

        // Assertion
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        retrieveSubjects.observeForever(new Observer<List<SubjectEntity>>() {
            @Override
            public void onChanged(@Nullable List<SubjectEntity> subjectEntities) {
                assertThat(subjectEntities.get(0).getTitle(), is("Mathematics"));
                countDownLatch.countDown();
                retrieveSubjects.removeObserver(this);
            }
        });
        countDownLatch.await(2, TimeUnit.SECONDS);

    }

تعقيب:

  • في الـ Arrange قمنا بانشاء مصفوفة عناصر SubjectEntity مغلفه بـ LiveData وعنصر واحد SubjectEntity.
  • في الـ Action قمنا باستخدام الدالة insertSubject لحفظ ذلك العنصر.
  • ثم قمنا بعمل له تحديث باستخدام الدالة updateSubject وهنا يجب علينا استخدام Constructor الـ Room (ليس الـ Constructor الخاص بنا) ونقوم بأنشاء عنصر جديد باستخدامه, ويكون به رقم الـ id من جدول قاعدة البيانات لذلك العنصر المراد تحديثه وايضاً القيم المتغيره ونقوم باستخدام القيم التي لم يجري عليها اي تعديل من العنصر السابق.

من اين احصل على رقم الـ id؟

  • في مثالنا هذا قمنا باستخدام الرقم ١ لان لايوجد الا هذا العنصر بقاعدة البيانات (هل تذكر autoGenerate للـ id في كلاس الـ SubjectEntity). ولكن تستطيع الغاء ميزة الـ autoGenerate واستبدالة بشئ من عندك حتى تعرف ارقام عناصرك بقاعدة البيانات. وايضاً تستطيع معرفة الرقم الخاص بالعنصر بعد حفظة (مثال استخدام getId بداخل الـ onChange على العنصر). وتستطيع ايضاً جعل دالة الـ insert في كلاس الـ SubjectEntity تقوم بارجاع رقم id بعد حفظ العنصر بقاعدة البيانات و -1 عند الفشل. واخيراً في مقدورك عمل دالة Query تقوم بالبحث عن عنصر ما وترجع رقم الـ id الخاص به.


اختبارات الـ CardDao الـ Paging

نقوم بكتابة دالة اختبار لدينا وهي لتجربة دالة الـ insert هل تعمل بالشكل المطلوب ام لا؟ وهل تم حفظ الـ Card في الام Subject ام لا؟

    @Test
    public void testCardAndSubject() throws InterruptedException{
        // Arrange
        final SubjectEntity subjectEntity = new SubjectEntity("Grammar", new Date(), 1);
        final CardEntity card1 = new CardEntity("Book", "Box with papers!", 1, new Date());
        final CardEntity card2 = new CardEntity("Paper", "Paper with lines!", 1, new Date());
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        final LiveData<PagedList<CardEntity>> mCardsByParentId = new LivePagedListBuilder<>(mCardDao.getCardsByParentId(1), 20).build(); // 20 ---> means 20 item per page!

        // Action
        mSubjectDao.insertSubject(subjectEntity);
        mCardDao.insertCard(card1);
        mCardDao.insertCard(card2);

        // Assert
        mCardsByParentId.observeForever(new Observer<PagedList<CardEntity>>() {
            @Override
            public void onChanged(@Nullable PagedList<CardEntity> cardEntities) {
                assertThat(cardEntities.get(0).getFrontSide(), is("Book"));
                assertThat(cardEntities.get(0).getBackSide(), is("Box with papers!"));
                assertThat(cardEntities.get(0).getParentId(), is(1));

                assertThat(cardEntities.get(1).getFrontSide(), is("Paper"));
                assertThat(cardEntities.get(1).getBackSide(), is("Paper with lines!"));
                assertThat(cardEntities.get(1).getParentId(), is(2));
                countDownLatch.countDown();
                mCardsByParentId.removeObserver(this);
            }
        });
        countDownLatch.await(2, TimeUnit.SECONDS);
    }

تعقيب:

  • في الـ Arrange: قمنا بتجهيز عناصرنا التي سوف نختبرها وهي عنصر Subject و عنصرين Card وايضاً وضعنا عنصر CountDownLatch حتى نعطي الـ LiveData Observer قليلاً من الوقت. لاحظ طريقة انشاء العنصر mCardsByParentId للـ Paging.
  • في الـ Action: قمنا بحفظ عنصر الـ Subject وعنصرين الـ Card.
  • في الـ Assert: قمنا بالتأكد من العنصر الاول للـ Card وهل هو في الـ Subject الاولى ام لا. كذلك للعنصر الثاني للـ Card (لاحظ انه سيفشل الاختبار لان العنصر الثاني غير موجود في الـ Subject الثانيه, فأساساً هي غير موجوده).

 

وهكذا قمنا باختبار كل من Room Database وSubjectDao و LiveData. وفي الدروس القادمة سوف نقوم باختبار ماتبقى من مكونات ان شاء الله.

 

نهاية الدرس

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

 

روابط ذات صلة:

 

التعليقات (2)

Ali Mohamed Radwan:

الجزء بتاع الاختبار دا صعب جدا .. هل لابد من كتابة الكود الخاص به ولا التكمله ف room وبعد الانتهاء من فهمها العوده لها مره اخري.

Mohammad Laif:

لاداعي لإجراء الاختبارات الان. فقط اذا اردت ان يكون المنطق سليم في برمجيتك قم بإجراء الاختبارات بشكل عام.

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !