اختبارات كود الواجهات (مقدمة)

أهلا أصدقائي!

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

مفهوم الاختبار Testing :card_index:

الاختبار عبارة عن منهجية مطبقة من أجل ** التحقق مما إذا كانت الشفرة المكتوبة تعطي المخرجات المرغوبة **.

لا بد من اختبار مكوناتك للأسباب التالية:

  1. تقليل الانحدارات
  2. ضمان سلامة الكود وقابلية التوسع والجودة
  3. مراقبة الأداء
  4. احصل على إعداد تطوير آمن

اختبار الوحدة Unit Testing :triangular_ruler:

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

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


ماذا تختبر؟

يقوم العديد من زملائي بالفعل باختبار مدخلات المُكَوِّن الكودي (component inputs). هذا في الواقع ليس ما هو مفهوم الاختبار هنا ، لذلك ، نحتاج في الواقع إلى اختبار مخرجات المكون الكودي (component outputs) بدلاً من ذلك.
سنستخدم “@vue/test-utils” مع إطار عمل اختبار “jest”.


اختبار مخرجات المكوّن الكودي (Component output)

لتنظيم هذا قليلاً ، إليك الأشياء التي نحتاجها بالفعل
اختبار في مكون Vue:

  1. القالب المطبوع بالصفحة Rendered Template
  2. الأحداث المنبعثة Emitted Events
  3. الآثار الجانبية (إجراءات VueX ، vue-router ، استدعاء الوظائف المستوردة ، الطرق ، mixins ، … إلخ)

سأعرض الآن الطريقة التقليدية :x: (غير صحيح) :x: التي يقوم بها معظم المطورون لهيكلة اختباراتهم:

describe('methods', () => {
  /* Testing every method in isolation */
})

describe('computed', () => {
  /* Testing every computed property in isolation */
})

describe('template', () => {
  /* Testing what is rendered. With the snapshot */
})

كما هو موضح أعلاه ، تبدو الاختبارات منظمة. ومع ذلك ، فإنه يتبع سياق اختبار :x: المدخلات :x: بدلاً من المخرجات

دعنا نلقي نظرة على هذا النموذج البسيط:

<template>
  <main>
    <div v-if="loading">
      Loading ...
    </div>
    <template v-else>
      <p v-if="error">
        Something went wrong!
      </p>
      <div v-else>
        <!-- some data -->
      </div>
    </template>
  </main>
</template>

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

لذا فإن المكون إما يحصل على البيانات ، أو يقوم بتحميل خطأ ، أليس كذلك؟
الآن دعونا نلقي نظرة على هيكل الاختبار هذا:

describe('when loading', () => {
  it.todo(`renders 'Loading...' text`)

  it.todo(`does not render the error message`)

  it.todo(`does not render data`)
})

describe('when there is an error', () => {
  it.todo(`does not render 'Loading...' text`)

  it.todo(`renders error message`)

  it.todo(`does not render data`)
})

لذلك ، في المثال أعلاه ، قمنا بتقسيم مواصفات الاختبار إلى مجموعتين رئيسيتين حيث أن لدينا مرحلتان رئيسيتان يجب اختبارهما:

  1. ضمن التحميل
  2. عندما يكون هناك خطأ

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

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


ابدأ بمصنع المكونات

مصنع المكونات هو ببساطة طريقة تنشئ مكون Vue (حوامل ضحلة أو Shallow mounts)

import { shallowMount } from '@vue/test-utils';

describe('My component test', () => {
  let wrapper;
  
  // Component Factory
  function createComponent() {
    wrapper = shallowMount(MyComponent, {/* optional params */})
  }

  // Destroy wrapper
  afterEach(() => {
    wrapper.destroy()
  })
})

يوضح المقتطف السابق أننا أنشأنا متغيرًا وقمنا بشكل اختياري بتعيين دالة/وظيفة createComponent () ، ولكن لماذا؟

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


استخدم الدوال المساعدة لتسهيل العثور على العناصر والمحتويات في المكون

بالنسبة للمكونات المعقدة للغاية ، قد نستخدم دوال اضافية لمساعدتنا في العثور على العناصر والمكونات بسهولة.
دعنا نلقي نظرة على هذا المقتطف:

import { shallowMount } from '@vue/test-utils';

describe('My component test', () => {
  let wrapper;

  const findConfirmBtn = wrapper.find('[data-testid="confirm-btn"]')
  const findModalComp = wrapper.findComponent(MyModalComponent)
  
  // Component Factory
  function createComponent() {
    wrapper = shallowMount(MyComponent, {/* optional params */})
  }


  // Destroy wrapper
  afterEach(() => {
    wrapper.destroy()
  })

  it('renders a modal', () => {
    createComponent();
    expect(findModalComp.exists()).toBe(true)
  })
})

لذلك ، كما رأينا هناك ، قمنا بإنشاء مثل لوحة لتحديد موقع العناصر المختلفة واستفدنا بالفعل من وظيفة createComponent () وهي من الطرق المميزة لكتابة كود نظيف!

ملاحظة => استخدام محددات البيانات مثل [data-testid="something"] هو أفضل ممارسة، لأنه مع مرور الوقت، قد يتغير العنصر نفسه، او الـclasses الخاصة به

إن [data-testid ="something"] مهم لأننا نطبق المعالجات من وقت لآخر وقد نغير إما اسم المكون أو الفئات المرفقة بهذا المكون. سيضمن ذلك عدم تأثر مواصفات الاختبار ونحن على ما يرام.

من الضروري جدا تجنب اختبار مدخلات المكون، أو طريقة الدالة نفسها

إنها ممارسة سيئة حقًا لاختبار المكونات الداخلية للمكون. دعني اريك مثالا:

export default {
  data() {
    return {
      count: 0
    }
  }
  computed: {
    double() {
      return this.count * 2
    }
  }
  methods: {
    incrementCount() {
      this.count++
    }
  }
}

الطريقة العادية التي تتبادر إلى الذهن لاختبار ذلك ستكون شيئًا كالتالي:

it('Calculates double correctly', () => {
  createComponent({ data: { count: 1 } })
  expect(wrapper.vm.double).toBe(2)
})


it('Calls correct method on btn click', () => {
  createComponent()
  jest.spyOn(wrapper.vm, 'incrementCount').mockImplementation(() => {})

  findIncrementBtn().trigger('click')
  expect(wrapper.vm.incrementCount).toHaveBeenCalled()
  expect(wrapper.vm.count).toBe(1)
})

هذا في الواقع نهج خاطئ: :x: لأنه يختبر ما إذا كان يتم استدعاء الدالة عند النقر فوق btn. ** بهذه الطريقة ، نعيد اختبار إطار عمل Vue ، وبالتالي ، هذا بعيد كل البعد عن اختبار المنطق الخاص بنا**.

إذا كنت تقول: “يجب أن نختبر الخاصيات المحسوبة على حدة :dizzy_face:” ، فأنت تحاول عكس الهندسة التي تم إجراؤها في إنشاء مكون SFC. ليس من العملي عزل خاصية محسوبة من SFC لأن هذا سيؤثر على عرض القالب!

في هذه الحالة يمكننا أن نقول أن أفضل طريقة للتحقق من الخاصيات المحسوبة هي ** من خلال عرض القالب **
:heavy_check_mark:. سأريك الكيفية في لحظات.

لذا ، دعنا نتخيل أن نموذجنا يبدو كالتالي:

<template>
  <div>
    <span data-testid="count">Count is: {{ count }}</div>
      <button data-testid="increment-button" @click="incrementCount">
        Inctrement
      </button>
      <p data-testid="double">Count x2: {{ double }}</p>
  </div>
</template>

لذلك ، بدلاً من اختبار دعائم API الخاص بـVue. يمكننا اختبار النتائج / المخرجات المعروضة في القالب نفسه
:heavy_check_mark: مثل:

const findDouble = wrapper.find('[data-testid="double"]')

it('Calculates double correctly', () => {
  createComponent({ data: { count: 1 } })
  // expect(wrapper.vm.double).toBe(2) //This was the wrong approach
  expect(findDouble().text()).toBe(`Count x2: 2`) // This is the best practice
})

// for an extended version, jest supports this format
it.each`
  a     |  expected
  ${0}  |  ${0}
  ${1}  |  ${2}
  ${10}  |  ${20}
  ${100}  |  ${200}
`('renders double count as $expected when count is $a',
  ({ a, expected } => {
    createComponent({ data: { count: a } })

    expect(findDouble().text()).toBe(`Count x2: ${expected}`)
  })
 )

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

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

باستخدام نفس الأسلوب يمكننا اختبار بقية “البيانات” و “الدوال” بنفس الطريقة كما يلي:

const findCount = () => wrapper.find('[data-testid="count"]')
const findIncrementBtn = () => wrapper.find('[data-testid="increment-btn"]')

it('Calls correct method on btn click', async () => {
  createComponent()
  expect(findCount().text()).toBe('Count: 0')

  findIncrementBtn().trigger('click')
  await nextTick()
  expect(findCount().text()).toBe('Count: 1')
})

بهذه الطريقة :heavy_check_mark: نحن نختبر المخرجات المعروضة في القالب.


:thumbsup: قواعد الإبهام :thumbsup:

  1. نسيان التأكيد على wrapper.vm
  2. لا تتجسس على الدوال، (ولا تجسسوا :smile:)
  3. إذا قمنا بإعادة تسمية الدالة أو حساباتها، يجب أن تجتاز الاختبار لأننا نهتم بالمخرجات فقط وليس بإسم أو عرق أو شكل أو لون الدالة :smile:

لماذا لا يجب اختبار المدخلات للمكون :question:

الحيلة هنا هي أنه عندما تختبر دالة ما بشكل منفصل، فإنها تجتاز الاختبار، ولكن إذا أشار مطور ما إليها بشكل خاطئ في القالب ، فستستمر في اجتياز الاختبار بدون مشكلة وهذا ليس ما نستهدفه ، حيث سيظل المكون المخصص خاطئًا و نحن نختبر Vue نفسه :smile:

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


اتبع المستخدم دائمًا

العودة إلى مثالنا

it('Calculates double correctly', () => {
  createComponent({ data: { count: 1 } })
  expect(findDouble().text()).toBe(`Count x2: 2`)

  //  now if the user increases the count
  wrapper.setData({ count: 2})
  expect(findDouble().text()).toBe(`Count x2: 4`)
})

يبدو هذا الاختبار جيدًا ، لكنه لا يزال خاطئًا :x: كما يجب أن نختبر تفاعل المستخدم نفسه

it('Calculates double correctly', async() => {
  createComponent({ data: { count: 1 } })
  expect(findDouble().text()).toBe(`Count x2: 2`)

  //  now if the user increases the count
  findIncrementBtn().trigger('click')
  await nextTick()
  expect(findDouble().text()).toBe(`Count x2: 4`)
})

بهذه الطريقة ، نتحقق عندما ينقر المستخدم على زر، يجب أن يعكس تغيير القيمة في النموذج ، وبهذه الطريقة ، يلامس اختبارنا منطق العمل الذي نحتاجه بالفعل للتحقق :heavy_check_mark:


افكار اخيرة

المكونات الفرعية هي الصناديق السوداء

يجب أن نستخدم shallowMount بدلاً من التركيب mount لأننا نحتاج إلى التركيز على المكون الذي نختبره. تساعدنا دالة shallowMount() في تحويل كل المكونات الفرعية لصناديق سوداء (أي لا تدخل معنا في الاختبار)

لا تنس مهام Vue الدقيقة

تأكد من استخدام المهام الدقيقة مثل nextTick()، وإلا فإن توقع الاختبار سيفشل.


1 Like