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

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

JestでReactのテストをする(6) 非同期のテスト

経験を重ねることでテストの書き方にも少しずつ慣れてきたのだが、非同期のテストには未だに苦手意識がある。
今回書いた内容についても、これがベストプラクティスだとは思わない。
実戦でやろうとすると、いろいろと課題が出てくると思う。
ただ、自分なりに少しは取っ掛かりを掴めた気がする。

なお、Promiseを使ってサンプルやテストを書いた。
Promiseについてはこちらを参照。
Promiseによる非同期処理の書き方

テスト対象のコードを書く

まず、テストの対象となるコードを書く。
作るのは、非同期でAPIから情報を取ってきて、それをビューに反映させるコンポーネント
ただ、今回重要なのは非同期ということなので、APIとのやり取りは実際には行わない。

const usersData = [
  {'name':'Bob', 'country':'America'},
  {'name':'Cate', 'country':'England'},
  {'name':'Bob', 'country':'Canada'},
  {'name':'Taro', 'country':'Japan'}
];
function searchUsername(name){
  return new Promise((resolve, reject)=>{
    let result = [];
    usersData.forEach(i=>{
      if(i.name === name){ result.push(i) };
    });
    if(result.length === 0){
      reject(new Error('該当するユーザーは存在しません。'));
    } else {
      resolve(result);
    };
  });
};
export function inquireDatabase(name, callback){
  return searchUsername(name)
    .then(result => {callback(result);})
    .catch(result => {callback(result);});
};

usersDataオブジェクトに、ユーザーの情報が入っている。
そしてその中身を検索して、該当するユーザーの情報を非同期で返すのが、searchUsername()
そしてそれをラップして、指定したコールバック関数を非同期で実行するのが、inquireDatabase()

例えば、inquireDatabase('John', myFunc)とすれば、Johnというユーザーの情報を探し、その結果を引数としてmyFuncを実行してくれる。

これらのコードを利用したReactのコンポーネントが、以下。

export default class SearchBox extends React.Component {
  constructor(props){
    super(props);
    this.state = {result: ''};
  };
  showUsersList(received){
    if(received instanceof Error){
      this.setState({result: received.message});
    } else {
      let array = received.map(obj=>{
        let data = [];
        Object.keys(obj).forEach(elem=>{
          data.push(obj[elem]);
        });
        return data.join(' ');
      });
      this.setState({result: array.join(', ')});
    };
  };
  render(){
    return(
      <div>
        <input type="text" onKeyUp={e=>{
          if(e.keyCode !== 13){ return; };
          inquireDatabase(e.target.value, this.showUsersList.bind(this));
        }}></input>
        <div>{this.state.result}</div>
      </div>
    );
  };
};

テキストボックスのonKeyUpで、inquireDatabase()を呼び出している。
得られた結果を引数にしてshowUsersList()を呼び出し、これにより、画面上の表示が変更される。

f:id:numb_86:20170205160607p:plain:w500

このサンプルではローカルのオブジェクトを検索しているが、実際の開発では、APIに問い合わせて検索を行うイメージ。

テストを書いてみる

inquireDatabase()をテストしていく。
これは非同期で動くので、これまでのような書き方だと、失敗する。

例えば、下記のテストは通らない。

describe('basic', ()=>{
  let mock;
  beforeEach(()=>{ mock = jest.fn(); });
  test('called', ()=>{
    inquireDatabase('Taro', mock);
    expect(mock).toHaveBeenCalled();
  });
});

inquireDatabase()のコールバック関数としてmockを設定しているのだが、mockが実行されたかテストすると、実行されていないと判定される。
これはもちろん、非同期で実行されるからだ。mockが呼び出される前に次の行のexpect()が実行されてしまうため、「mockは実行されていないよ」と判断されてしまう。

このような事態を避け、非同期の関数を上手くテストできるようにすることが、この記事の目的である。

setTimeout()を使う

要はコールバックが呼び出されるまでテストを実行しなければいいのだから、setTimeout()で適当に待機することで、先程のテストは通るようになる。

inquireDatabase('Taro', mock);
setTimeout(()=>{
  expect(mock).toHaveBeenCalled();
}, 1000);

だがこれは、スマートではないだろう。
何秒待てばいいかは事前には分からないし、テストによって異なる。
こういう形ではなく、コールバックが呼び出されたことを検知してテストを実行したほうがいい。
Promiseを使うことで、それが可能になる。

Promiseを使う

以下のように書くと、上手く動いてくれる。

return inquireDatabase('Taro', mock)
  .then(() => expect(mock).toHaveBeenCalled());

inquireDatabase()then()をつなげ、そのなかでexpect()を呼び出している。
こうすることで、コールバックの実行が終わった時点でテストを実行してくれる。

returnも重要である。
これがあるから、expect()は実行される。
もしこのreturnがなかったら、then()の呼び出しを待つこと無くプログラムは進んでいき、そのままテストが終わってしまう。

mockが意図した引数を受け取れていることも、確認できる。

return inquireDatabase('Taro', mock)
  .then(() => expect(mock).toHaveBeenCalledWith([{'name':'Taro', 'country':'Japan'}]));

Promiseオブジェクトでのみ、可能

このようなテストを書けるのは、テスト対象であるinquireDatabase()が、Promiseオブジェクトを返すからである。

export function inquireDatabase(name, callback){
  return searchUsername(name)
    .then(result => {callback(result);})
    .catch(result => {callback(result);});
};

だからこそ、then()をつなげていくことが出来る。
テストを上手くやるためにはテストしやすいコードを書くべき、というのはよく言われるが、それは非同期でも同じということだ。

ここまでのまとめ

ここまで書いたテストコードをまとめたものを、以下に載せておく。

describe('basic', ()=>{
  let mock;
  beforeEach(()=>{ mock = jest.fn(); });
  test('not to be called', ()=>{
    inquireDatabase('Taro', mock);
    expect(mock).not.toHaveBeenCalled();
  });
  test('setTimeout', ()=>{
    inquireDatabase('Taro', mock);
    setTimeout(()=>{
      expect(mock).toHaveBeenCalled();
    }, 1000);
  });
  test('Promise', ()=>{
    return inquireDatabase('Taro', mock)
      .then(() => expect(mock).toHaveBeenCalled());
  });
  test('Promise argument', ()=>{
    return inquireDatabase('Taro', mock)
      .then(() => expect(mock).toHaveBeenCalledWith([{'name':'Taro', 'country':'Japan'}]));
  })
});

複数のexpect()を実行

上記のコードだと、1回のtest()で1度しかexpect()を実行していない。
これを複数回実行することは、もちろん可能である。
方法は2つ。

まず先に、コードを載せておく。

describe('multiple', ()=>{
  let mock;
  beforeEach(()=>{ mock = jest.fn(); });
  test('single', ()=>{
    return inquireDatabase('Taro', mock)
      .then(()=>{
        expect(mock).toHaveBeenCalled();
        expect(mock).not.toHaveBeenCalledTimes(2);
        expect(mock).toHaveBeenCalledWith([{'name':'Taro', 'country':'Japan'}]);
      });
  });
  test('chain', ()=>{
    return inquireDatabase('Sam', mock)
      .then(() => expect(mock).toHaveBeenCalled())
      .then(() => expect(mock).toHaveBeenCalledWith(new Error('該当するユーザーは存在しません。')))
      .then(() => expect(mock).toHaveBeenCalledTimes(1))
      .then(() => inquireDatabase('Cate', mock))
      .then(() => expect(mock).toHaveBeenCalledWith([{'name':'Cate', 'country':'England'}]))
      .then(() => expect(mock).toHaveBeenCalledTimes(2));
  });
  test('combine', ()=>{
    return inquireDatabase('Taro', mock)
      .then(()=>{
        expect(mock).toHaveBeenCalled();
        expect(mock).toHaveBeenCalledTimes(1);
        expect(mock).toHaveBeenCalledWith([{'name':'Taro', 'country':'Japan'}]);
      })
      .then(() => inquireDatabase('Cate', mock))
      .then(() => expect(mock).toHaveBeenCalledTimes(2));
  });
});

1つ目は、単純にthen()のなかで複数のexpect()を実行するということ。難しいことは何もない。
もう一つは、次々にthen()をつなげていき、そのなかでexpect()を実行する方法。これでも問題ない。

両方を組み合わせることも、もちろん可能。

どういう方法でもいいと思うが、then()をつなげていく場合、途中で再び関数を呼び出せるという利点がある。
上記のコードではinquireDatabase('Cate', mock)がそれにあたる。

参考資料

就職活動の際に現役エンジニアの方々から聞いたこと

年末年始を挟んで1ヶ月半くらい就職活動をしていたのだが、そこで、現役のエンジニアの方々と話すことが出来た。

大学生のとき「就職活動は様々な業界の社会人と話せるチャンス!」みたいなことを言われて「うるせえ」としか思えなかったが、今回は実際に、いい機会になった。自分が関心を持っている職種や企業だったから。

忘れないうちに、メモしておく。

いくつか注意点を。

とにかく列挙したが、そのエンジニアの方が普段から思っていることもあれば、何となくその場で思いついたこともあると思う。
だから、重要度などは濃淡がある。
あくまでも会話の流れで出てきたものを拾ってまとめただけなので、そのつもりで。

その場でメモしていたわけではないので、細部が異なる可能性もある。

複数の方が近い意味合いのことを話していた場合は、一つにまとめた。

文脈やニュアンスが様々なので、これだけ見ると矛盾するような内容であっても、必ずしも対立するものではないと思っている。
ここに書いてあるものは全て、前後のコンテクストを抜き取ってしまってあるので。
その会社固有の話なども、抜き取ってある。

それでは、列挙していく。

  • GitHubの使い方は習得しておいて欲しい。
  • コードはGitHubで公開したほうがいい。
  • GitHubに限らず、自分のポートフォリオサイトがあったほうが望ましい。フロントエンドなら特に。
  • 言語はまず1種類をしっかり理解して軸を作ったほうがいい。そうすれば、他の言語も理解できるから。
  • SQLは触りだけでもやっておいたほうがいい。
  • APIキーなどの管理には、環境変数を使うとよい。
  • Herokuを使うと、デプロイは楽である。特にExpressで開発したアプリケーションの場合、ドキュメントも整備されているため使いやすい。
  • Reactを使う場合、1つのファイルには1つのコンポーネントだけ記述したほうがよい。
    • 過去の自分のコードを見てもらった際のフィードバックとして。
  • ReactNativeは十分に実用的で、Reactだけでちゃんとしたアプリを作れる。
  • コードリーディングの訓練としては、OSSを読むのが王道。
    • 特に、同じ機能を持つものを自分で作ってみて、それと比較しながら読むと勉強になる。
  • コードレビューは大切。なので、人からの指摘を受け入れる姿勢は重要。
  • テキストベースでのコミュニケーションが多くなるので、それが円滑に出来ることは重要。
  • 一緒に働いていく上では、お互いに尊敬できること、そして、しっかりとコミュニケーションを取れることが大切。
  • 出来るエンジニアたちは、複数の言語を使えることが多く、特定の言語などに「こだわらない」傾向がある。
  • フロントエンドやJavaScriptを究めるためには、複数の言語もやったほうがいい。そうしてこそ、レベルアップできる。
    • 例えば、Reactが影響を受けたElmという言語。あるいは、関数型言語であるHaskell
    • PureScriptやTypeScriptなどのAltJSもいい。
  • 学習の対象として、Lispもいいと思う。メタプログラミングLispの特徴。Rubyでもそういったことは出来るが、Lispで考え方を理解するのは悪くない。
    • 他言語の学習という会話のなかで「Lispはどうすかね?」と聞いてみた際の返答。その方は私よりもかなり若いのだが、Lispもそれなりに書けるらしい。出来る人は何でも出来る……。
  • 大規模、大人数のときは、TypeScriptを採用する。型が便利というか、問題があった際にコンパイルするとエラーを出してくれるのが便利。
  • 実戦でしか学べないことはある。独学では、自分の想像の範囲内のことしか出来ない。実戦だと、その範疇を越えた条件や状況に遭遇できる。

その他、感想。

「フロントエンドエンジニア」として応募したからだが、JavaScriptだけでなくCSSも重要ということは、複数の会社で言われた。

話を聞いてみると、バックエンドはRubyを使っているところが多かった。

GitHubでどんどん公開したほうがいいというのは、どこでも言われた。
まあ、それはそうだろうな。エンジニアとしての能力をアピールするには実際にコードを見せるのが一番だし、せっかくGitHubという便利なものがあるのだから。

コミュニケーション能力が重要というのは、印象に残った。もちろん、技術力や学習意欲があるのは前提だが。
これは自分もそう思う。
一定以上のクオリティのものは、一人で作り上げることは稀で、何らかの共同作業によって作られる。プルリクエストやコードレビューによって開発が行われる。
そうなってくるとやはり、他者と適切なコミュニケーションが取れることは必須になってくる。
「コミュニケーション能力」というと体育会系、リーダーシップ、飲み会の幹事、みたいなイメージが独り歩きしている感じもするが、もちろんそういうものではない。
とはいえ言語化するのは難しく、だからこそ、「コミュニケーション能力」という言葉が一部の人からは目の敵にされてしまっているのだろうけど。


建前や定型文ではなく本心で「貴重なお話ありがとうございました」という感じである。
誰かとプログラミングの話をしたことなど無かったから。