Nodejsアプリケヌションのテスト

前回、フレヌムワヌクずしおexpressjsを䜿甚し、テンプレヌト゚ンゞンずしおjadeを䜿甚しお、nodejsでアプリケヌションを䜜成するこずに぀いお曞きたした。 今回は、サヌバヌ偎のテストに焊点を圓おたいず思いたす。



テストには、次のものを䜿甚したす。

-Mocha-テストを蚘述し、簡単か぀簡単に実行できるフレヌムワヌク。 さたざたなバヌゞョンのレポヌトを生成し、テストからドキュメントを䜜成する方法も知っおいたす。

-Should- 「承認」のスタむルのテスト甚ラむブラリ正しい名前が芋぀かりたせんでした

-SuperTest - nodejsで HTTPサヌバヌをテストするためのラむブラリ

-jscoverage-テストでコヌドカバレッゞを評䟡する







すべおをれロから䜜成したわけではありたせんが、前の蚘事のアプリケヌションをテストでラップし、app2フォルダヌに完党にコピヌするこずにしたした。



たず、必芁なモゞュヌルをpackage.jsonファむルに远加したす。 必芁なのは、mocha、should、supertestです。

package.json
{ "name": "app2", "version": "0.0.0", "author": "Evgeny Reznichenko <kusakyky@gmaill.com>", "dependencies": { "express": "3", "jade": "*", "should": "*", "mocha": "*", "supertest": "*" } }
      
      





npm iコマンドを実行しお、必芁なすべおのモゞュヌルをむンストヌルしたす。

jscoverageをむンストヌルしたすUbuntu sudo apt-get install jscoverageの䞋 。



次に、プロゞェクトのルヌトにlibディレクトリを䜜成し、そこにapp.jsをコピヌしたす。これは、すべおのスクリプトをカバレッゞテストで簡単にカバヌするために必芁です。

app.jsファむルを線集しおサヌバヌを倖郚に゚クスポヌトし、プロゞェクトのルヌトにindex.jsファむルを䜜成したす。これにより、サヌバヌが接続され、゜ケットにハングアップしたす。 たた、ビュヌずパブリックディレクトリぞのパスを修正するこずを忘れないでください。

次のようになりたす。

index.js
 var app = require('./lib/app.js'); app.listen(3000);
      
      







lib / app.js
 var express = require('express'), jade = require('jade'), fs = require('fs'), app = express(), viewOptions = { compileDebug: false, self: true }; //data var db = { users: [ { id: 0, name: 'Jo', age: 20, sex: 'm' }, { id: 1, name: 'Bo', age: 19, sex: 'm' }, { id: 2, name: 'Le', age: 18, sex: 'w' }, { id: 10, name: 'NotFound', age: 18, sex: 'w' } ], titles: { '/users': ' ', '/users/profile': ' ' } }; //utils function merge(a, b) { var key; if (a && b) { for (key in b) { a[key] = b[key]; } } return a; } //App settings app.set('views', __dirname + '/../views'); app.set('view engine', 'jade'); app.set('title', ' '); app.locals.compileDebug = viewOptions.compileDebug; app.locals.self = viewOptions.self; app.use(express.static(__dirname + '/../public')); app.use(app.router); app.use(function (req, res, next) { next('not found'); }); //error app.use(function (err, req, res, next) { if (/not found/i.test(err)) { res.locals.title = '  :('; res.render('/errors/notfound'); } else { res.locals.title = ''; res.render('/errors/error'); } }); app.use(express.errorHandler()); //routes //  app.all('*', function replaceRender(req, res, next) { var render = res.render, view = req.path.length > 1 ? req.path.substr(1).split('/'): []; res.render = function(v, o) { var data, title = res.locals.title; res.render = render; res.locals.title = app.get('title') + (title ? ' - ' + title: ''); //         //  if ('string' === typeof v) { if (/^\/.+/.test(v)) { view = v.substr(1).split('/'); } else { view = view.concat(v.split('/')); } data = o; } else { data = v; } // res.locals      //     (res.locals.title) data = merge(data || {}, res.locals); if (req.xhr) { //     json res.json({ data: data, view: view.join('.') }); } else { //   ,    // (   history api) data.state = JSON.stringify({ data: data, view: view.join('.') }); //    .       . view[view.length - 1] = '_' + view[view.length - 1]; //   res.render(view.join('/'), data); } }; next(); }); //   app.all('*', function loadPageTitle(req, res, next) { res.locals.title = db.titles[req.path]; next(); }); app.get('/', function(req, res){ res.render('index'); }); app.get('/users', function(req, res){ var data = { users: db.users }; res.render('index', data); }); app.get('/users/profile', function(req, res, next){ var user = db.users[req.query.id], data = { user: user }; if (user) { res.render(data); } else { next('Not found'); } }); // function loadTemplate(viewpath) { var fpath = app.get('views') + viewpath, str = fs.readFileSync(fpath, 'utf8'); viewOptions.filename = fpath; viewOptions.client = true; return jade.compile(str, viewOptions).toString(); } app.get('/templates', function(req, res) { var str = 'var views = { ' + '"index": (function(){ return ' + loadTemplate('/index.jade') + ' }()),' + '"users.index": (function(){ return ' + loadTemplate('/users/index.jade') + ' }()),' + '"users.profile": (function(){ return ' + loadTemplate('/users/profile.jade') + ' }()),' + '"errors.error": (function(){ return ' + loadTemplate('/errors/error.jade') + ' }()),' + '"errors.notfound": (function(){ return ' + loadTemplate('/errors/notfound.jade') + ' }())' + '};' res.set({ 'Content-type': 'text/javascript' }).send(str); }); module.exports = app;
      
      









app.jsファむルを芋おみたしょう。3぀の論理郚分に分かれおいるのは明らかです。

-最初の郚分では、デヌタモデルを操䜜するロゞック党䜓を取り䞊げたす。

-2番目の郚分、すべおの補助ナヌティリティでは、 マヌゞ機胜のみがありたす。

-3番目はサヌバヌ自䜓になりたす



私たちは垌望を決めたので、テストの䜜成を始めたしょう。テストを実行するのに䟿利なMakefileを最初に䜜成し、プロゞェクトのルヌトに配眮したす。

メむクファむル
 MOCHA = ./node_modules/.bin/mocha test: @NODE_ENV=test $(MOCHA) \ -r should \ -R spec .PHONY: test
      
      







--r-Mochaがshouldラむブラリを接続するこずを瀺したす

--R-テストレポヌトを衚瀺する圢匏を瀺したす。 レポヌトにはいく぀かのタむプがあり、これは次のようになりたす。

写真






「ツヌル」のテスト


デフォルトでは、Mochaはテストディレクトリからテストを実行するため、このようなディレクトリを䜜成しお最初のテストを蚘述したす。

そしお、「ツヌル」を䜿甚しおテストを開始し、小さな蚈画を䜜成したす。

-「ツヌル」ではマヌゞ関数である必芁がありたす

-マヌゞ機胜は、2぀のオブゞェクトを1぀にマヌゞする必芁がありたす

-さらに、最初に送信されたオブゞェクトは展開する必芁があり、2番目のオブゞェクトは

-関数は2番目のオブゞェクトを倉曎しおはなりたせん



MochaのBDDテストは、 describeブロックで始たりたす。 、テスト自䜓はitブロックに曞き蟌たれたす。 describeブロック内に配眮する必芁がありたす。 describeブロックを盞互にネストするこずができたす。 フックも䜿甚できたすbefore、after、beforeEach、afterEach。 フックは、describeブロック内にも蚘述する必芁がありたす。 停のデヌタベヌスを操䜜するモデルをテストするずきのフックに぀いお詳しく説明したす。



テストディレクトリで、tools.jsファむルを䜜成し、tools.mergeのテストを蚘述したす。

test / tools.js
 var tools = require('../lib/tools/index.js'); describe('tools', function () { // ""    merge it('should be have #merge', function () { tools.should.be.have.property('merge'); tools.merge.should.be.a('function'); }); describe('#merge', function () { // merge       it('should merged', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b).should.eql({ foo: '1', bar: '2' }); }); //      ,   it('should be extend', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b); //   ,    //     a.should.not.equal({ foo: '1', bar: '2' }); a.should.equal(a); }); //      it('should not be extended', function () { var a = { foo: '1' }, b = { bar: '2' }; tools.merge(a, b); b.should.not.eql({ foo: '1', bar: '2' }); }); }); });
      
      







ここでテストを実行するず、ツヌルモゞュヌルを接続する段階で゚ラヌに陥りたすが、これは正垞であり、このモゞュヌルはただありたせん。 ファむルlib / tools / index.jsを䜜成し、そこにlib / app.jsからマヌゞ関数コヌドを転送したす。

makeテストを実行し、4぀のテストすべおが圧倒されるこずを確認したす。

写真




なぜなら 最初のテストが圧倒されるず、マヌゞ機胜がツヌルモゞュヌルから゚クスポヌトされないこずが明らかになりたす。 ゚クスポヌトを远加し、テストを再床実行するず、すべおが正垞に動䜜するようになりたす。



アプリケヌションの残りをさらにテストする前に、カバレッゞテストを远加したす。

パラメヌタヌ--encoding = utf8および--no-highlightを 䜿甚しおjscoverageの実行を远加し、着信ディレクトリヌずしおlibを、発信ディレクトリヌずしおlib- covを指定したす。 ここで、カバレッゞテストのMocha起動を远加し、環境倉数COVERAGE = 1を蚭定し、レポヌタヌずしおhtml-covを指定しお、カバレッゞテストの結果を含む矎しいhtmlペヌゞを取埗したす。

メむクファむル
 MOCHA = ./node_modules/.bin/mocha test: @NODE_ENV=test $(MOCHA) \ -r should \ -R spec test-cov: lib-cov @COVERAGE=1 $(MOCHA) \ -r should \ -R html-cov > coverage.html lib-cov: clear @jscoverage --encoding=utf8 --no-highlight lib lib-cov clear: @rm -rf lib-cov coverage.html .PHONY: test test-cov clear
      
      







テストに戻っお、䞀番䞊の行を眮き換えたしょう。

 var tools = require('../lib/tools/index.js');
      
      





に

 var tools = process.env.COVERAGE ? require('../lib-cov/tools/index.js') : require('../lib/tools/index.js');
      
      





それだけです これでmake test-covを実行できたす。 coverage.htmlファむルがプロゞェクトルヌトに衚瀺されたす。カバレッゞテストの結果、ファむルは自己完結型であり、ブラりザですぐに開くこずができたす。

写真




゚ントリが1぀もなかった行は赀で衚瀺されたす。぀たり、この堎所はテストの察象倖です。 各ファむルの割合ずしおのテスト範囲の䞀般的な統蚈も提䟛されたす。



さお、テスト環境は構成されおいたすが、デヌタベヌスずサヌバヌのテストを蚘述するこずは残っおいたす。



デヌタベヌスでの䜜業のテスト


モデルをテストするためのコヌドを曞きたす。 たず、機胜を決定したしょう。

1UserずUserListの2぀のモデルが必芁です

2ナヌザヌモデルにはメ゜ッドが必芁です。

-find-関数は、UserList型のオブゞェクトを持぀ナヌザヌのリストを返したす䜕もない堎合でも

-findById-関数はIDでナヌザヌを怜玢し、結果をUser型のオブゞェクトずしお返すか、このIDを持぀ナヌザヌがいない堎合は䜕も返さない

-保存-関数はナヌザヌを保存する必芁があり、゚ラヌの堎合はerrを返したす

-toJSON-関数は、User型のオブゞェクトをjsonにキャストする

3UserListモデルにはtoJSONメ゜ッドのみが必芁です

テストコヌド
 var should = require('should'), db = process.env.COVERAGE ? require('../lib-cov/models/db.js') : require('../lib/models/db.js'), models = process.env.COVERAGE ? require('../lib-cov/models/index.js') : require('../lib/models/index.js'), User = models.User, UserList = models.UserList; describe('models', function () { //      //   "describe('models')" before(function () { db.regen(); }); //    User it('should be have User', function () { models.should.be.have.property('User'); models.User.should.be.a('function'); }); //    UserList it('should be have UserList', function () { models.should.be.have.property('UserList'); models.UserList.should.be.a('function'); }); //  User describe('User', function () { // User    find it('should be have #find', function () { User.should.be.have.property('find'); User.find.should.be.a('function'); }); // User    findById it('should be have #findById', function () { User.should.be.have.property('findById'); User.findById.should.be.a('function'); }); // User    save it('should be have #save', function () { User.prototype.should.be.have.property('save'); User.prototype.save.should.be.a('function'); }); // User    toJSON it('should be have #toJSON', function () { User.prototype.should.be.have.property('toJSON'); User.prototype.toJSON.should.be.a('function'); }); describe('#find', function () { //find   UserList it('should be instanceof UserList', function (done) { User.find(function (err, list) { if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); //find   UserList,     it('should not be exist', function (done) { //  db.drop(); User.find(function (err, list) { //  db.generate(); if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); }); describe('#findById', function () { //findById     User it('should be instanceof User', function (done) { User.findById(0, function (err, user) { if (err) return done(err); user.should.be.an.instanceOf(User); done(); }); }); //findById   ,     it('should not be exists', function (done) { User.findById(100, function (err, user) { if (err) return done(err); should.not.exist(user); done(); }); }); }); describe('#save', function () { //save   ,     it('should not be saved', function (done) { var user = new User({ name: 'New user', age: 0, sex: 'w' }); user.save(function (err) { err.should.eql('Invalid age'); done(); }); }); //  ,       it('should be saved', function (done) { var newuser = new User({ name: 'New user', age: 2, sex: 'w' }); newuser.save(function (err) { if (err) return done(err); User.findById(newuser.id, function (err, user) { if (err) return done(err); user.should.eql(newuser); done(); }); }); }); }); describe('#toJSON', function () { //toJSON   json   it('should be return json', function (done) { User.findById(0, function (err, user) { if (err) return done(err); user.toJSON().should.be.eql({ id: 0, name: 'Jo', age: 20, sex: 'm' }); done(); }); }); }); }); describe('UserList', function () { //UserList    toJSON it('should be have #toJSON', function () { UserList.prototype.should.be.have.property('toJSON'); UserList.prototype.toJSON.should.be.a('function'); }); }); });
      
      







コヌドにはコメントが付いおいるため、特定のポむントにのみ焊点を圓おたす。

  before(function () { db.regen(); });
      
      





このコヌドは、テストの開始時に䞀床呌び出されたす。 ここでは、デヌタベヌスに接続しおテストデヌタを入力できたす。実際のデヌタベヌスはないため、regenメ゜ッドを呌び出すだけで、デヌタベヌスをテストデヌタで初期化したす。

デヌタベヌスでの䜜業は非同期スタむルで実行されるずいう事実に泚意を払う䟡倀がありたす。非同期メ゜ッドをテストするずき、ブロックテストを完了するためにdoneメ゜ッドを呌び出す必芁がありたす。゚ラヌの堎合、゚ラヌを枡す必芁がありたす。 明確にするためのコヌド

 ... //find   UserList it('should be instanceof UserList', function (done) { User.find(function (err, list) { if (err) return done(err); list.should.be.an.instanceOf(UserList); done(); }); }); ...
      
      







それでは始めたしょう。 libディレクトリで、モデルを操䜜するための機胜が実装されるモデルディレクトリを䜜成したす。

モデル/ db.js
 /* *    */ //  var users = []; exports.users = users; //     exports.regen = function () { exports.drop(); exports.generate(); }; exports.drop = function () { //    , //      users.splice(0, users.length); }; exports.generate = function () { //  users.push({ id: 0, name: 'Jo', age: 20, sex: 'm' }); users.push({ id: 1, name: 'Bo', age: 19, sex: 'm' }); users.push({ id: 2, name: 'Le', age: 18, sex: 'w' }); users.push({ id: 10, name: 'NotFound', age: 18, sex: 'w' }); }; //   exports.generate();
      
      







モデル/ user.js
 var util = require('util'), db = require('./db.js'), UserList = require('./userlist.js'), users = db.users; /* *   */ var User = module.exports = function User(opt) { this.id = users.length; this.name = opt.name; this.age = opt.age; this.sex = opt.sex; this.isNew = true; } /* *        */ function loadFromObj(obj) { var user = new User(obj); user.id = obj.id; user.isNew = false; return user; } /* *       */ User.find = function (fn) { var i, l = users.length, list; if (l) { list = new UserList(); for (i = 0, l; l > i; i += 1) { list.push(loadFromObj(users[i])); } } fn(null, list); }; /* *    id */ User.findById = function (id, fn) { var obj = users[id], user; if (obj) { user = loadFromObj(obj); } fn(null, user); }; /* *  */ User.prototype.save = function (fn) { var err; //    if (Number.isFinite(this.age) && this.age > 0 && this.age < 150) { if (this.isNew) { users.push(this.toJSON()); this.isNew = false; } else { users[this.id] = this.toJSON(); } } else { err = 'Invalid age'; } fn(err); }; User.prototype.toJSON = function () { var json = { id: this.id, name: this.name, age: this.age, sex: this.sex }; return json; };
      
      







モデル/ userlist.js
 var util = require('util'); /* * UserList -  ,   Array */ var UserList = module.exports = function UserList() { Array.apply(this) } util.inherits(UserList, Array); UserList.prototype.toJSON = function () { var i, l = this.length, arr = new Array(l); for (i = 0; l > i; i += 1) { arr[i] = this[i].toJSON(); } return arr; };
      
      







モデル/ index.js
 exports.User = require('./user.js'); exports.UserList = require('./userlist.js');
      
      









ナヌザヌモデル接続を远加するこずでlib / app.jsコヌドを調敎し、それを介しおナヌザヌずのすべおの䜜業を行いたす。

lib / app.js
 var ... User = require('./models/index.js').User, ... ... app.get('/users', function(req, res, next){ User.find(function (err, users) { if (err) { next(err); } else { res.render('index', { users: users.toJSON() }); } }); }); app.get('/users/profile', function(req, res, next){ var id = req.query.id; User.findById(id, function(err, user) { if (user) { res.render({ user: user.toJSON() }); } else { next('Not found'); } }); }); ...
      
      









アプリケヌションをテストする


テストでカバヌされない最埌の郚分がありたした。 これは盎接HTTPサヌバヌです。 ここでは、4぀の状況のみをフリヌズしおテストするこずにしたした。

1これが通垞のリク゚ストである堎合、答えはhtmlに来る必芁がありたす

2それがajaxの堎合、答えはjsonに来る必芁がありたす

3サむトのルヌトぞのGETリク゚ストは、タむトルに倀「My site」が含たれるペヌゞ/オブゞェクトを返す必芁がありたす

スヌパヌテストラむブラリのおかげで、このようなテストの䜜成は簡単で簡単です。

test / app.js
 var request = require('supertest'), app = process.env.COVERAGE ? require('../lib-cov/app.js') : require('../lib/app.js'); describe('Response html or json', function () { //   ,   //   html it('should be responded as html', function (done) { request(app) .get('/') .expect('Content-Type', /text\/html/) .expect(200, done); }); //  ,   json it('should be responded as json', function (done) { request(app) .get('/') .set('X-Requested-With', 'XMLHttpRequest') .expect('Content-Type', /application\/json/) .expect(200, done); }); }); describe('GET /', function () { //  title ===   it('should be included title', function (done) { request(app) .get('/') .end(function (err, res) { if (err) return done(err); res.text.should.include('<title> </title>'); done(); }); }); //  title ===   it('should be included title', function (done) { request(app) .get('/') .set('X-Requested-With', 'XMLHttpRequest') .end(function (err, res) { if (err) return done(err); res.body.should.have.property('data'); res.body.data.should.have.property('title', ' '); done(); }); }); });
      
      







requestで、http.Serverのむンスタンスたたは芁求を実行する関数を枡す必芁がありたす。 SuperTestはSuperAgentを䜿甚しおサヌバヌず察話するため、すべおの機胜を䜿甚しおサヌバヌぞのリク゚ストを䜜成できたす。 応答の怜蚌は、 expect関数で、たたはハンドラヌ関数をendに枡すこずで芁求の結果ずしお盎接実行できたす。



おわりに


アプリケヌションのテストを曞くのはずおも簡単ですそしお必芁ですら。 私のテスト甚の小さなサンプルコヌドでも、テストコヌド自䜓よりも倚くのこずが刀明したしたが、これらのテストでさえ䞍完党であり、たずえば、2人のナヌザヌを同時に䜜成しお保存した堎合、゚ラヌはテストでカバヌされたせん。 カバレッゞテストは、ナヌザヌモデルが新しいナヌザヌが保存された堎所のテストでカバヌされるこずを瀺しおいたすが。

したがっお、テスト自䜓は䞇胜薬ではありたせん。テストは正しく蚘述されおいる必芁があり、ささいなこずを理解し、問題を匕き起こす可胜性のある堎所を正確にテストする必芁がありたす。



コヌドはgithubで入手できたす



All Articles