آموزش تست کردن برنامه‌های Nodejs با Mocha و Chai و SinonJS - قسمت 2 - کار با Spies و Stubs و Mocks

دسته بندی: آموزش
زمان مطالعه: 12 دقیقه
۱۰ بهمن ۱۳۹۷

در این قسمت میخوام مطلب آموزش تست کردن برنامه‌های Nodejs با Mocha و Chai و SinonJS رو ادامه بدم و با استفاده از مثالهای کاربردی نحوه کار با Spies و Stubs و Mocks رو بهتون آموزش بدم.

خب در ابتدا یک دایرکتوری بنام controller به وجود میارم و فایل app.controller.js رو در اون میسازم.

Spies

فرض کنید که کدهای زیر در فایل بالا قرار داده شده است:

module.exports = {
  getIndexPage: (req, res) => {
    res.send("Hey");
  },
};

همونطور که میبینید یک متد بنام getIndexPage وجود داره که 2 پارامتر ورودی req  و res میگیره و متن Hey رو برای کاربر ارسال میکنه. برای تست کردن این فایل یک فایل در مسیر /tests/controller/app.controller.test.js میسازم و کدهای زیر رو در اون قرار میدم:

const chai = require("chai");
const expect = chai.expect;

const indexPage = require("../../controller/app.controller");

describe("getIndexPage", function() {
  it("should return index page", function() {
    let req = {};
    let res = {
      send: function() {}
    };

    indexPage.getIndexPage(req, res);
  });
});

همونطور که میبینید فایل app.controller.js رو در این فایل تست وارد کردیم و 2 شئ بنام req و res به وجود میارم و برای res یک متد send قرار دادم. در نهایت متد getIndexPage رو با req و res فراخوانی کردیم.

تا اینجا کار خاصی انجام ندادیم و نمیتونیم با این موارد چیزی رو تست کنیم. ما حالا میتونیم از spy استفاده کنیم و کد مورد نظر رو تست کنیم. شما میتونین assertion هایی رو برای spy تعریف کنید بخاطر اینکه spy یک تابع ساختگی رو در اختیارمون قرار میده و میتونیم با استفاده از اون اجرای تابع رو track کنیم.

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;

const indexPage = require('../../controller/app.controller');

describe("getIndexPage", function() {
  it("should return index page", function() {
    let req = {};
    let res = {
      send: sinon.spy(),
    };

    indexPage.getIndexPage(req, res);

    // let's see what we get on res.send
    console.log(res.send)
  });
});

همونطور که میبینید sinon رو وارد پروژه کردیم و برای متد send یک spy قرار دادیم. برای به وجود آوردن spy از sinon.spy() استفاده میکنیم. برای اینکه ببینیم spy چه اطلاعاتی رو در اختیارمون قرار میده میتونیم res.send رو در console چاپ کنیم.

برای اجرا کردن تست‌ها از دستور mocha tests/**/*.* استفاده میکنیم و به معنای اینه که همه فایلهایی که در دایرکتوری و sub directory های tests وجود داره رو اجرا کن. خب حالا میخوایم از امکانات spy استفاده کنیم و تست مورد نظرمون رو انجام بدیم. ما میخوایم موارد زیر رو تست کنیم و از کارکرد اونا مطمئن بشیم:

  • ما انتظار داریم که متد res.send یکبار اجرا بشه.
  • ما انتظار داریم که در فراخوانی اول تابع res.send آرگومان Hey رو دریافت کنیم.

برای اینکار بصورت زیر عمل میکنیم:

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;

const indexPage = require('../../controller/app.controller');

describe("getIndexPage", function() {
  it("should send hey", function() {
    let req = {};
    let res = {
      send: sinon.spy(),
    };

    indexPage.getIndexPage(req, res);

    expect(res.send.calledOnce).to.be.true;

    expect(res.send.firstCall.args[0]).to.equal('Hey');
  });
});

همونطور که میبینید چک کردیم که ویژگی calledOnce برابر با true باشه و آرگومان اول firstCall برابر با Hey باشه. اگر تست رو اجرا کنیم، خروجی بصورت زیر خواهد بود:

همونطور که میبینید تست مورد نظر با موفقیت انجام میشه و همه چیز همونطوری هست که انتظارشو داریم. حالا مثلا اگر بجای Hey کلمه bla رو قرار بدیم، خواهیم دید که تست fail میشه. بصورت زیر:

در مثال بالا تابعی وجود نداشت و ما یک spy ساختیم و کارهای مورد نظرمون رو انجام دادیم. حالا اگر یک تابع از قبل داشتیم باشیم و بخوایم برای اون spy قرار بدیم و اطلاعات اون رو مثل یک جاسوس (spy) به دست بیاریم، باید چکار کنیم؟ برای اینکار یک تابع ساده رو تعریف میکنیم و با استفاده از متد sinon.spy به اون متصل میشیم. بصورت زیر:

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;

const user = {
  addUser: (name) => {
    this.name = name;
  },
};

describe("User", function() {
  describe("addUser", function() {
    it("should add a user", function() {
      sinon.spy(user, "addUser");

      // lets log `addUser` and see what we get
      console.log(user.addUser);
    });
  });
});

همونطور که میبینید یک متد بنام addUser داریم که یک ورودی name میگیره و اون رو در this.name قرار میده. با استفاده از sinon.spy برای متد addUser مربوط به شئ user یک spy قرار دادیم . حالا user.addUser رو چاپ کردیم تا ببینیم که چه اطلاعاتی رو spy در مورد متد مورد نظر در اختیارمون قرار میده. اگر console رو مشاهده کنید خواهید دید که spy اطلاعات زیادی رو در مورد تابع از قبل تعریف شده نیز در اختیارتون قرار میده.

حالا فرض کنید که ما متد addUser رو فراخوانی کنیم و بعد از اون میخوایم تست کنیم که متد مورد نظر یک بار فراخوانی شده است یا خیر. برای اینکار بصورت زیر عمل میکنیم:

const sinon = require('sinon');
const chai = require('chai');
const expect = chai.expect;

const user = {
  addUser: (name) => {
    this.name = name;
  },
};

describe("User", function() {
  describe("addUser", function() {
    it("should add a user", function() {
      sinon.spy(user, "addUser");

      user.addUser('Mohammad Esfandiari');

      expect(user.addUser.calledOnce).to.be.true;
    });
  });
});

حالا اگر تست بالا رو اجرا کنیم، میبینیم که pass میشه و معلوم میشه که تابع مورد نظر یکبار فراخوانی شده است.

پس تا اینجای کار فهمیدیم که spies یا جاسوس‌ها به چه دردی میخورن و میتونیم هم برای توابع ساختگی و هم توابع از پیش تعریف شده از اونا استفاده کنیم. با استفاده از spy ها میتونین اطلاعات مفیدی رو در مورد نحوه اجرا شدن توابع به دست بیارید و کدهاتون رو از جنبه‌های مختلف تست کنید. استفاده از spies در sinon خیلی ساده هست و بیشتر ویژگی‌ها بر پایه اون ساخته شده‌اند. spy ها یک نقطه شروع خوب برای کار با SinonJS هستند.

Stubs

Stub ها خیلی عالی هستند. بخاطر اینکه همه ویژگی‌های مربوط به Spy ها رو دارند ولی برخلاف spy کل تابع رو جایگزین میکنند. Stubs ها در حالتهای زیر خیلی کاربردی هستند:

  • فراخوانی توابع external که باعث آهسته شدن تست میشن (مانند درخواست‌های HTTP و ارتباط با Database)
  • شبیه‌سازی حالت‌های مختلف برای یک قطعه کد (مثلا اگر یک ارور به وجود بیاد چه اتفاقی میوفته و یا اینکه اگر موفقیت‌آمیز باشه چی بشه)

در فایل app.controller.js موجود در دایرکتوری controllers، متد getIndexPage رو بصورت زیر تغییر میدیم:

module.exports = {
  getIndexPage: (req, res) => {
    if (req.user.isLoggedIn()) {
      return res.send("Hey");
    }

    res.send("Ooops. You need to log in to access this page");
  },
};

همونطور که میبینید در این متد چک شده که کاربر login هست یا خیر و اگر login باشه Hey و در غیر اینصورت متن مورد نظر به کاربر نشون داده میشه و به اون میگه باید در سایت Login بکنه.

isLoggedIn در این حالت یک متد هست که بررسی میکنه که کاربر فعلی Login هست یا خیر. این متد میتونه با استفاده از JWT Token لاگین بودن رو متوجه بشه و یا مستقیما به Database متصل بشه و Login بودن کاربر رو بررسی کنه. برای ما فرقی نمیکنه که ساختار این متد چی هست و به چه روشی این کار رو انجام میده. ما فرض میکنیم که این متد کارهای زیادی رو انجام میده و مدت زمان زیادی هم طول میکشه و اگر کاربر Login باشه true و در غیر اینصورت false رو برمی‌گردونه.

در فایل test کدهای زیر رو جایگزین کدهای قبلی بکنید:

const sinon = require("sinon");
const chai = require("chai");
const expect = chai.expect;

const indexPage = require("../../controller/app.controller");

describe("AppController", function()  {
  describe("getIndexPage", function() {
    it("should send hey when user is logged in", function() {
      // instantiate a user object with an empty isLoggedIn function
      let user = {
        isLoggedIn: function() {}
      }

      // Stub isLoggedIn function and make it return true always
      const isLoggedInStub = sinon.stub(user, "isLoggedIn").returns(true);

      // pass user into the req object
      let req = {
        user: user
      };

      // Have `res` have a send key with a function value coz we use `res.send()` in our func
      let res = {
        send: sinon.spy(),
      };

      indexPage.getIndexPage(req, res);
      // let's see what we get on res.send
      // console.log(res.send);
      // `res.send` called once
      expect(res.send.calledOnce).to.be.true;
      expect(res.send.firstCall.args[0]).to.equal("Hey");

      // assert that the stub is logged in at least once
      expect(isLoggedInStub.calledOnce).to.be.true;
    });
  });
});

همونطور که میبینید در خط 16 یک stub برای متد isLoggedIn مربوط به شئ user به وجود آوردیم و کاری کردیم که همیشه مقدار true رو برگشت بده. Stub ساخته شده جایگزین متد isLoggedIn میشه و ما میتونیم سناریوی Login بودن کاربر رو تست کنیم. در حالتی که کاربر Login هست باید متن Hey به اون نشون داده بشه. حالا اگر تست‌ها رو مجددا اجرا کنیم، خواهیم دید که pass خواهند شد.

همچنین میتونین Login نبودن کاربر رو نیز تست کنیم. برای اینکار بصورت زیر عمل میکنیم:

...

describe("AppController", function()  {
  describe("getIndexPage", function() {
    it("should send hey when user is logged in", function() {
      ...
    });

    it("should send something else when user is NOT logged in", function() {
      // instantiate a user object with an empty isLoggedIn function
      let user = {
        isLoggedIn: function(){}
      }

      // Stub isLoggedIn function and make it return false always
      const isLoggedInStub = sinon.stub(user, "isLoggedIn").returns(false);

      // pass user into the req object
      let req = {
        user: user
      }

      // Have `res` have a send key with a function value coz we use `res.send()` in our func
      let res = {
        // replace empty function with a spy
        send: sinon.spy()
      }

      indexPage.getIndexPage(req, res);
      // let's see what we get on res.send
      // console.log(res.send);
      // `res.send` called once
      expect(res.send.calledOnce).to.be.true;
      expect(res.send.firstCall.args[0]).to.equal("Ooops. You need to log in to access this page");

      // assert that the stub is logged in at least once
      expect(isLoggedInStub.calledOnce).to.be.true;
    })
  });
});

اونجاهایی که ... قرار داده شده یعنی همون کدهای قبلی رو قرار بدین. مثال بالا یک کار خیلی ساده هست و شما میتونین کارهای پیشرفته‌تری رو با Stub ها انجام بدین. برای مطالعه مستندات Stubs میتونین بر روی این لینک کلیک کنید.

Mocks

با استفاده از Mocks ما میتونیم مشخص کنیم که چیزها چطوری باید کار کنند و با استفاده از mock.verify() میتونین مطمئن بشید که همونطور که میخواید کار میکنه یا خیر. با اینکار کدها کمتر و خواناتر میشن. با استفاده از تست‌هایی که قبلا نوشتیم ما میتونیم:

  • میتونیم یک mock برای شئ res به وجود بیاریم.
  • میخوایم متد send با آرگومان Hey یکبار فراخوانی بشه.
  • در آخر متد mock.verify() رو فراخوانی میکنیم.

برای اینکار بصورت زیر عمل میکنیم:

const sinon = require("sinon");
const chai = require("chai");
const expect = chai.expect;

const indexPage = require("../../controller/app.controller");

describe("AppController", function()  {
  describe("getIndexPage", function() {
    it("should send hey when user is logged in", function() {
      // instantiate a user object with an empty isLoggedIn function
      let user = {
        isLoggedIn: function(){}
      }

      // Stub isLoggedIn function and make it return true always
      const isLoggedInStub = sinon.stub(user, "isLoggedIn").returns(true);

      // pass user into the req object
      let req = {
        user: user,
      };

      // Have `res` have a send key with a function value coz we use `res.send()` in our func
      let res = {
        send: function() {},
      };

      // mock res
      const mock = sinon.mock(res);
      // build how we expect it work
      mock.expects("send").once().withExactArgs("Hey");

      indexPage.getIndexPage(req, res);
      expect(isLoggedInStub.calledOnce).to.be.true;

      // verify that mock works as expected
      mock.verify();
    });
  });
});

همونطور که میبینید در خط 29 mock رو به وجود آورده و چیزی که از اون انتظار داریم رو در خط 31 مشخص کردیم و در نهایت در خط 37 متد verify رو برای تست اون فراخوانی کردیم. اگر دستور mocha test/**/*.* رو مجددا اجرا کنیم، خواهیم دید که تست pass خواهد شد.

نتیجه‌گیری

در این قسمت در مورد کار با Spies و Stubs و Mocks توضیحاتی رو در اختیارتون قرار دادیم و با استفاده از مثالهای ساده کاربرد اونا رو بهتون یاد دادیم.

در قسمت بعد در مورد تست کردن کدهای Async توضیح میدم و یاد میگیریم که چطور با اونا کار کنیم.

نویسنده
بسیار به طراحی وب علاقمندم و به سرعت در حال یادگیری تمام مباحث پیشرفته هستم و دوست دارم که به دیگران هم یاد بدهم.

نظرات کاربران

اولین دیدگاه این پست رو تو بنویس !

ارسال دیدگاه
خوشحال میشیم دیدگاه و یا تجربیات خودتون رو با ما در میون بذارید :

 
گزارش مشکل