full_stack_101 هيكل تطبيقات الواجهة الخلفية: مدخل إلى الاختبارات (unit tests)


ابراهيم الخضور

سنعود للعمل على نسخة الواجهة الخلفية لتطبيق الملاحظات الذي بدأناه في القسم 3 من سلسلة full_stack_101.

الهيكل العام للمشروع

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

├── index.js
├── app.js
├── build
│   └── ...
├── controllers
│   └── notes.js
├── models
│   └── note.js
├── package-lock.json
├── package.json
├── utils
│   ├── config.js
│   ├── logger.js
│   └── middleware.js  

استخدمنا حتى هذه اللحظة الأمرين console.log وconsole.error في طباعة بيانات الشيفرة التي تهمنا على الطرفية. لكن من الأفضل فصل الشيفرات التي تتعلق بأمور الطباعة في وحدة مستقلة خاصة بها سنسميها logger.js، وسنضعها في المجلد utils:

const info = (...params) => {
  console.log(...params)
}

const error = (...params) => {
  console.error(...params)
}

module.exports = {
  info, error
}

كما نلاحظ، تحتوي الوحدة على دالتين الأولى info تتولى أمر طباعة الرسائل العادية، والأخرى error تتولى طباعة رسائل الخطأ.

لوضع شيفرة الطباعة في وحدة منفصلة مزايا عدة، فلو أردنا مثلًا طباعة السجلات أو الرسائل إلى ملف أو إلى خدمة خارجية لإدارة السجلات مثل graylog أو papertrail، لن يكون علينا سوى تعديل الشيفرة في ملف واحد.

سيصبح ملف تشغيل التطبيق index.js على الشكل التالي:

const app = require('./app') // the actual Express application
const http = require('http')
const config = require('./utils/config')
const logger = require('./utils/logger')

const server = http.createServer(app)

server.listen(config.PORT, () => {
  logger.info(`Server running on port ${config.PORT}`)
})

تنحصر وظيفة الملف index.js الآن، بإدراج التطبيق الفعلي من الملف app.js وتشغيله. ستقوم الدالة info بطباعة عبارة على الطرفية تدل على أن التطبيق يعمل.

كذلك سننقل الشيفرة التي تتعامل مع متغيرات البيئة إلى وحدة مستقلة اسمها config.js ونضعها في المجلد utils:

require('dotenv').config()

const PORT = process.env.PORT
const MONGODB_URI = process.env.MONGODB_URI

module.exports = {
  MONGODB_URI,
  PORT
}

يمكن لأي جزء من التطبيق الوصول إلى متغيرات البيئة بإدراج الوحدة السابقة:

const config = require('./utils/config')

logger.info(`Server running on port ${config.PORT}`)

نقلنا كذلك الشيفرة التي تتعامل مع المسارات إلى وحدة خاصة. وطالما أن مصطلح "متحكمات" يستخدم للإشارة إلى هذه المسارات، سننشئ مجلدًا باسم controller وسنضع فيه الوحدة notes.js التي ستحتوي شيفرة المسارات التي تتعلق بالملاحظات. سيكون محتوى هذه الوحدة كالتالي:

const notesRouter = require('express').Router()
const Note = require('../models/note')

notesRouter.get('/', (request, response) => {
  Note.find({}).then(notes => {
    response.json(notes)
  })
})

notesRouter.get('/:id', (request, response, next) => {
  Note.findById(request.params.id)
    .then(note => {
      if (note) {
        response.json(note)
      } else {
        response.status(404).end()
      }
    })
    .catch(error => next(error))
})

notesRouter.post('/', (request, response, next) => {
  const body = request.body

  const note = new Note({
    content: body.content,
    important: body.important || false,
    date: new Date()
  })

  note.save()
    .then(savedNote => {
      response.json(savedNote)
    })
    .catch(error => next(error))
})

notesRouter.delete('/:id', (request, response, next) => {
  Note.findByIdAndRemove(request.params.id)
    .then(() => {
      response.status(204).end()
    })
    .catch(error => next(error))
})

notesRouter.put('/:id', (request, response, next) => {
  const body = request.body

  const note = {
    content: body.content,
    important: body.important,
  }

  Note.findByIdAndUpdate(request.params.id, note, { new: true })
    .then(updatedNote => {
      response.json(updatedNote)
    })
    .catch(error => next(error))
})

module.exports = notesRouter

هذه الشيفرة هي محتوى الملف index.js القديم إجمالًا ما عدا بعض التغييرات المهمة. حيث أنشأنا كائن متحكم بالمسار في بداية الملف:

const notesRouter = require('express').Router()

//...

module.exports = notesRouter

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

ويجب الانتباه إلى الطريقة التي اختصرنا فيها عنوان المسار. فلقد عرفنا في النسخة السابقة مسارًا على الشكل:

app.delete('/api/notes/:id', (request, response) => {

بينما أصبح التعريف في النسخة الجديدة كالتالي:

notesRouter.delete('/:id', (request, response) => {

ما هو إذًا بالتحديد كائن التحكم بالمسار؟ يزودنا توثيق المكتبة express بالتعريف التالي:

اقتباس

يعرّف كائن التحكم بالمسار على أنه حالة برمجية تضم أداة وسطية ومسارات. يمكن اعتباره "تطبيقًا مصغرًا" قادر على القيام بوظائف الأداة الوسطية ووظائف المسارات. يضم كل تطبيق Express متحكم مسار مدمج معه.

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

يستخدم الملف app.js -الذي ينشئ التطبيق الوظيفي المطلوب- هذا المتحكم بالطريقة التالية:

const notesRouter = require('./controllers/notes')
app.use('/api/notes', notesRouter)

يُستخدم المتحكم بالمسار الذي عرفناه في الشيفرة السابقة، إذا كان الطلب موجهًا إلى الموقع الذي يبدأ عنوانه بالصيغة "/api/notes". ولهذا على المتحكم notesRouter أن يعرّف المسارت بعناوين نسبية فقط على شكل مسار فارغ "/" أو أن يذكر المعامل فقط "id:/".

سيبدو الملف app.js بعد إجراء التعديلات على النحو التالي:

const config = require('./utils/config')
const express = require('express')
const app = express()
const cors = require('cors')
const notesRouter = require('./controllers/notes')
const middleware = require('./utils/middleware')
const logger = require('./utils/logger')
const mongoose = require('mongoose')

logger.info('connecting to', config.MONGODB_URI)

mongoose.connect(config.MONGODB_URI,
 { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    logger.info('connected to MongoDB')
  })
  .catch((error) => {
    logger.error('error connection to MongoDB:', error.message)
  })

app.use(cors())
app.use(express.static('build'))
app.use(express.json())
app.use(middleware.requestLogger)

app.use('/api/notes', notesRouter)

app.use(middleware.unknownEndpoint)
app.use(middleware.errorHandler)

module.exports = app

ستجد أن التطبيق قد استخدم العديد من الأدوات الوسطية ومنها الأداة notesRouter التي ارتبطت بالمسار "api/notes/". نقلنا أيضًا الأداة الوسطية الخاصة التي أنشأناها في القسم السابق باسم middleware.js إلى المجلد utils بعد إجراء القليل من التعديلات:

const logger = require('./logger')

const requestLogger = (request, response, next) => {
  logger.info('Method:', request.method)
  logger.info('Path:  ', request.path)
  logger.info('Body:  ', request.body)
  logger.info('---')
  next()
}

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

const errorHandler = (error, request, response, next) => {
  logger.error(error.message)

  if (error.name === 'CastError') {
    return response.status(400).send({ error: 'malformatted id' })
  } else if (error.name === 'ValidationError') {
    return response.status(400).json({ error: error.message })
  }

  next(error)
}

module.exports = {
  requestLogger,
  unknownEndpoint,
  errorHandler
}

أُوكلت مهمة الاتصال مع قاعدة البيانات إلى الوحدة الرئيسية app.js. بينما تتحمل الوحدة note.js الموجودة في المجلد models مسؤولية تعريف تخطيطات Mongoose للملاحظات.

const mongoose = require('mongoose')

mongoose.set('useFindAndModify', false)

const noteSchema = new mongoose.Schema({
  content: {
    type: String,
    required: true,
    minlength: 5
  },
  date: {
    type: Date,
    required: true,
  },
  important: Boolean,
})

noteSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString()
    delete returnedObject._id
    delete returnedObject.__v
  }
})

module.exports = mongoose.model('Note', noteSchema)

باختصار، سيبدو هيكل المشروع بشكله الجديد كالتالي:

├── index.js
├── app.js
├── build
│   └── ...
├── controllers
│   └── notes.js
├── models
│   └── note.js
├── package-lock.json
├── package.json
├── utils
│   ├── config.js
│   ├── logger.js
│   └── middleware.js  

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

لا توجد معايير أو تفاهمات خاصة بين المطورين على تسمية مجلدات المشروع عند بناء تطبيقات express. بالمقابل تحتاج بعض المكتبات مثل Ruby on Rails هيكلًا خاصًا.

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

ستجد التطبيق بشكله الكامل في الفرع part4-1 على GitHub.

إن نسخت المشروع لتحتفظ به لنفسك، نفذ الأمر npm install، قبل أن تشغل التطبيق بالأمر npm run.

التمارين 4.1 - 4.2

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

4.1 قائمة بالمدونات: الخطوة 1

لنتخيل أنك تلقيت بريدًا إلكترونيًا له جسم التطبيق التالي:

const http = require('http')
const express = require('express')
const app = express()
const cors = require('cors')
const mongoose = require('mongoose')

const blogSchema = new mongoose.Schema({
  title: String,
  author: String,
  url: String,
  likes: Number
})

const Blog = mongoose.model('Blog', blogSchema)

const mongoUrl = 'mongodb://localhost/bloglist'
mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true })

app.use(cors())
app.use(express.json())

app.get('/api/blogs', (request, response) => {
  Blog
    .find({})
    .then(blogs => {
      response.json(blogs)
    })
})

app.post('/api/blogs', (request, response) => {
  const blog = new Blog(request.body)

  blog
    .save()
    .then(result => {
      response.status(201).json(result)
    })
})

const PORT = 3003
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

حول التطبيق إلى مشروع npm قابل للعمل. هيئ التطبيق ليُنفّذ بمساعدة nodemon. يمكنك أن تنشئ قاعدة بيانات جديدة على MongoDB Atlas، أو استخدم قاعدة البيانات التي أنشأناها في تمرينات القسم السابق. تأكد من إمكانية إضافة مدونات إلى القائمة باستخدام Postman أو VS Code REST client وتأكد كذلك أن التطبيق سيعيد المدونات التي أضيفت إلى الجهة التي طلبتها.

4.2 قائمة بالمدونات: الخطوة 2

قسم التطبيق إلى وحدات منفصلة كما فعلنا سابقًا في هذا القسم.

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

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

اختبار تطبيقات Node

لقد أهملنا كليًا ناحية مهمة من نواحي تطوير البرمجيات وهي الاختبارات المؤتمتة. سنبدأ رحلتنا في مجال الاختبارات، باختبارات الأجزاء unit test. إن منطق التطبيق الذي نعمل عليه بسيط فلن يكون موضوع اختبارات الأجزاء مهمًا لهذه الدرجة. مع ذلك لننشئ ملفًا جديدًا باسم for_testing.js في المجلد utils ولنعرف فيه دالتين نستخدمهما لاختبار بعض نواحي كتابة الشيفرة:

const palindrome = (string) => {
  return string
    .split('')
    .reverse()
    .join('')
}

const average = (array) => {
 const reducer = (sum, item) => {
    return sum + item
  }

  return array.reduce(reducer, 0) / array.length
}

module.exports = {
  palindrome,
  average,
}
اقتباس

تستخدم الدالة average تابع المصفوفات reduce. إن لم تكن على دراية باستخدام هذا التابع، فمن المفيد أن تتابع مقاطع الفيديو الثلاثة الأولى من سلسلة Functional Javascript على YouTyube.

هناك الكثير من المكتبات أو أدوات الاختبار التي تُستخدم مع JavaScript. سنستخدم في منهاجنا المكتبة Jest التي طورت واستخدمت من قبل Facebook. وتشابه هذه المكتبة مكتبة Mocha الزعيمة السابقة لأدوات اختبار JavaScript. ومن البدائل المطروحة أيضًا مكتبة ava التي اكتسبت شعبية كبيرة في بعض النطاقات.

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

اقتباس

لمستخدمي windows: يمكن أن لا تعمل Jest إن كان في مسار المشروع مجلدات تحتوي أسماءها على فراغات.

طالما أن الاختبارات ستنفذ فقط في مرحلة التطوير، سنثبت Jest كاعتمادية للتطوير:

npm install --save-dev jest

لنعرف سكربت npm باسم test لتنفيذ الاختبارات باستخدام Jest وإنشاء تقرير عن التنفيذ باستخدام الأسلوب verbose:

{
  //...
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "build:ui": "rm -rf build && cd ../../../2/luento/notes && npm run build && cp -r build ../../../3/luento/notes-backend",
    "deploy": "git push heroku master",
    "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push && npm run deploy",
    "logs:prod": "heroku logs --tail",
    "lint": "eslint .",
    "test": "jest --verbose"  },
  //...
}

تحتاج المكتبة Jest سكربت لنحدد فيه أن بيئة التنفيذ هي Node. ويتم ذلك بإضافة السطر التالي في نهاية الملف package.json:

{
 //...
 "jest": {
   "testEnvironment": "node"
 }
}

أو بدلًا عن ذلك، تبحث Jest عن ملف تهيئة باسم jest.config.js، حيث تُعرّف داخله بيئة التنفيذ كالتالي:

module.exports = {
  testEnvironment: 'node',
};

سننشئ مجلدًا مستقلًا للاختبارات يدعى tests ونضع فيه الملف palindrome.test.js الذي يحتوي الشيفرة التالية:

const palindrome = require('../utils/for_testing').palindrome

test('palindrome of a', () => {
  const result = palindrome('a')

  expect(result).toBe('a')
})

test('palindrome of react', () => {
  const result = palindrome('react')

  expect(result).toBe('tcaer')
})

test('palindrome of releveler', () => {
  const result = palindrome('releveler')

  expect(result).toBe('releveler')
})

ستعترض قواعد تهيئة المدقق ESLint الذي أضفناها إلى المشروع في الفصل السابق على الأمرين test و expect في ملف الاختبار، ذلك أن قواعد التهيئة لا تسمح بوجود متغيرات شاملة Globals. لنتخلص من ذلك بإضافة القيمة jest": true" إلى الخاصية env في الملف eslintrc.js:

module.exports = {
  "env": {
    "commonjs": true 
    "es6": true,
    "node": true,
    "jest": true,  },
  "extends": "eslint:recommended",
  "rules": {
    // ...
  },
};

يدرج الملف الاختبار في السطر الأول الدالة التي سنختبرها، ويسندها إلى متغير يدعى palindrome:

const palindrome = require('../utils/for_testing').palindrome

تُعرّف الحالات المختبرة بشكل فردي باستخدام الدالة test. تقبل الدالة معاملين، الأول سلسلة نصية تصف الاختبار والثاني دالة تُعرِّف طريقة تنفيذ اختبار الحالة.

تبدو طريقة اختبار الحالة الثانية في شيفرة ملف الاختبار palindrome.js كالتالي:

() => {
  const result = palindrome('react')

  expect(result).toBe('tcaer')
}

ننفذ أولًا الشيفرة التي سنختبرها، حيث نولد سلسلة معكوسة للنص "react". ثم نتحقق من النتيجة باستخدام الدالة expect. تغلف الدالة expect القيمة الناتجة على شكل كائن يقبل الكثير من توابع المطابقة التي يمكن استخدامها للتحقق من صحة النتيجة. يمكن استخدام تابع المطابقة toBe كون عملية المقارنة في حالتنا بين سلسلتين نصيتين.

كما هو متوقع ستنجح جميع الاختبارات:

test_pass_001.png

تتوقع المكتبة Jest أن تحتوي أسماء ملفات الاختبارات العبارة "test." وسنلتزم في منهاجنا بهذا التوجيه وسنجعل لاحقة كل ملفات الاختبار على الشكل "test.js."

تتمتع Jest بإظهارها رسائل خطأ ممتازة، سنجعل الاختبار يفشل لعرض ذلك:

test('palindrom of react', () => {
  const result = palindrome('react')

  expect(result).toBe('tkaer')
})

عند تنفيذ الاختبار السابق ستظهر رسالة الخطأ التالية:

jest_error_msg_002.png

لنضف الآن عدة اختبارات إلى الدالة average في الملف average.test.js الموجود في المجلد tests.

const average = require('../utils/for_testing').average

describe('average', () => {
  test('of one value is the value itself', () => {
    expect(average([1])).toBe(1)
  })

  test('of many is calculated right', () => {
    expect(average([1, 2, 3, 4, 5, 6])).toBe(3.5)
  })

  test('of empty array is zero', () => {
    expect(average([])).toBe(0)
  })
})

سنلاحظ أن الدالة لن تعمل بالشكل الصحيح مع المصفوفة الفارغة، لأن ناتج القسمة على 0 في JavaScript سيعطي القيمة NaN.

empty_array_error_003.png

تصحيح المشكلة السابقة أمر بسيط جدًا:

const average = array => {
  const reducer = (sum, item) => {
    return sum + item
  }

  return array.length === 0
    ? 0
    : array.reduce(reducer, 0) / array.length
}

ستعيد الدالة القيمة 0 إذا كان طول المصفوفة 0. ونستخدم في بقية الحالات التابع reduce لحساب المتوسط.

يجب الانتباه إلى عدة أمور في الاختبارات التي أجريناها. فلقد أسمينا الكتلة النصية التي تصف الاختبار "average":

describe('average', () => {
  // tests
})

تستخدم الكتل التي تصف الاختبارات في تجميع الاختبارات ضمن مجموعات. كما تسمى نتيجة الاختبار في Jest باسم الكتلة أيضًا.

describe_block_004.png

وتبرز أهمية الكتل describe عندما نحاول تشغيل بعض الإعدادات المشتركة أو إنهاء العمليات المتعلقة بمجموعة من الاختبارات.

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

test('of empty array is zero', () => {
  expect(average([])).toBe(0)
})

التمارين 4.3 - 4.7

لننشئ مجموعة من الدوال المساعدة التي ستُخصص للتعامل مع قائمة المدونات. ضع هذه الدوال في ملف يدعى list_helper.js ضمن مجلد اسمه utils. اكتب الاختبارات في ملف يحمل اسمًا مناسبًا وضعه في المجلد tests.

4.3 الدوال المساعدة واختبارات الأجزاء: الخطوة 1

عرّف في البداية دالةً باسم dummy تستقبل مصفوفة من منشورات مدونة كمعامل وتعيد دائمًا القيمة 1. سيبدو محتوى الملف list_helper.js حتى هذه اللحظة كالتالي:

const dummy = (blogs) => {
  // ...
}

module.exports = {
  dummy
}

تحقق أن معلومات التهيئة لاختبارك ستعمل مع الاختبار التالي:

const listHelper = require('../utils/list_helper')

test('dummy returns one', () => {
  const blogs = []

  const result = listHelper.dummy(blogs)
  expect(result).toBe(1)
})

4.4 الدوال المساعدة واختبارات الأجزاء: الخطوة 2

عرف دالةً جديدةً تدعى totalLikes تستقبل قائمة بمنشورات مدونة كمعامل وتعيد مجموع الإعجابات في كل منشورات هذه المدونة.

اكتب اختبارًا مناسبًا للدالة. يفضل أن تضع الاختبار ضمن كتلة describe، كي تُجمّع التقارير المتولدة عن الاختبارات بشكل واضح.

helpers_step2_005.png

يمكن أن تُعرّف عناصر دخل الدالة على النحو التالي:

describe('total likes', () => {
  const listWithOneBlog = [
    {
      _id: '5a422aa71b54a676234d17f8',
      title: 'Go To Statement Considered Harmful',
      author: 'Edsger W. Dijkstra',
      url: 'http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html',
      likes: 5,
      __v: 0
    }
  ]

  test('when list has only one blog, equals the likes of that', () => {
    const result = listHelper.totalLikes(listWithOneBlog)
    expect(result).toBe(5)
  })
})

إن وجدت أن إدخال قائمة المدونات يتطلب الكثير من الجهد، استخدم القائمة الجاهزة التي وضعناها على GitHub.

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

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

هناك أسلوب آخر لتنفيذ اختبار محدد بكتابة اسم الكتلة (اسم الاختبار) عند تنفيذ الاختبار مع الصفة t-.

npm test -- -t 'when list has only one blog, equals the likes of that'

4.5 الدوال المساعدة واختبارات الأجزاء: الخطوة 3 *

عرّف دالة جديدة باسم favoriteBlog تستقبل قائمة بالمدونات كمعامل. تكتشف الدالة المدونة التي تحمل أكبر عدد من الإعجابات. يكفي أن تعيد الدالة مدونة واحدة إن كان هناك أكثر من واحدة.

يمكن أن تعيد الدالة قيمة لها الصيغة التالية:

{
  title: "Canonical string reduction",
  author: "Edsger W. Dijkstra",
  likes: 12
}

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

اكتب الاختبار ضمن كتلة describe. كرر ذلك في التمارين الباقية أيضًا.

4.6 الدوال المساعدة واختبارات الأجزاء: الخطوة 4 *

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

يُنجَز هذا التمرين بلا استخدام أية مكتبات إضافية. لكنه في المقابل فرصة مواتية لتعلم استخدام المكتبة Lodash.

عرّف دالة اسمها mostBlogs تستقبل مصفوفة من المدونات كمعامل. تعيد الدالة اسم المؤلف الذي لديه العدد الأكبر من المدونات، كما تعيد عدد هذه المدونات:

{
  author: "Robert C. Martin",
  blogs: 3
}

إن وجد أكثر من مؤلف يحقق المطلوب، يكفي أن تعرض أحدهم.

4.7 الدوال المساعدة واختبارات الأجزاء: الخطوة 5 *

عرّف دالة باسم mostLikes تستقبل مصفوفة من المدونات كمعامل. تعيد الدالة المؤلف الذي حاز على أكبر عدد من الإعجابات، كما تعيد العدد الكلي لتلك الإعجابات:

{
  author: "Edsger W. Dijkstra",
  likes: 17
}

إن وجد أكثر من مؤلف يحقق المطلوب، يكفي أن تعرض أحدهم.

ترجمة -وبتصرف- للفصل Structure of backend application, introduction to testing من سلسلة Deep Dive Into Modern Web Development





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن