دسته بندی نشده

Mocking پایگاه داده در Node با Jest

در مقاله قبلی، یک api اکسپرس را آزمایش کردیم که یک کاربر ایجاد کرد. ما فقط رابط http را آزمایش کردیم، اما هرگز نتوانستیم پایگاه داده را آزمایش کنیم زیرا هنوز از تزریق وابستگی اطلاعی نداشتیم. اکنون که می دانیم چگونه پایگاه داده را تزریق کنیم، می توانیم درباره Mocking یاد بگیریم.

در این مقاله، نحوه استفاده از Mocking برای آزمایش نحوه تعامل یک اپلیکیشن اکسپرس با پایگاه داده را خواهیم آموخت.

POST /users

بیایید درخواست پستی که کاربر جدیدی ایجاد می کند را بررسی کنیم. مشتری یک نام کاربری و رمز عبور را در بدنه درخواست ارسال می‌کند و این داده‌ها باید در نهایت در پایگاه داده ذخیره شوند تا کاربر جدید باقی بماند.

چگونه این را تست کنیم؟

  • ما می‌توانیم یک آزمایش خودکار بنویسیم که یک درخواست POST برای سرور ما ایجاد کند تا یک کاربر جدید ایجاد کند، سرور می‌تواند منطق داخلی را اجرا کند، شاید نام کاربری و رمز عبور را تأیید کند، سپس آن را در یک پایگاه داده ذخیره می‌کند.
  • سپس می‌توانیم مستقیماً از پایگاه داده پرس و جو کنیم و بررسی کنیم که داده‌ها واقعاً به درستی در پایگاه داده ذخیره شده‌اند.
  • یا پس از آن می‌توانیم درخواست دیگری از سرور ارائه کنیم تا کاربر را امتحان کرده و لاگین کند و اگر جواب داد، می‌دانیم که کاربر باید به درستی ذخیره شده باشد.

این آزمایش‌ها واقعاً خوب است که در برنامه ما داشته باشیم و جریان کاربر واقعی برنامه را آزمایش کنیم، همه قطعات مختلف را دقیقاً با هم ادغام می‌کند، دقیقاً همانطور که در حال تولید هستند. یکی از مسائل مربوط به این تست ها این است که ما در نهایت بسیاری از چیزها را یکجا آزمایش می کنیم. سرور، مقداری منطق داخلی، اتصال به پایگاه داده و در مثال دوم، دو درخواست http مجزا. اگر آزمایشی با شکست مواجه شود، تشخیص اینکه کدام قسمت از برنامه کار نمی کند ممکن است دشوار باشد.

اگر بخواهیم هر قسمت از برنامه را جداگانه آزمایش کنیم چه؟ سرور HTTP، منطق داخلی و لایه پایگاه داده را جداگانه آزمایش کنید. اگر بتوانیم همه چیز را در انزوا کامل آزمایش کنیم، دقیقاً می دانیم که چه چیزی کار می کند و چه چیزی کار نمی کند. اگر تستی با شکست مواجه شود، بسیار واضح خواهد بود که مشکل کجاست و رفع آن مشکل آسان‌تر خواهد بود.

ما هنوز باید سیستم را به طور کلی آزمایش کنیم، این هنوز مهم است، اما شاید بتوانیم پس از اینکه همه چیز را جداگانه آزمایش کردیم، این کار را انجام دهیم.

قبل از انجام این کار، باید به وابستگی ها نگاهی بیندازیم:

  • سرور http به منطق اعتبارسنجی داخلی و بسته بندی پایگاه داده وابسته است. برای اجرا باید بتواند کد را از این قسمت‌های برنامه اجرا کند، بنابراین آزمایش آن به صورت مجزا کمی سخت به نظر می‌رسد.
  • منطق داخلی به هیچ بخش دیگری از برنامه وابسته نیست، این کد است که می تواند به راحتی اجرا شود و به صورت مجزا آزمایش شود.
  • بسته بندی پایگاه داده به هیچ بخش دیگری از برنامه وابسته نیست، به یک پایگاه داده واقعی، شاید mysql یا mongo یا چیزی دیگر وابسته است، بنابراین این امر به بررسی خاصی نیاز دارد، اما به هیچ بخش دیگری از برنامه ما وابسته نیست.

بیایید برای لحظه ای فرض کنیم که منطق داخلی و بسته بندی پایگاه داده قبلاً به طور کامل آزمایش شده اند. می دانیم که این دو بخش از برنامه به صورت مجزا کار می کنند. بنابراین ما می توانیم آنها را در حال حاضر فراموش کنیم. اما چگونه می‌خواهیم قسمت سرور http برنامه را به‌صورت مجزا آزمایش کنیم، در حالی که به این قطعات دیگر وابسته است؟

موکینگ (Mocking)

هنگامی که ما از یک آزمایش در یک تست خودکار استفاده می کنیم، از نسخه جعلی یک چیز واقعی استفاده می کنیم. ما می توانیم از نسخه جعلی برای آزمایش تعاملات استفاده کنیم. ما می‌توانیم آزمایش کنیم که createUser تابع واقعاً فراخوانی شده است، و داده‌های صحیح ارسال شده است، اما پایگاه داده واقعی را آزمایش نمی‌کنیم. به بلوک کد زیر نگاهی بیندازید:

app.post('/users', async (req, res) => {
  const { username, password } = req.body
  database.createUser(username, password)
})

در برنامه تولیدی ما، پایگاه داده شیئی خواهد بود که به یک پایگاه داده واقعی درخواست می دهد، شاید MySQL یا Mongo یا چیز دیگری. اما در تست های خود می توانیم از یک پایگاه داده ساختگی استفاده کنیم و روشی را که به آن createUserمتد گفته شده است تست کنیم.

Mocking با Jest

در اینجا برنامه اکسپرس ما از پست قبلی در مورد آزمایش اکسپرس apis آمده است:

import express from 'express'

let app = express()

app.use(express.json())
app.post('/users', async (req, res) => {
  const { username, password } = req.body
  if (!username || !password) {
    res.send(400)
    return
  }

  res.send({userId: 0})
})

export default app

اولین کاری که باید انجام دهیم این است که از تزریق وابستگی برای ارسال در پایگاه داده به برنامه استفاده کنیم:

import express from 'express'

export default function(database) {
  let app = express()

  app.use(express.json())
  app.post('/users', async (req, res) => {
    const { username, password } = req.body
    if (!username || !password) {
      res.send(400)
      return
    }

    res.send({userId: 0})
  })

  return app
}

در تولید، ما در یک پایگاه داده واقعی قبول می‌شویم، اما در آزمون‌های خود در یک پایگاه داده ساختگی قبول می‌شویم.

بیایید فایل app.test.js را اصلاح کنیم. ما در حال حاضر فقط به آزمایش هایی که شامل پایگاه داده می شوند نگاه می کنیم:

import request from "supertest"
import makeApp from "./app.js"
import { jest } from '@jest/globals'

const createUser = jest.fn()
const app = makeApp({createUser})

describe("POST /users", () => {

  beforeEach(() => {
    createUser.mockReset()
  })

  describe("when passed a username and password", () => {
    // should save the username and password in the database
    // should contain the userId from the database in the json body
  })

})

jest.fn() یک تابع ساختگی با هدف کلی جدید ایجاد می کند که می توانیم از آن برای آزمایش تعامل بین سرور و پایگاه داده استفاده کنیم. بنابراین ما می توانیم آن را به برنامه داخل یک شیء ارسال کنیم. به یاد داشته باشید که برنامه منتظر یک شی پایگاه داده است که حاوی یک createUserتابع است، بنابراین این فقط یک نسخه ساختگی از یک پایگاه داده است.

توجه: اگر از ماژول‌های es استفاده می‌کنیم، باید jest را از «@jest/globals» وارد کنیم.

توابع ساختگی Jest نحوه فراخوانی آنها را پیگیری می کند. آنها پارامترهایی را که به آنها منتقل شده اند و تعداد دفعاتی که آنها را با جزئیات دیگر فراخوانی کرده اند ذخیره می کنند. به همین دلیل، باید قبل از هر تست، تابع را بازنشانی کنیم تا از یک تست دیگر حالتی باقی نماند.

پارامترهای تابع ساختگی

برنامه تماماً با یک پایگاه داده ساختگی راه اندازی شده است، اکنون زمان نوشتن یک آزمایش است:

describe("when passed a username and password", () => {
  test("should save the username and password in the database", () => {
    const body = {
      username: "username",
      password: "password"
    }
    const response = await request(app).post("/users").send(body)
    expect(createUser.mock.calls[0][0]).toBe(body.username)
    expect(createUser.mock.calls[0][1]).toBe(body.password)
  })
})

تابع createUser هر بار که تابع فراخوانی می شود، آنچه را که به تابع ارسال می شود، پیگیری می کند. بنابراین createUser.mock.calls[0] نشان دهنده داده هایی است که در اولین باری که فراخوانی می شود منتقل می شود. سرور باید تابع را با نام کاربری و رمز عبور مانند این createUser (نام کاربری، رمز عبور) فراخوانی کند، بنابراین createUser.mock.calls[0][0] باید نام کاربری باشد و createUser.mock.calls[0][0] باید باشد. رمز عبور

اگر تست را اجرا کنیم باید شکست بخورد زیرا سرور تابع createUser را فراخوانی نمی کند. بیایید آن را در app.js تغییر دهیم:

app.post('/users', async (req, res) => {
  const { username, password } = req.body
  if (!username || !password) {
    res.send(400)
    return
  }

  database.createUser(username, password)

  res.send({userId: 0})
})

اکنون تست باید بگذرد زیرا تابع createUser به درستی فراخوانی می شود.

به یاد داشته باشید، این آزمایش پایگاه داده واقعی نیست، موضوع در حال حاضر این نیست. ما برای آزمایش اینکه تعامل بین بخش‌های مختلف برنامه به درستی کار می‌کند، از ماک استفاده می‌کنیم. بنابراین تا زمانی که createUser در پایگاه داده «واقعی» به درستی کار کند، و سرور به درستی عملکرد را فراخوانی می کند، همه چیز در برنامه تمام شده باید به درستی کار کند.

با این حال، آزمون برای این کافی نیست که من را راحت کند. این فقط یک ترکیب نام کاربری و رمز عبور را آزمایش می کند، من احساس می کنم حداقل باید دو نام وجود داشته باشد تا به من اطمینان دهد که این تابع به درستی فراخوانی شده است، بنابراین بیایید تست را تنظیم کنیم:

test("should save the username and password in the database", () => {
  const bodyData = [
    {
      username: "username1",
      password: "password1"
    },
    {
      username: "username2",
      password: "password2"
    }
  ]
  for (const body of bodyData) {
    createUser.mockReset()
    const response = await request(app).post("/users").send(body)
    expect(createUser.mock.calls[0][0]).toBe(body.username)
    expect(createUser.mock.calls[0][1]).toBe(body.password)
  }
})

اکنون در حال آزمایش دو ترکیب نام کاربری و رمز عبور هستیم، اما اگر بخواهیم می‌توانیم موارد بیشتری را اضافه کنیم.

مقدار بازگشتی تابع Mock

ما آزمایش کرده‌ایم که برنامه داده‌های صحیح را به createUser ارسال می‌کند، اما همچنین باید آزمایش کنیم که از مقدار بازگشتی تابع به درستی استفاده کند. createUser باید شناسه کاربری که به تازگی ایجاد شده را برگرداند. سرور باید آن مقدار را بگیرد و در پاسخ به مشتری ارسال کند.

test("should contain the userId from the database in the json body", () => {

  createUser.mockResolvedValue(1)

  const response = await request(app).post("/users").send({
    username: "username",
    password: "password"
  })
  expect(response.body.userId).toBe(1)
})

createUser.mockResolvedValue(1) از آنجایی که پایگاه داده واقعی کارها را به صورت ناهمزمان انجام می دهد، تابع ساختگی ما نیز createUserباید همین کار را انجام دهد. بنابراین، 1 را به عنوان userId جعلی برمی گرداند و http api باید با آن مقدار پاسخ دهد.

این تست در حال حاضر ناموفق خواهد بود، بنابراین بیایید این را در app.js پیاده سازی کنیم:

app.post('/users', async (req, res) => {
  const { username, password } = req.body
  if (!username || !password) {
    res.send(400)
    return
  }

  const userId = database.createUser(username, password)

  res.send({userId})
})

این باید برای موفقیت در آزمون کافی باشد. این دقیقاً نحوه تعامل فایل app.js با پایگاه داده است. اما باز هم، آزمایش واقعاً آنقدر آزمایش نمی‌کند که به من اعتماد کند، بنابراین بیایید تست را کمی اصلاح کنیم:

test("should contain the userId from the database in the json body", () => {
  for (let i = 0; i < 5; i++) {
    createUser.mockResolvedValue(i)
    const response = await request(app).post("/users").send({
      username: "username",
      password: "password"
    })
    expect(response.body.userId).toBe(i)
  }
})

اکنون در حال آزمایش 5 مقدار id مختلف است. این فقط یک عدد تصادفی است که من انتخاب کردم، اما انجام این کار در یک حلقه for ساده به نظر می رسید. به هر حال، همین الان کافی است تا مطمئن شوید که برنامه به درستی با پایگاه داده ارتباط برقرار می کند.

بعد، احتمالاً باید آن پایگاه داده را آزمایش کنیم.