express項目集成mocha測試框架

mocha誕生於2011年,是一個特徵豐富的javascript測試框架,能夠運行在node.js和瀏覽器上,使異步測試更簡單和有趣。mocha測試連續運行,容許靈活和準確的報告,同時將未捕獲的異常映射到正確的測試用例。javascript

背景

公司項目中,沒有自動化的單元測試,而是經過寫if/else判斷,多少有點懵逼html

所在在種種考慮之下,咱們就選擇mocha測試框架作單元測試前端

測試報告

在terminal裏運行vue

npm run mochawesome
複製代碼

完成項目中的單元測試java

單擊 file:///+項目所在地址+/mochawesome-report/mochawesome.htmlnode

最終獲得的就是這一份測試報告react

待測試的接口

須要測試的代碼以下git

'use strict';

const router = require('express').Router();
const passport = require('passport');
const User = require('../collections/user');
const log = require('../services/logger').createLogger('userAuthentication');
const AUTH_ERR = require('../constant/errMessage').AUTH;
const COMM_ERR = require('../constant/errMessage').COMMON;

/** * @api {get} /v1/auth/ User auth information * @apiName UserAuthInfo * @apiGroup userAuthentication * * @apiParam {null} null. * * @apiSuccess {String} username The username of the current user. * @apiSuccess {date} last User last logon time. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "test", * "last": "2019-06-03T06:22:53.567Z" * } * * @apiError NOT_LOGIN The current User was not logon. * * @apiErrorExample Error-Response: * HTTP/1.1 401 Unauthorized * { * "err": "NOT_LOGIN", * "message": "User has not logon in!" * } */

router.get('/', function(req, res) {
  if (req.user) {
    res.json({
      username: req.user.username,
      last: req.user.last
    });
  } else {
    res.status(401).json({
      err: 'NOT_LOGIN',
      message: AUTH_ERR.NOT_LOGIN
    });
  }
});

/** * @api {post} /v1/auth/register User Register * @apiName UserRegister * @apiGroup userAuthentication * * @apiParam {String} username New user's name. * @apiParam {String} password New user's password. * * @apiSuccess {String} username The username of the register user. * @apiSuccess {string} message The registering success info. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "gushen", * "message": "User registered successful" * } * * @apiError REGISTER_FAILURE The register failure. * * @apiErrorExample Error-Response: * HTTP/1.1 500 Internal Server Error * { * "err": "REGISTER_FAILURE", * "message": "User register failure!" * } */
router.post('/register', function(req, res, next) {
  User.register(new User({ username: req.body.username }), req.body.password, function(err) {
    if (err) {
      log.error(err);
      res.status(500).json({
        err: 'REGISTER_FAILURE',
        message: AUTH_ERR.REGISTER_FAILURE
      });
      return;
    }

    log.info('user ' + req.body.username + ' registered successful!');
    res.json({
      username: req.body.username,
      message: 'User registered successful'
    });

  });
});

/** * @api {post} /v1/auth/login User login * @apiName UserLogin * @apiGroup userAuthentication * * @apiParam {String} username User's name. * @apiParam {String} password User's password. * * @apiSuccess {String} username The username of the register user. * @apiSuccess {string} message The messgaer if the user login in successful. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "test", * "message": "Authentication Success" * } * * @apiError REGISTER_FAILURE The register failure. * * @apiErrorExample Error-Response: * HTTP/1.1 401 Unauthorized * { * "err": "AUTHENTICATE_FAILURE", * "message": "Authenticate failure" * } */
router.post('/login', isAhenticated, passport.authenticate('local'), function(req, res) {
  if (req.user) {
    log.info(`${req.user.username} login in successful`);
    res.json({
      username: req.user.username,
      message: 'Authentication Success'
    });
    return;
  }
  log.info(`${req.user.username} login failure`);
  res.status(401).json({
    err: 'AUTHENTICATE_FAILURE',
    message: `${req.user.username} login failure`
  });

});

/** * @api {post} /v1/auth/user/:username User delete * @apiName UserDelete * @apiGroup userAuthentication * * @apiParam {String} username User's name. * * @apiSuccess {String} username The username of the deleted user. * @apiSuccess {string} message The message if deleting successful. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "gushen", * "message": "Delete User Successful" * } * * @apiError NOT_LOGIN The register failure. * * @apiErrorExample Error-Response: * HTTP/1.1 401 Unauthorized * { * "err": "NOT_LOGIN", * "message": "User has not logon in!" * } */
router.delete('/user/:username', function(req, res) {
  if (!req.user) {
    res.status(401).json({
      err: 'NOT_LOGIN',
      message: AUTH_ERR.NOT_LOGIN
    });
    return;
  }

  // if (!req.params.username) {
  // res.json({
  // err: 'PARAMS_NOT_CORRECT',
  // message: 'No deleted user name'
  // });
  // return;
  // }

  User.deleteOne({ username: req.params.username }, (err) => {
    if (err) {
      log.error(err);
      res.status(500).json({
        err: 'SERVER_ERROR',
        message: COMM_ERR.SERVER_ERROR
      });
      return;
    }

    res.json({
      username: req.params.username,
      message: 'Delete User Successful'
    });

    log.info(`${req.params.username} has been deleted`);
  });
});

/** * @api {post} /v1/auth/changepassword User change password * @apiName UserChangePassword * @apiGroup userAuthentication * * @apiParam {String} username User's name. * @apiParam {String} oldpassword User's old password. * @apiParam {String} newpassword User's old password. * * @apiSuccess {String} username The username of the user. * @apiSuccess {string} message The message if changing password successful. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "test", * "message": "change password successful" * } * * @apiError AUTHENTICATE_FAILURE The register failure. * * @apiErrorExample Error-Response: * HTTP/1.1 401 Unauthorized * { * "err": "AUTHENTICATE_FAILURE", * "message": "Password or username is incorrect" * } */
router.post('/changepassword', function(req, res) {
  User.findOne({ 'username': req.body.username }, (err, user) => {
    if (err) {
      log.error(err);
      res.status(500).json({
        err: 'SERVER_ERROR',
        message: COMM_ERR.SERVER_ERROR
      });
      return;
    }

    if (!user) {
      res.status(500).json({
        err: 'USER_NOT_EXIST',
        message: AUTH_ERR.USER_NOT_EXIST
      });
      return;
    }

    user.changePassword(req.body.oldpassword, req.body.newpassword, (err, value) => {
      if (err) {
        log.error(err);
        res.status(401).json({
          err: 'AUTHENTICATE_FAILURE',
          message: err.message
        });
        return;
      }

      log.info(`${req.body.username} change password successful`);
      res.json({
        username: req.body.username,
        message: 'change password successful'
      });

    });
  });
});

/** * @api {get} /v1/auth/logout User login out * @apiName UserLogout * @apiGroup userAuthentication * * @apiSuccess {String} username The username of the user. * @apiSuccess {string} message The message if user login out successful. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "test", * "message": "logout successful" * } * * @apiError NOT_LOGIN There is no user logon in. * * @apiErrorExample Error-Response: * HTTP/1.1 401 Unauthorized * { * "err": "NOT_LOGIN", * "message": "No user has been logon" * } */
router.get('/logout', function(req, res) {
  const user = req.user;
  if (!user) {
    res.status(401).json({
      err: 'NOT_LOGIN',
      message: 'No user has been logon'
    });
    return;
  }

  // user login out
  req.logout();
  if (!req.user) {
    res.json({
      username: user.username,
      message: 'logout successful'
    });

    log.info(`${user.username} has been logon out`);
    return;
  }

  res.status(500).json({
    err: 'SERVER_ERROR',
    message: 'logout failure!'
  });

});


function isAhenticated(req, res, next) {
  User.findOne({ 'username': req.body.username }, (err, user) => {
    if (err) {
      log.error(err);
      res.json({
        err: 'SERVER_ERROR',
        message: COMM_ERR.SERVER_ERROR
      });
      return;
    }
    // If user is not existed
    if (!user) {
      res.json({
        err: 'USER_NOT_EXIST',
        message: AUTH_ERR.USER_NOT_EXIST
      });

      return;
    }


    user.authenticate(req.body.password, (err, value) => {
      if (err) {
        log.error(err);
        res.json({
          err: 'SERVER_ERROR',
          message: COMM_ERR.SERVER_ERROR
        });
      } else if (value) {
        return next();
      } else {
        res.json({
          err: 'AUTHENTICATE_FAILURE',
          message: AUTH_ERR.AUTHENTICATE_FAILURE
        });
      }
    });
  });
}


module.exports = router;

複製代碼

這是一套常見的有關用戶登陸註冊驗證的接口github

由於文本只涉及到這個模塊,因此將這個模塊的接口都寫在userAuthentication測試套件下web

describe('userAuthentication', function() {}
複製代碼

測試代碼

'use strict';

const request = require('supertest');
const url = 'http://localhost:5001';
// eslint-disable-next-line no-unused-vars
const should = require('should');

var userCookie;

// 用戶名密碼
const user = {
  username: 'name',
  password: 'password'
};

// 測試更改密碼(每次測試完調換)
const user2 = {
  username: 'uu2',
  password: 'oldpassword'
};

const newUser2 = {
  username: 'uu2',
  oldpassword: 'oldpassword',
  newpassword: 'newpassword'
};

// const user22={
// username: 'uu2',
// password: 'newpassword'
// };
// const oldUser2 = {
// username: 'uu2',
// oldpassword: 'newpassword',
// newpassword: 'oldpassword'
// };

describe('userAuthentication', function() {
  // 測試註冊接口
  describe('UserRegister', function() {
    describe('POST /register', function() {
      // eslint-disable-next-line max-len
      it('register success', function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'User registered successful'
            });
            if (err) throw err;
            done();
          });
      });
      it('repeated registration failure.', function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user)
          .expect(500)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'REGISTER_FAILURE'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
  // 測試登陸接口
  describe('UserLogin', function() {
    describe('POST /login', function() {
      it('login success', function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'Authentication Success'
            });
            if (err) throw err;
            done();
          });
      });
      it('USER_NOT_EXIST.', function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send({
            username: 'a',
            password: 'admin'
          })
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'USER_NOT_EXIST'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
  // 權限驗證
  describe('UserAuthInfo', function() {
    describe('GET /api/v1/auth/', function() {
      // 沒有登陸,權限驗證
      it('The current User was not login.', function(done) {
        request(url)
          .get('/api/v1/auth/')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 權限驗證前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('The username of the current user.', function(done) {
        request(url)
          .get('/api/v1/auth/')
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.have.keys('username');
            if (err) throw err;
            done();
          });
      });
    });
  });
  // 測試用戶註銷接口
  describe('UserLogout', function() {
    describe('GET /logout', function() {
      // 沒有登陸,測試註銷
      it('NOT_LOGIN.', function(done) {
        request(url)
          .get('/api/v1/auth/logout')
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 註銷成功前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('logout successful.', function(done) {
        request(url)
          .get('/api/v1/auth/logout')
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'logout successful'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
  // 測試更改用戶密碼接口
  describe('UserChangePassword', function() {
    describe('POST /changepassword', function() {
      // 更改用戶密碼前先註冊-登陸
      // eslint-disable-next-line no-undef
      before(function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user2)
          .end(function(err, res) {
            if (err) throw err;
            done();
          });
      });
      // eslint-disable-next-line no-undef
      before(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user2)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('change password successful', function(done) {
        request(url)
          .post('/api/v1/auth/changepassword')
          .set('Cookie', userCookie)
          .send(newUser2)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'change password successful'
            });
            if (err) throw err;
            done();
          });
      });
      it('AUTHENTICATE_FAILURE', function(done) {
        request(url)
          .post('/api/v1/auth/changepassword')
          .set('Cookie', userCookie)
          .send(newUser2)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'AUTHENTICATE_FAILURE'
            });
            if (err) throw err;
            done();
          });
      });
      // after(function(done) {
      // request(url)
      // .post('/api/v1/auth/login')
      // .send(user22)
      // .set('Accept', 'application/json')
      // .end(function(err, res) {
      // if (!err) {
      // userCookie = res.header['set-cookie'];
      // done();
      // }
      // });
      // });
      // after(function(done) {
      // request(url)
      // .post('/api/v1/auth/changepassword')
      // .set('Cookie', userCookie)
      // .send(oldUser2)
      // .expect(200)
      // .end(function(err, res) {
      // res.body.should.containEql({
      // message: 'rechange password successful'
      // });
      // if (err) throw err;
      // done();
      // });
      // });
    });
  });
  // 測試刪除用戶接口
  describe('UserDelete', function() {
    describe('DELETE /user/:username', function() {
      it('NOT_LOGIN.', function(done) {
        request(url)
          .delete(`/api/v1/auth/user/${user.username}`)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 刪除用戶前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('delete user success', function(done) {
        request(url)
          .delete(`/api/v1/auth/user/${user.username}`)
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'Delete User Successful'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
});
複製代碼

測試前的準備

測試框架

所謂"測試框架",就是運行測試的工具。經過它,能夠爲JavaScript應用添加測試,從而保證代碼的質量。

一般應用會有單元測試(Unit tests)和功能測試(Functional tests),複雜大型應用可能會有整合測試(Integration tests)。

  1. 單元測試:關注應用中每一個零部件的正常運轉,防止後續修改影響以前的組件。
  2. 功能測試:確保其總體表現符合預期,關注可否讓用戶正常使用。
  3. 整合測試:確保單獨運行正常的零部件整合到一塊兒以後依然能正常運行。

開發人員主要是集中單元測試,做爲開發中的反饋。

單元測試的好處:

  1. 若是能經過單元測試,那麼經過後續測試且軟件總體正常運行的機率大大提升。
  2. 單元測試發現的問題定位到細節,容易修改,節省時間。
  3. 追蹤問題變得更加方便。

選擇單元測試框架

單元測試應該:簡單,快速執行,清晰的錯誤報告。 測試框架基本上都作了同一件事兒:

  1. 描述你要測試的東西
  2. 對其進行測試
  3. 判斷是否符合預期

選擇框架會考慮下面的點:

  1. 斷言(Assertions):用於判斷結果是否符合預期。有些框架須要單獨的斷言庫。
  2. 適合 TDD / BDD:是否適合 測試驅動型 / 行爲驅動型 的測試風格。
  3. 異步測試:有些框架對異步測試支持良好。
  4. 使用的語言:大部分 js 測試框架使用 js。
  5. 用於特定目的:每一個框架可能會擅長處理不一樣的問題。
  6. 社區是否活躍。 注:
  7. TDD:測試驅動型的開發方式,先寫測試代碼,以後編寫能經過測試的業務代碼,能夠不斷的在能經過測試的狀況下重構。
  8. BDD:與 TDD 很類似,測試代碼的風格是預期結果,更關注功能,看起來像需求文檔。

其實都是先寫測試代碼,感受BDD 風格更人性。

測試工具的類型

組合使用工具很常見,即便已選框架也能實現相似的功能

  1. 提供測試框架(Mocha, Jasmine, Jest, Cucumber)
  2. 提供斷言(Chai, Jasmine, Jest, Unexpected)
  3. 生成,展現測試結果(Mocha, Jasmine, Jest, Karma)
  4. 快照測試(Jest, Ava)
  5. 提供仿真(Sinon, Jasmine, enzyme, Jest, testdouble)
  6. 生成測試覆蓋率報告(Istanbul, Jest, Blanket)
  7. 提供類瀏覽器環境(Protractor, Nightwatch, Phantom, Casper)

解釋上面提到的點:

  1. 測試框架,即組織你的測試,當前流行 BDD 的測試結構。
  2. 快照測試(snapshot testing),測試 UI 或數據結構是否和以前徹底一致,一般 UI 測試不在單元測試中
  3. 仿真(mocks, spies, and stubs):獲取方法的調用信息,模擬方法,模塊,甚至服務器

Jest/Jasmine/Mocha框架特色

  • Jest

    • facebook 坐莊
    • 基於 Jasmine 至今已經作了大量修改添加了不少特性
    • 開箱即用配置少,API簡單
    • 支持斷言和仿真
    • 支持快照測試
    • 在隔離環境下測試
    • 互動模式選擇要測試的模塊
    • 優雅的測試覆蓋率報告,基於Istanbul
    • 智能並行測試(參考)
    • 較新,社區不十分紅熟
    • 全局環境,好比 describe 不須要引入直接用
    • 較多用於 React 項目(但普遍支持各類項目)
  • Jasmine

    • 開箱即用(支持斷言和仿真)
    • 全局環境
    • 比較'老',坑基本都有人踩過了
    • AVA
    • 異步,性能好
    • 簡約,清晰
    • 快照測試和斷言須要三方支持
    • Tape
    • 體積最小,只提供最關鍵的東西
    • 對比其餘框架,只提供最底層的 API
  • Mocha

    • 靈活(不包括斷言和仿真,本身選對應工具)
    • 流行的選擇:chai,sinon
    • 社區成熟用的人多,測試各類東西社區都有示例
    • 須要較多配置
    • 可使用快照測試,但依然須要額外配置

綜上所述,Mocha 用的人最多,社區最成熟,靈活,可配置性強易拓展,Jest 開箱即用,裏邊啥都有提供全面的方案,Tape 最精簡,提供最基礎的東西最底層的API。因此本文就選擇用mocha。

mocha特徵

  1. 瀏覽器支持
  2. 全局變量泄漏檢測
  3. 簡單異步支持,包括promise
  4. 能夠選擇運行與regexp匹配的測試
  5. 測試覆蓋率報告
  6. 自動退出以防止活動循環「掛起」
  7. 字符串差別支持
  8. 易於元生成套件和測試用例
  9. 用於運行測試的javascript API
  10. 配置文件支持
  11. CI支持等的正確退出狀態
  12. mocha.opts文件支持
  13. 自動檢測和禁用非tty的着色
  14. 可單擊套件標題以篩選測試執行
  15. 將未捕獲的異常映射到正確的測試用例
  16. 節點調試器支持
  17. 異步測試超時支持
  18. 檢測多個要完成的調用
  19. 測試重試支持
  20. 測試特定超時
  21. 咆哮支持
  22. 報告測試持續時間
  23. 突出顯示慢速測試
  24. 文件監視程序支持
  25. 使用所需的任何斷言庫
  26. extensible reporting, bundled with 9+ reporters
  27. 可擴展測試DSL或「接口」
  28. 每一個鉤子以前、以後、全部鉤子以前、以後
  29. 任意蒸騰器支持(coffee-script 等)
  30. TextMate bundle

斷言庫should

Mocha自己是不包含斷言庫的,因此咱們須要本身選擇斷言庫。should是一個很簡單的、貼近天然語言的斷言庫。固然,Mocha是適配全部的斷言庫的,若是你喜歡其餘的斷言庫好比expect之類的,你也能夠把它包含進來使用。

http測試模塊SuperTest

單單使用Mocha和should就幾乎能夠知足全部JavaScript函數的單元測試。可是對於Node應用而言,不只僅是函數的集合,好比一個web應用的測試。這時候就須要配合一個http代理,完成Http請求和路由的測試。 Supertest是一個HTTP代理服務引擎,能夠模擬一切HTTP請求行爲。Supertest能夠搭配任意的應用框架,從而進行應用的單元測試。

測試套件describe

describe塊稱爲"測試套件"(test suite),表示一組相關的測試。它是一個函數,第一個參數是測試套件的名稱("加法函數的測試"),第二個參數是一個實際執行的函數。

測試用例it

it塊稱爲"測試用例"(test case),表示一個單獨的測試,是測試的最小單位。它也是一個函數,第一個參數是測試用例的名稱("1 加 1 應該等於 2"),第二個參數是一個實際執行的函數。

鉤子hooks

Mocha在describe塊之中,提供測試用例的四個鉤子:before()、after()、beforeEach()和afterEach()。它們會在指定時間執行。

describe('hooks', function() {

  before(function() {
    // 在本區塊的全部測試用例以前執行
  });

  after(function() {
    // 在本區塊的全部測試用例以後執行
  });

  beforeEach(function() {
    // 在本區塊的每一個測試用例以前執行
  });

  afterEach(function() {
    // 在本區塊的每一個測試用例以後執行
  });

  // test cases
});
複製代碼

安裝mocha> = v3.0.0,npm的版本應該> = v2.14.2。除此,確保使用Node.js的版本> = v4來運行mocha

mocha小例子

安裝

做爲項目的依賴進行安裝

npm install --save-dev mocha
複製代碼

開始

mkdir test
cd test
touch test.js
複製代碼

加入測試代碼

'use strict'
var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return 0 when the value is not present', function() {
      assert.equal([1, 2, 3].indexOf(1), 1);
    });
  });
});
複製代碼

安裝依賴

npm install --save-dev assert
複製代碼

執行測試

./node_modules/mocha/bin/mocha
複製代碼

報錯結果

Array
    #indexOf()
      1) should return 0 when the value is not present


  0 passing (5ms)
  1 failing

  1) Array
       #indexOf()
         should return 0 when the value is not present:

      AssertionError [ERR_ASSERTION]: 0 == 1
      + expected - actual

      -0
      +1
      
      at Context.<anonymous> (test/test.js:6:14)

複製代碼

package.json中寫入命令

"mocha": "mocha"
複製代碼

執行命令

npm run mocha
複製代碼

正確測試

'use strict'
var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return 0 when the value is not present', function() {
      assert.equal([1, 2, 3].indexOf(1), 0);
    });
  });
});
複製代碼

正確結果

Array
    #indexOf()
      ✓ should return 0 when the value is not present


  1 passing (4ms)
複製代碼

到這裏,對mocha就有了初步的認識

開始測試

瞭解了背景和框架後,正式開啓測試

添加依賴

npm install --save-dev mocha mochawesome should supertest
複製代碼

在scripts中添加命令

"mochawesome": "./node_modules/.bin/mocha --reporter mochawesome",
"dev": "node index.js"
複製代碼

mochawesome生成報告

dev啓動項目

註冊接口的測試

  1. 從註冊接口中,我得知該接口返回兩個狀態碼,分別是200和500,對應的註冊成功和註冊失敗
  2. 那麼測試中就有兩個註冊成功和失敗的測試用例
  3. 每一個測試用例針對每一個狀態返回的值判斷
  4. 經過便可
  5. 不經過,要麼是接口有問題,要麼是寫的測試有問題
/** * @api {post} /v1/auth/register User Register * @apiName UserRegister * @apiGroup userAuthentication * * @apiParam {String} username New user's name. * @apiParam {String} password New user's password. * * @apiSuccess {String} username The username of the register user. * @apiSuccess {string} message The registering success info. * * @apiSuccessExample Success-Response: * HTTP/1.1 200 OK * { * "username": "gushen", * "message": "User registered successful" * } * * @apiError REGISTER_FAILURE The register failure. * * @apiErrorExample Error-Response: * HTTP/1.1 500 Internal Server Error * { * "err": "REGISTER_FAILURE", * "message": "User register failure!" * } */
router.post('/register', function(req, res, next) {
  User.register(new User({ username: req.body.username }), req.body.password, function(err) {
    if (err) {
      log.error(err);
      res.status(500).json({
        err: 'REGISTER_FAILURE',
        message: AUTH_ERR.REGISTER_FAILURE
      });
      return;
    }

    log.info('user ' + req.body.username + ' registered successful!');
    res.json({
      username: req.body.username,
      message: 'User registered successful'
    });

  });
});

複製代碼
  1. UserRegister是第二層套件
  2. POST /register是第三層套件
  3. register success是測試用例名稱
  4. it的方法是測試用例的方法
  5. 請求url地址
  6. 發送post請求
  7. 發送用戶名和密碼
  8. 斷言狀態碼
  9. 斷言返回的值
  10. 執行下一步

注意: 每一個測試用例結束後必須帶上done,不然沒有結束標識,會超時報錯

// 測試註冊接口
  describe('UserRegister', function() {
    describe('POST /register', function() {
      // eslint-disable-next-line max-len
      it('register success', function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'User registered successful'
            });
            if (err) throw err;
            done();
          });
      });
      it('repeated registration failure.', function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user)
          .expect(500)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'REGISTER_FAILURE'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
複製代碼

登陸接口的測試

沒什麼好講的,同測試註冊接口步驟一致

describe('UserLogin', function() {
    describe('POST /login', function() {
      it('login success', function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'Authentication Success'
            });
            if (err) throw err;
            done();
          });
      });
      it('USER_NOT_EXIST.', function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send({
            username: 'a',
            password: 'admin'
          })
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'USER_NOT_EXIST'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
複製代碼

權限驗證的測試

  1. 權限驗證就有點不同了,由於驗證權限前必須先登陸,這時候就要用上mocha的鉤子
  2. 權限是經過cookie驗證,因此驗證前必需要帶上cookie
  3. before鉤子中加入
userCookie = res.header['set-cookie'];
複製代碼
  1. 在斷言的請求中帶上Cookie
.set('Cookie', userCookie)
複製代碼
// 權限驗證
  describe('UserAuthInfo', function() {
    describe('GET /api/v1/auth/', function() {
      // 沒有登陸,權限驗證
      it('The current User was not login.', function(done) {
        request(url)
          .get('/api/v1/auth/')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 權限驗證前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('The username of the current user.', function(done) {
        request(url)
          .get('/api/v1/auth/')
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.have.keys('username');
            if (err) throw err;
            done();
          });
      });
    });
  });
複製代碼

用戶註銷接口的測試

沒什麼好講的,同測試權限驗證步驟一致

describe('UserLogout', function() {
    describe('GET /logout', function() {
      // 沒有登陸,測試註銷
      it('NOT_LOGIN.', function(done) {
        request(url)
          .get('/api/v1/auth/logout')
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 註銷成功前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('logout successful.', function(done) {
        request(url)
          .get('/api/v1/auth/logout')
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'logout successful'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
複製代碼

更改用戶密碼的測試

更改用戶密碼前先註冊-登陸

// 測試更改用戶密碼接口
  describe('UserChangePassword', function() {
    describe('POST /changepassword', function() {
      // 更改用戶密碼前先註冊-登陸
      // eslint-disable-next-line no-undef
      before(function(done) {
        request(url)
          .post('/api/v1/auth/register')
          .send(user2)
          .end(function(err, res) {
            if (err) throw err;
            done();
          });
      });
      // eslint-disable-next-line no-undef
      before(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user2)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('change password successful', function(done) {
        request(url)
          .post('/api/v1/auth/changepassword')
          .set('Cookie', userCookie)
          .send(newUser2)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'change password successful'
            });
            if (err) throw err;
            done();
          });
      });
      it('AUTHENTICATE_FAILURE', function(done) {
        request(url)
          .post('/api/v1/auth/changepassword')
          .set('Cookie', userCookie)
          .send(newUser2)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'AUTHENTICATE_FAILURE'
            });
            if (err) throw err;
            done();
          });
      });
      // after(function(done) {
      // request(url)
      // .post('/api/v1/auth/login')
      // .send(user22)
      // .set('Accept', 'application/json')
      // .end(function(err, res) {
      // if (!err) {
      // userCookie = res.header['set-cookie'];
      // done();
      // }
      // });
      // });
      // after(function(done) {
      // request(url)
      // .post('/api/v1/auth/changepassword')
      // .set('Cookie', userCookie)
      // .send(oldUser2)
      // .expect(200)
      // .end(function(err, res) {
      // res.body.should.containEql({
      // message: 'rechange password successful'
      // });
      // if (err) throw err;
      // done();
      // });
      // });
    });
  });
複製代碼

問題是我改完後得將密碼改回來,這一步我沒有成功,很奇怪爲何?

目前得每次測試完後將新舊密碼調換,真的很麻煩

刪除用戶的測試

沒什麼好講的,同測試權限驗證步驟一致

describe('UserDelete', function() {
    describe('DELETE /user/:username', function() {
      it('NOT_LOGIN.', function(done) {
        request(url)
          .delete(`/api/v1/auth/user/${user.username}`)
          .expect(401)
          .end(function(err, res) {
            res.body.should.containEql({
              err: 'NOT_LOGIN'
            });
            if (err) throw err;
            done();
          });
      });
      // 刪除用戶前先登陸
      beforeEach(function(done) {
        request(url)
          .post('/api/v1/auth/login')
          .send(user)
          .set('Accept', 'application/json')
          .end(function(err, res) {
            if (!err) {
              userCookie = res.header['set-cookie'];
              done();
            }
          });
      });
      it('delete user success', function(done) {
        request(url)
          .delete(`/api/v1/auth/user/${user.username}`)
          .set('Cookie', userCookie)
          .expect(200)
          .end(function(err, res) {
            res.body.should.containEql({
              message: 'Delete User Successful'
            });
            if (err) throw err;
            done();
          });
      });
    });
  });
複製代碼

未完待續

口渴了,快去喝杯mocha吧

參考文獻

mocha官網

測試框架 Mocha 實例教程

前端測試框架對比(js單元測試框架對比)

mocha 和 supertest

使用mocha給mongoose寫單元測試

nodejs使用mocha進行接口測試

node中使用 mocha + supertest + should 來寫單元測試

【Node開發筆記】單元測試工具Mocha和SuperTest

一步一步搭建react應用-node中使用 mocha + supertest + should 來寫單元測試

mocha + chai + supertest 測試 node server

Should.js

接口自動化 開源框架學習-supertest

supertest

Nodejs單元測試小結

使用mocha進行單元測試

最後,別忘了給這個項目點一個star哦,謝謝支持。

blog

一個學習編程技術的公衆號。天天推送高質量的優秀博文、開源項目、實用工具、面試技巧、編程學習資源等等。目標是作到我的技術與公衆號一塊兒成長。歡迎你們關注,一塊兒進步,走向全棧大佬的修煉之路

相關文章
相關標籤/搜索