30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

mochaによる非同期のテストの書き方

テストフレームワークmochaでの、非同期のテストの書き方について。

mochaの導入方法などはこちらを参照。
1年近く前のエントリだが、そんなに間違ったことは書いていないはず。

なぜ非同期のテストには工夫が必要なのか

mochaでは、doneを使うことで、簡単に非同期のテストを書ける。

しかし、doneについて説明する前に、そもそもなぜdoneを使う必要があるのかを説明する。
これは、非同期のテストに工夫が必要である理由の説明でもある。

テストのなかで非同期の関数を使うと、非同期の処理が行われる前にテストが終わってしまう。
非同期の処理を待つことなくテストが終了してしまうため、テストが機能しない。
これが、非同期のテストには工夫が必要な理由である。

fs.readFile()を例に、具体的に説明する。
この関数は指定したファイルを読み込むものだが、非同期で実行される。

const fs = require('fs');

fs.readFile('./hoge.txt', 'utf8', (err, data) => {
  if(err) throw err;
  console.log(data);
})

console.log('finish!');

読み込ませるファイルを用意した上で上記のコードを実行すると、finish!と表示された後に、読み込んだファイルの内容が表示される。
つまり、fs.readFile()の結果を待つことなく、プログラムは進んでいく。

テストでも同じように動くため、問題が発生する。

const assert = require('assert');
const fs = require('fs');

describe('async test', () => {
  it('readFile', () => {
    fs.readFile('./hoge.txt', 'utf8', (err, data) => {
      assert(false);  // 本来はここでエラーとなる
    })
  })
});

assert(false)としているので、本来はここでエラーとなり、テストはパスしない。
だが実際には、このテストコードを実行するとパスしてしまう。
fs.readFile()のコールバックの実行を待つことなくプログラムが進んでいき、assert(false)が呼び出される前にテストが終わってしまうからだ。

これを解決し、テストできるようにするのが、doneである。

const assert = require('assert');
const fs = require('fs');

describe('async test', () => {
  it('readFile', (done) => {
    fs.readFile('./hoge.txt', 'utf8', (err, data) => {
      assert(false);
      done(); // ここでテストが終了する
    })
  })
});

使い方は簡単で、itの仮引数にdoneを渡せばいい。
そうすれば、そのテストコードでdone()が呼ばれるまではテストは終了せず、コールバックが実行され、assert(false)も実行される。

doneを使ったPromiseのテスト

Promiseのテストも、doneによって書くことが出来る。

下記は、引数を足し合わせた結果を非同期で返す関数asyncAdd()のテスト。
先程と同様にdone()が呼ばれるまで待つことで、非同期の結果についてテスト出来る。

const assert = require('assert');

function asyncAdd(a, b) {
  return new Promise((resolve) => {
    resolve(a+b);
  });
};

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 5);
      done();
    });
  })
});

しかし、このコードには一つ問題がある。テストが通らなかった際にdone()が呼ばれず、テストが正常に終了しない。

テストを次のように書き換えてテストが通らないようにした場合、Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.というエラーが発生する。

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 9); // ここで例外が発生するため、Rejectedなpromiseオブジェクトが返される
      done(); // これは呼ばれない
    });
  })
});

これを回避するためには、以下のように書く。

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 9);
    }).then(done, done);
  })
});

テストがパスすれば最後のthen()の第一引数のdone()が、失敗した場合は第二引数のdone()が呼ばれるため、done()が呼ばれないという状況を回避できる。

doneを使わないPromiseのテスト

mochaでは、done()を使わずにPromiseのテストを書くことも出来る。

テストするPromiseオブジェクトをreturnするようにして、そのthen()のなかにアサートを書く、という方法である。

以前書いたJestでの方法と同じだ。
JestでReactのテストをする(6) 非同期のテスト

const assert = require('assert');

function asyncAdd(a, b) {
  return new Promise((resolve) => {
    resolve(a+b);
  });
};

describe('async test', () => {
  it('return Promise', () => {
    let result = 0;
    return asyncAdd(2, 3).then((result) => {
      assert(result === 9);
    });
  })
});

参考資料

GitのTagの使い方

今までGitでタグを使うことはなかったが、勤務先で使っているので、勉強。
Git自体の理解(特に考え方や概念に対する理解)がかなり怪しいので、最初はよく分からなかった。備忘録としてまとめておく。

そもそもタグとは何か

コミットのエイリアス、という認識でいいようだ。
コミットに対して分かりやすい名前を付けたのが、タグ。

つまり、コミットを指定して行う操作は、コミットの代わりにタグを指定してもいい。
例えばgit show コミットIDgit checkout -b ブランチの名前 コミットIDは、コミットIDの部分をタグの名前に置き換えても同じことが出来る。

基本的な操作

タグを打つ

$ git tag タグの名前 コミットID

コミットIDを省略した場合は、現在のブランチの最新のコミットに対して、タグが打たれる。

タグの一覧を見る

$ git tag

タグの内容を確認する

$ git show タグの名前


現在のブランチには存在しないタグであっても、問題なく見れる。
というが、タグには、ブランチという概念がない。
そもそもコミットにはブランチという概念がないのだと思う。
このあたりが、そもそもよく分かっていなかった。
hogeブランチのfugaコミット」が存在するのではなく、fugaコミットが独立して存在しており、それをhogeブランチが持っている、という解釈のほうが正しいのだと思う。正確な仕様はまた違うのだろうけど。

ブランチが関係してくるのは一部のケースだけであり、基本的には、タグを使う際はブランチは意識しなくていいのかもしれない。


タグを削除する

$ git tag -d タグの名前

タグの名前を変更する

$ git tag 新しいタグの名前 古いタグの名前
$ git tag -d 古いタグの名前

一つのコミットに対して複数のタグを打つことが出来る。
なので、変更後のタグを追加し、そのあとで古いタグを削除してしまえば、リネームできる。

タグをリモートリポジトリと共有する

タグは、通常のプッシュやプルでは、共有できない。

タグのプッシュ

以下のコマンドで、当該タグの情報をプッシュできる。

$ git push リモートリポジトリ タグの名前

全てのタグの情報をプッシュする場合はこのコマンド。

$ git push リモートリポジトリ --tags

タグのプル

$ git pull リモートリポジトリ --tags

リモートリポジトリのタグを削除

ローカルリポジトリでタグを削除した状態で$ git push リモートリポジトリ --tagsとしても、削除の情報は共有されない。
以下のコマンドで削除する必要がある。リネームによって古いタグを削除した場合も、当然行う必要がある。

$ git push リモートリポジトリ :削除したタグの名前

GitHubでの表示

他のホスティングサービスでも同様だと思うが、GitHubでコミットを見ると、タグの情報を確認できる。
以下のように、表示される。

コミットID(タグ) GitHubでの表示
1bacc28
8ef5ecf
ae548ed (4.0) 4.0
b8bdadb 4.0
f39a14a (3.0) 4.0 3.0
7bba876 4.0 3.0
0b2392a (2.0) 4.0 … 2.0
de1b3bf 4.0 … 2.0
5c173e8 (1.0) 4.0 … 1.0
dc5d009 4.0 … 1.0
d8df26c 4.0 … 1.0

コミットIDは、上に行くほど新しい。

基本的には上記の表のように表示されるが、正しく表示されないことがある。
試行錯誤した結果、GitHubで設定しているデフォルトブランチ、そのブランチに存在しないコミットについては、タグ情報は表示されない模様。
例えば、0b2392a2.0を打っているコミット)以降のコミットがデフォルトブランチに存在しない場合、それらのコミットのタグ情報は空白となる。
それより前のコミットについては、上記の表と全く同じように表示される。