20190507のJavaScriptに関する記事は27件です。

ゆうちょPay対応の払込取扱票を印刷するWebアプリを作った

はじめに

ゆうちょPayのQRコードを払込取扱票に印刷するWebアプリを作りました。
払込取扱票とはこういうものです。
image.png

ゆうちょPay対応の払込取扱票印刷アプリ
https://yuucho-pay.com/

Chromeでの実行を推奨します。

GW中に作ったせいか、ついテンションが上がってドメインまで取得してしまいました。

ゆうちょPayが始まります

2019年5月8日(水)からゆうちょPayが使えるようになります。
弊社でもゆうちょPayのQRコード決済を導入を検討しているので、先日ゆうちょ銀行の方に説明に来てもらいました。
以下はその時伺った話の抜粋です。(注意:サービス開始前に聞いた話ですので仕様が変わる可能性もあります)

  • ゆうちょPayと紐付けできるのは個人の総合口座のみ(法人の口座ではアカウントを作れない)
  • 請求書払いでQRコード決済ができるのは今のところゆうちょPayだけ
  • インターフェースはCSVファイル(金額などを記載したCSVファイルをアップロードして、QRコード情報を記載したCSVをダウンロードする)
  • 払込取扱票にQRコードを印刷して決済した場合は108円の手数料がかかる
  • その他の紙にQRコードを印刷して決済した場合は3%の手数料がかかる
  • メールにアプリ起動リンクを載せて顧客に送信して決済した場合は108円の手数料がかかる
  • 電算システムのバーコードとゆうちょPayのQRコードは併用できない(将来的に電算システムのバーコードのみでゆうちょPayの決済ができるようになる)
  • QRコードを印刷するのが難しくて導入のネックになっているという加盟店候補が多いらしい

弊社では毎月かなりの枚数の払込取扱票を発行しています。そのため、ゆうちょPayには期待していたのですが、弊社の顧客は大部分が法人なので、そもそもゆうちょPayアカウントを作れないことがわかりました。
それでも個人の口座を使って立て替え払いするシチュエーションはあるかもしれないので、一応利用はするつもりです。
決済あたりの手数料は、基本料金も考慮すると電算システムより安くなるので、法人顧客が利用できないのは本当に残念ですが仕方ありません。

インターフェースがCSVファイルのアップロードとダウンロードのみというのは使い勝手が悪いので、システムを作ったGMOさんには頑張ってAPIを作っていただきたいところです。

それから同じQRコードでも、印刷する紙によって手数料の体系が異なるという仕様は、まともに運用できるとは思えないので、実際にサービスが始まったらなかったことになるのではないかと勝手に予想しています。

QRコードの印刷が難しい・・・?

さて、ここからが本題ですが、説明を伺っているときに「QRコードの印刷が難しい」という話が何度も出てきました。
「別に難しくないですよね?」といったら驚かれましたが、実際Qiitaの読者層だったらQRコードを印刷する仕組みを実装するのは難しくないですよね?
でも社内にITエンジニアのいない会社では本当に難しいらしく、「じゃあ、誰でも簡単にQRコードを払込取扱票に印刷できる仕組みを作ったら需要があるんじゃないか」と思ったのが今回のアプリを作ったきっかけです。
「広告を載せればいい小遣い稼ぎになるかも」とも思いましたが・・・。

実装

現時点のキャプチャです。かなり荒削りです。
image.png

QRコードの表示

QRコードの表示はGoogle Chart APIを使用しています。
使い方はとても簡単で、例えば「1RkT7UiAjn04oxxZIP9gzsr4QOBOuJ9J」というデータをQRコードに変換したい場合は、以下のように指定するだけでQRコードが生成されます。

https://chart.googleapis.com/chart?cht=qr&chs=80x80&chl=1RkT7UiAjn04oxxZIP9gzsr4QOBOuJ9J

印刷位置の調整

こちらの記事を参考にさせていただきました。

paper.cssを使ってブラウザだけで名刺を印刷する

当初はPDFを生成するつもりでしたが、HTMLとCSSだけで精密な印刷位置調整ができることがわかり、よりシンプルに実装することができました。

ホスティング

Netlifyでホスティングしています。
今のところは複雑なことは何もしていません。
完全な静的サイトです。

ソースコードを見ていただければ分かる通り、入力されたデータは保持していないので顧客情報が漏洩する心配もありません。

今後の展望

記事執筆時点ではサービスが始まっていないので、ゆうちょPayが今後発展するかどうかは分かりません。
QRコード決済が乱立する状況を踏まえると、成功する確率は低いと思っています。

それでも今回のアプリには、もう少し手を入れて改良して行くつもりです。
現在構想しているのは以下のような内容です。

  • フレームワークの見直し(JQueryとBootstrap → Vue.jsとBulma)
  • デフォルト値の記憶
  • 外部システムとの連携

現在のシンプルさを損なわない範囲で実装していきたいと考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでinput type="file"で選択した画像をプレビューで出す方法。

おはようございます。かけるです。
今回は、vue.jsとhtmlでinput type="file"で選択した画像をプレビューをだす方法を紹介したいと思います。
2019-05-07 (10).png
こんな感じのドラッグアンドドロップもできるものを作ります。

コード全体

        <div class="imgContent">
          <div class="imagePreview">  
          <img :src="uploadedImage" style="width:100%;" />
             <input type="file" class="file_input" name="photo" @change="onFileChange"  accept="image/*" />
          </div>
        </div>

.imgContent {
   width: 90%;
    max-width: 700px;
    margin:auto;
     margin-bottom:40px;
} 
.imagePreview {
    height:30vh;
    background: rgb(240, 240, 240);
    overflow:hidden;
    border-radius: 10px;
    background-position: center center;
    background-size: cover;
     margin-bottom:30px;
     position: relative;
 }
 .fileUpload {
  text-align: center;
  position: absolute;
  height: 25px;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
  color:rgb(134, 134, 134);
  padding: 20px;
}

input自体をopacity:0;にすることで、あたかもイメージボックスを押して起動しているように見せることができます。これをベースにデザインなど変えてみたら面白いと思います。

    onFileChange(e) {
      let files = e.target.files || e.dataTransfer.files;
      this.createImage(files[0]);
    },
    // アップロードした画像を表示
    createImage(file) {
      let reader = new FileReader();
      reader.onload = (e) => {
        this.uploadedImage = e.target.result;
      };
      reader.readAsDataURL(file);
    }

createImageのFileReader()で画像が読み込まれて、それがonFileChange()で表示される仕組みになっています。

2019-05-07 (11).png
こんな感じで表示されます!!

まとめ

僕は、吉岡里帆が大好きです。
twitterやっているのでよかったら見てください!
読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

axiosのinterceptorsで、リクエストの前処理を共通して行う

axiosを使ってAPIリクエストを送る際に、前処理を行いたいケースがありました。
リクエストを送る前に毎回処理を記載する方法もあるのですが、interceptorsを使用することで綺麗に書けそうだったので調べてみました。

interceptorsとは

interceptは途中で捕まえるといった意味のようです。

You can intercept requests or responses before they are handled by then or catch.

リクエストを送る前にログを出してみる

interceptorsを使用して、毎回リクエストとレスポンスの前にログを出力させてみます。

import axios from 'axios';

axios.interceptors.request.use(request => {
  console.log(request.url)
  return request
})

axios.interceptors.response.use(response => {
  console.log(response.url)
  return response
})


axios.post('/path')
.then(() => {
  console.log('ok')
})
.catch(() => {
  console.log('ng')
})

// /path
// 200
// ok

下記の順番で出力されていることがわかりました。
1. interceptors.requestのログ
2. interceptors.responseのログ
3. then()のログ

実際にはパラメータやパスなどに処理を加える

実際に使う場合には、このようにリクエストを送る前処理を共通化させる場合などに使えると思います。

axios.interceptors.request.use(request => {
  JSON.stringify(request.data)
  return request
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-app で 絶対パス(absolute path) をwebpack に頼らず設定する #react

ReactでAbsolute Pathを設定するときに以前と変わっていたので共有も含めて更新!

過去

.env ファイルを作って以下のように定義していた

.env
NODE_PATH=src

現在

jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

これまで通り開発しようとしたらすぐに分からず1~2時間ほどハマっていた(汗)

しっかりと公式ドキュメント見ていくべきだった。。
スクショ.png

参照先: https://facebook.github.io/create-react-app/docs/importing-a-component#absolute-imports

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Chrome 拡張機能でタイムライン上のツイートを「ばぶ」らせてみた

タイムラインを「ばぶ」らせてみた

Chrome 拡張機能を公開した。アイコンをクリックすることで、タイムライン上のツイートの本文、ユーザー名、日時を「赤ちゃん言葉」に置き換えることができる1

スクリーンショット 2019-05-07 3.55.51.jpg

ponchie.001.jpeg

実際の動作は以下のツイートの動画から確認できる(リリース版とは見た目が多少異なる点に注意)。


5/8追記

github へのリンクはこちらです。

なぜこのような拡張機能を作ったのか

世界平和のため。私見だが、他人のツイートに反感を感じるとき、文章の内容と同じくらい文章の書き方にもイラつくことが多いような気がする。そこで、タイムライン上の全員のツイートを赤ちゃんレベルの見た目にまで戻してしまえば、少なくとも文章から受ける印象は均質化されるのではないか、と考えた。我ながらムチャクチャな理屈である。

(読みにくいことはこの上ないが)全員が赤ちゃん言葉で喋っているタイムラインであれば、文書の印象から受ける単なる「感想」ではなく、文章の内容についての「意見」を持つことができるのではないか?ということである。 「みんな赤ちゃんみたいだなー」と思って終わるかもしれないけど。

もちろん、赤ちゃん化とまでいかなくとも、漢字をひらがな化するだけでもよさそうではあるが、そこは学習のためにもう一手間かけたくなった。単に自然言語処理の一種である形態素解析を用いて遊んでみたかったというのもある。

対象とする読者

  • これから Chrome 拡張機能を開発したい人
  • すでに拡張機能を開発しているが、外部 API との通信で困っている人
  • 手を動かしながら JS の勉強をしたい人
  • 自分

筆者について

非情報系理系院生。研究の関係(数値解析、統計処理)で C/C++/bash を 3 年ほど。C はリファレンスがなくてもおおよそ書けるが、ファイル入出力周辺は怪しい。C++ は研究関連の解析フレームワークを使用するために使っている。それ以外には、python, perl を多少齧った程度。

JavaScript に興味を持ったきっかけ

そもそものきっかけは、機械学習による自然言語処理に興味を持ったこと2。それに関連して学習データを用意するため、Web スクレイピングを勉強し始める。だがスクレピング技術の習得には Web ページの HTML 周りの基礎的な知識が不足していることを感じ、ぼちぼち勉強を始める。

JS もついでに勉強し始めたところ、コーディングの柔軟性や応用範囲の広さに魅力を感じた。また、以前から iOS アプリ開発にも関心があったのだが、敷居の高さを感じ手をつけていなかった。だが、最近は JS でもアプリ開発が可能である3ことを知り、本腰を入れて JS を勉強することに決める。

そこで実際に手を動かしながら学習を進めるため、比較的お手軽そうな Chrome 拡張機能の開発に挑戦することに。この記事にはリリース版から抜き出したコードの一部も掲載した。我ながら読みづらいコードだと思うが、ご容赦ください...。

Chrome 拡張機能とは

Chrome 上で実行できるアプリケーションのようなもの。ブラウザ上でのアクション(ボタンをクリック、ページ上で右クリックなど)に連動してなんらかの処理を実行するようなものが多い。

拡張機能の本体は、機能の説明をまとめた JSON 形式のファイルと、実際の挙動を書いた JS ファイルから構成されている。

  • Manifest File

    • 拡張機能に関する情報を書くファイル。 JSON 形式で書く。
  • Content Script

    • 任意のページで実行できるスクリプト。
    • chrome.*API が一部のものしか用いることができない。
    • ページ内で定義されている変数や関数にアクセスできない。
    • DOM の取得・操作はできる。
  • Background Page

    • バックグラウンドで動くページであり、そこで様々な処理を行うことができる。
    • 常にメモリを占有してしまう。
  • Event Page

    • やはりバックグラウンドで動作するが、必要なときだけ立ち上がって動作が完了すると閉じる。
  • Browser Action / Page Action

    • chrome のツールバーに追加できるボタン。
    • 特定の web ページを対象としないような拡張機能は Browser Action が吉。常にurlバーの右に表示されている
    • ボタンがクリックされたときに用意した html ファイルをポップアップで表示させることができる

manifest file

まずは拡張機能に関する情報をまとめて記述する manifest file の書き方について。manifest file の記述に必要な JSON 形式についてはこちらの記事が参考になった。

manifest.json
{
  "manifest_version": 2,
  "name": "ばぶったー",
  "version": "0.0.4",
  "description": "ワンクリックで Twitter タイムラインを「赤ちゃん言葉」にします。",
  "short_name": "BBT",
  "content_scripts": [
    {
      "matches": [
        "https://twitter.com/*"
      ],
      "js": [ "js/content.js" ],
      "all_frames": true,
      "run_at": "document_end"
    }
  ],
  "icons": {
    "16": "akachan.jpg",
    "48": "akachan.jpg",
    "128": "akachan.jpg"
  },
  "browser_action": {
    "default_icon": {
      "19": "akachan.jpg"
    },
    "default_title": "Bubtter"
  },
  "background": {
    "scripts": [ "js/background.js" ],
    "persistent": false
  },
  "permissions": [
    "tabs",
    "activeTab",
    "background",
    "https://jlp.yahooapis.jp/MAService/V1/*"
  ]
}
  • name, version, description
    • chrome ストアに表示される内容を記述する。
  • content_scripts
    • 実行するスクリプトのパスや、スクリプトが実行されるページの URL などを記述する。
  • icons
    • ストア上や、ブラウザ上の管理画面に表示されるアイコン画像のパスを指定できる。
  • browser_action
    • アドレスバー横のアイコンの画像の指定や、アイコンクリック時にポップアップされるページの html ファイルなどを記述できる(今回は未使用)。
  • background
    • background で実行されるスクリプトを指定する。
  • permissions
    • ユーザーにパーミッションを要求する内容を記述する。今回はブラウザで表示中の tab に対して操作をするので "tabs" を記述している。外部API を使用する場合など、表示しているページと別の URL と通信する場合はここに記述する必要がある。

おおまかな実行内容のイメージ

  1. アイコンのクリックを検知する
  2. タイムライン上のテキスト(ツイート本文、ユーザー名、日時など)を取得する
  3. 形態素解析API に fetch 経由でテキスト内容を送信する
  4. xml 形式で結果(読みがな、品詞など)を読みこむ
  5. 品詞に応じて文字列を操作する
  6. ページ内のテキスト要素に、変更後の文字列を書き込む

以下、順を追って解説していく。

アイコンのクリックを検知する

"background" として指定された js ファイルは、常にバックグラウンドで実行されている。
ここでブラウザ上でボタンがクリックされたことを検知し、メッセージを送信する。

background.js
chrome.browserAction.onClicked.addListener(function(tab){
  chrome.tabs.sendMessage(tab.id, "myClick"); /* 第2引数の中味はなんでもよい */
});

contents_script 側では送信されたメッセージに反応して実行する処理を記述する。

content.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
  if( request == "myClick") {
    makeInfa();
  }
});

具体的な実行内容を書いた関数 makeInfa() の内容は以下の通り。

content.js
function makeInfa() {
  /* クリック時にページタイトルを変更。再度クリックで、ページタイトルの変更を検出してページ再読み込み。 */
  const bubTitle = "ばぶったー";
  let pageTitle = document.title;
  if (pageTitle === bubTitle) {
    document.title = "Twitter";
    chrome.runtime.sendMessage(
      {
        message: "fix it",
      },
      function(resp){
        return;
      }
    );
  } else {
    document.title = bubTitle;
  }

  /* ページ内要素の変更 */
  let darekaTweet = document.getElementsByClassName("TweetTextSize");
  for(let darekaTweetCnt=0;darekaTweetCnt<darekaTweet.length;darekaTweetCnt++){
    doConvert(darekaTweet[darekaTweetCnt]);
  }
  //...
}

content script では表示しているページ内の要素の取得、削除、改変などが可能だ。
Chrome の「要素を検証」(mac では opt + command + I)を使えば、ページ内で操作したい要素の Class 名や Tag 名が確認できる。

ブラウザ操作からの DOM の操作についてはこちら が参考になった。

タイムライン上のテキスト(ツイート本文、ユーザー名、日時など)を取得する

contetnt.js
function doConvert(obj) {
  let set = "";
  /* 一行にまとめてから送信 */
  for (let i=0;i<obj.length;i++){
    if (i===obj.length-1){
      for(let k=0;k<obj[i].innerText.length;k++){
        if(obj[i].innerText[k].match(regMatchExp)){
          set += obj[i].innerText[k];
        }
      }
    } else {
      for(let k=0;k<obj[i].innerText.length;k++){
        if(obj[i].innerText[k].match(regMatchExp)){
          set += obj[i].innerText[k];
        }
      }
      set += separateChar; /* 区切り文字を挟んでおき、あとで分割する */
    }
  }

  /* API との通信のためのメッセージ送信 */
  chrome.runtime.sendMessage(
    {
      message: "do it",
      text: set
    },
    function(response){
      let arr = response.split(separateChar);
      for(let j=0;j<obj.length;j++){
        for (let regCnt=0;regCnt<regExp.length;regCnt++){
          arr[j] = arr[j].replace(regExp[regCnt], resTex[regCnt]);
        }
      }
    }
  );
};

なぜ content script 側で直接 API との通信を行わないのかというと、後述するCORB(クロスオリジン)の問題があるからである。

形態素解析API に fetch 経由でテキスト内容を送信する => xml 形式で結果(読みがな、品詞など)を読みこむ => 品詞に応じて文字列を操作する

background.js
chrome.extension.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.message == "do it") {
      const sentence = request.text; /* content script 側で送った text */

      let lastURL = URL + `?appid=` + APIKEY + `&sentence=` + encodeURI(sentence);
      const method = 'POST';
      const headers = {
        'Accept': 'application/json',
        'Content-Type': `application/x-www-form-urlencoded; charset=utf-8`
      };
      const obj = JSON.stringify({
        appid: APIKEY,
        sentence: sentence,
        results: "ma"
      });
      const body = Object.keys(obj).map((key)=>key+"="+encodeURIComponent(obj[key])).join("&");
      fetch(lastURL,
        {
          method,
          headers,
          body
        }
      ).then(
        function(res){
          if(res.ok){
            return res;
          } else {
            sendResponse("しっぱいでちゅ");
            throw "error";
          }
        }
      ).then(res => res.text()) /* Yahoo! の API の場合は xml 形式で返ってくる */
      .then(function(text){
        let dom = new DOMParser().parseFromString(text, 'text/xml');
        return dom;
      })
      .then(function(dom){
        let responseSent = "";
        let text = dom.querySelectorAll("reading");
        let kind = dom.querySelectorAll("pos");
        let surf = dom.querySelectorAll("surface");

        for(let cnt=0;cnt<text.length;cnt++){
          if(kind[cnt].innerHTML === "助動詞" && cnt === text.length-1){
            responseSent += text[cnt].innerHTML + 'でちゅ。';
          } else if (kind[cnt].innerHTML === "名詞") {
            let matchFlg = 0;
            for(let nameCnt in Object.keys(youjiTxt)){
              if(surf[cnt].innerHTML === Object.keys(youjiTxt)[nameCnt]){
                responseSent += "" + youjiTxt[Object.keys(youjiTxt)[nameCnt]] + "";
                ++matchFlg;
                break;
              }
            }
            if (!matchFlg) responseSent += "" + text[cnt].innerHTML + "";
            if (cnt === text.length-1) responseSent += "でしゅ";
          } else if (kind[cnt].innerHTML === "助詞") {
            responseSent += text[cnt].innerHTML + '、';
          } else if (kind[cnt].innerHTML === "動詞" && cnt === text.length-1) {
            responseSent += text[cnt].innerHTML + 'だっちゃ。';
          } else if (kind[cnt].innerHTML === "形容詞") {
            let bufEnd = text[cnt].innerHTML.slice(-1,0);
            responseSent += text[cnt].innerHTML.slice(0,-1) + 'ー' + bufEnd;
          } else if (kind[cnt].innerHTML === "形容動詞") {
            responseSent += text[cnt].innerHTML + 'でちゅ';
          } else if (kind[cnt].innerHTML === "感動詞") {
            responseSent += text[cnt].innerHTML + '!';
          } else {
            responseSent += text[cnt].innerHTML;
          }
        }
        sendResponse(responseSent);
      });
    } else {
      chrome.tabs.getSelected(null, function(tab){
        let newURL = tab.url
        chrome.tabs.update(tab.id, {url: newURL});
      });
      sendResponse("hoge");
    }
    return true;
  });

Web 上のリソースに JavaScript からアクセスする方法は

  • XHR
  • jQuery.ajax
  • fetch

と変化してきているという。昔の方法もまだ使えるが、現在は fetch の使用が推奨されているらしい。そこで素直に fetch を使用。記述に関してはこちら が参考になった。
fetch で POST する際の記述の仕方でつまづいていたのだが、こちら を読んで解決。

具体的なテキスト処理では Yahoo! JAPAN の日本語形態素解析 API を使用した。詳細はこちら から。今回は使用しなかったが、単語ごとの出現頻度なども使用できるらしい。

結果は xml 形式で受け取るので、うまく必要な情報(読みがな、品詞名)を取り出すのに少々苦労した。JSON 形式にも対応しているのかは不明(すみません...)。

ページ内のテキスト要素に、変更後の文字列を書き込む

content.js 内で background からのレスポンスを受け取ってからの処理である。
舌足らずな文章に見えるよう、文字列に対して置換操作を行う。その後、要素を変更するオブジェクトにテキスト内容を代入した。

幼児語についてはこちら を参考にした。つかれたでしゅ。

content.js
let regMatchExp = /^[-:\/a-zA-Z0-9\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf、。「」【】]+$/;

let regExp = [ /でちゅ/g, /つ/g, /す/g, /ま/g, /ふ/g, /る/g, /おう/g, /いう/g,  /、{2,}/g, /、。/g ];
let resTex = [ "", "ちゅ", "しゅ", "みゃ", "ひゅ", "ゆ", "おー", "ゆー",  "、", "。" ];

chrome.runtime.sendMessage(
    {
      message: "do it",
      text: set
    },
    function(response){
      let arr = response.split(separateChar);
      for(let j=0;j<obj.length;j++){
        for (let regCnt=0;regCnt<regExp.length;regCnt++){
          arr[j] = arr[j].replace(regExp[regCnt], resTex[regCnt]); /* 愚直にテキスト置換 */
        }
      }
    }
  );

クロスオリジン(CORB; Cross-Origin Read Blocking)問題

クロスオリジン問題とは、ある種の攻撃を防ぐために、画像デコーダやJavaScriptエンジンがクロスオリジンのリソースを読み込む前にブロックする仕組みだという。
content script 内にそのまま fetch の内容を書いたところ、この問題にぶつかった。

どうやら fetch で異なるドメインのリソースを使用するには、本来はクライアント側、サーバー側双方で設定する必要があるらしい。今回のように外部APIを使用する場合は如何ともしがたいのではないか...(と思っているのだが、間違っていたらすみません)。

その後も

  • jQuery.ajax() ならクライアント側をいじるだけでなんとかなりそう?
  • XHR なら permissions をいじるだけで API を使っても cross domain 問題で怒られないらしい。参考記事はこちら

など他の方法も検討したが、最終的には

  • fetch でも、contents 側ではなく background 側に記述すれば CORB は起こらない ということを知った。参考記事はこちら

その他の苦労した点

contents script に対応する js ファイルが読み込まれるタイミング

当初はブラウザアクションではなく、ページを読み込むタイミングで content script の内容を実行することを考えていた。
ページの読み込みがすべて終わってからでないと、実行時にツイート本文の取得がうまくいかない(manifest.json では "run_at" で実行タイミングを指定できそうな雰囲気があるが、ここに "document_end" を記述しただけではうまくいかなかった)。

ページ読みこみ時の実行では、以下のようなエラーが表示され Mutation Observer を機能させることができなかった。

Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node’.

対処法としては、こちら を参考にした。

結局、ブラウザアクション(ボタンをクリック)からスクリプトを実行するよう変更したので、上記の内容は実装されていないのだが。

背景画像の変更

「ばぶったー」の名前にふさわしく、「幼児」「赤ちゃん」などでググると表示されるフリー素材画像を背景画像に設定しようと考えていたのだが、2015年7月から twitter の背景画像の変更機能は廃止されたそうだ。残念。

ページ要素の変更を検出して実行

公開したバージョンでは通信量を減らすため実装されていないが、

  • タイムラインを書き換えた後の、新たなツイートの読み込みを検出し、再度書き換えを実行すること
  • 実行中に twitter.com 内の他のページに移動した際にページ上の書き換えた内容を保持すること

も可能である。こちら が参考になった。

リリースについて

全ファイルを圧縮して、chrome デベロッパーダッシュボード にアップロードするだけ。初回だけ $ 5.0 支払う必要がある。

今後の展望

改良したい点、できたらいいなと思う点など。

  • 形態素解析を内部で処理する。現状使用している外部APIは通信量に制約があるので、mecab などを使うとか?
  • ツイート内の画像を「ばぶ」感のあるテイストにして置き換える(手書き風とか)。画像認識系の機械学習ライブラリ案件か?道のりは長い。
  • 何かアイデアがありましたらコメントいただけると幸いです。

  1. ラブリーなアイコンは人に描いてもらいました。スペシャルサンクスです。ただ、画像サイズの指定をミスったので、細かすぎたかも。 

  2. @youwht さんの記事(https://qiita.com/youwht/items/0b204c3575c94fc786b8 )を参考に、Word2Vec を使って新元号を予想したりして遊んでいた。これについてはそのうち記事にまとめたい。 

  3. Facebook や Instagram なども使っている React Native なるものを使えばいけるらしい。次はこれを勉強しようと考えている。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング学習記録80〜詳解JavaScript DOM編〜

今日やったこと

  • ドットインストール「JavaScript基礎文法編」復習
  • ドットインストール「詳解JavaScriptオブジェクト編」復習
  • ドットインストール「詳解JavaScript DOM編」

3倍速で復習しました。
ほとんど理解している状態での復習なので、3倍速でも特に問題なく進められました。

オブジェクト編も2周してみて、ようやく大体理解できるようになりました。

1周目は#23の例外処理の意味がよくわからなかったのですが、2周目で「これは便利だ!」と思いました。

例外処理を行うと、エラーが出ても処理が止まらずに最後まで実行できるようになります。

コードが複雑になればなるほど、途中で実行が止まることがよくあるので、そういった意味でこれは便利だなと思いました。

あと、DOM編を動画視聴のみですが、一通り見てみました。

以前、ドットインストールのタイピングゲームを作ったときにちょっと理解できていなかったところが理解できるようになりました。

やはり最低限の基礎は身につけておくべきですね。

ただ、これでドットインストールのJavaScriptのカリキュラムになるアプリ、ゲームは理解できるようになると思うので、明日からまた1つずつ作っていきたいと思います。

それでは、明日も引き続き頑張ります。

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Promise.allの並列処理数を15行の関数でサクッと実装する

非同期IOであるJavaScriptの非同期処理を扱いやすくするためにPromiseやasync/awaitが使われることが標準となってきました。

非同期処理を複数同時に実行する場合、Promise.allを利用することで同時並行で進め、全てが完了したタイミングで処理が終了してくれます。

多くの場合はPromise.allで事足りるのですが、これが数百・数千といった単位になった場合、一気に外部サイトにリクエストを送ろうものなら大きな問題となりうるので、並列の実行数を規定したい時の対応を紹介します。

外部ライブラリを使わなくても簡単に実装できる

Promise.allの並列数を設定したいニーズは一定数あるようで、有力なPromiseライブラリであるbluebirdにもPromise.mapという機能が用意されているので、そちらを利用すると指定した並列数で処理を実行してくれます。

とはいえ並列数を指定するためだけに比較的大きなライブラリであるbluebirdをインストールするのも気が引けるので、どうせならもっと簡単に実現できるのではないかと考えて実装したところ、TypeScriptでわずか15行で実現できました

const concurrentPromise = async <T>(promises: (() => Promise<T>)[], concurrency: number): Promise<T[]> => {
  const results: T[] = [];
  let currentIndex = 0;

  while (true) {
    const chunks = promises.slice(currentIndex, currentIndex + concurrency);
    if (chunks.length === 0) {
      break;
    }
    Array.prototype.push.apply(results, await Promise.all(chunks.map(c => c())));
    currentIndex += concurrency;
  }
  return results;
};

処理の内容としては並列数分だけ先頭から要素を取り出し、要素の末尾までループで実行し続け、最後まで完了した時点で実行結果を返します。

実行結果の確認

それでは上の関数を使って実際に動かしてみましょう。setTimeoutするだけの関数を配列に入れて実行します。

const timeoutExecution = (time: number): Promise<number> => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(time);
      resolve(time);
    }, time);
  });
};

(async () => {
  const promises = [...Array(10).fill(null)].map((_, i) => timeoutExecution.bind(null, i * 100));
  await concurrentPromise(promises, 5);
})();

実行結果は以下の通りです。

0
100
200
300
400
500
600
700
800
900
1000

文字だけを見ると伝わりませんが、確かに並列処理がされており、一つの並列セットの処理が完了してから次のセットが実行されていました?

引数を取る関数を渡す場合

型定義を見ると引数を取らないPromise型の関数のみ渡せるように見えが、引数を取ることも可能です。

const sum = (x: number, y: number): number => {
  return x + y;
};

const executor = sum.bind(null, 10, 20);
console.log(executor());

このようにbindを通じて引数を設定した変数に代入すれば、引数なしのFunction型に変換できます。

かつてはbindを使うとTypeScriptの型情報を失ってしまっていたのですが、Version 3.2からbindの厳密な型チェックが可能となり、tsconfig.jsonに"strictBindCallApply": trueを追加すれば安全に実行することが可能です。

何よりコード量が少なく、かつある程度汎用的に利用できるので、同様のユースケースの際にぜひ参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Promise.allの並列処理数を規定する関数を15行でサクッと実装する

非同期IOであるJavaScriptの非同期処理を扱いやすくするためにPromiseやasync/awaitが使われることが標準となってきました。

非同期処理を複数同時に実行する場合、Promise.allを利用することで同時並行で進め、全てが完了したタイミングで処理が終了してくれます。

多くの場合はPromise.allで事足りるのですが、これが数百・数千といった単位になった場合、一気に外部サイトにリクエストを送ろうものなら大きな問題となりうるので、並列の実行数を規定したい時の対応を紹介します。

外部ライブラリを使わなくても簡単に実装できる

Promise.allの並列数を設定したいニーズは一定数あるようで、有力なPromiseライブラリであるbluebirdにもPromise.mapという機能が用意されているので、そちらを利用すると指定した並列数で処理を実行してくれます。

とはいえ並列数を指定するためだけに比較的大きなライブラリであるbluebirdをインストールするのも気が引けるので、どうせならもっと簡単に実現できるのではないかと考えて実装したところ、TypeScriptでわずか15行で実現できました

const concurrentPromise = async <T>(promises: (() => Promise<T>)[], concurrency: number): Promise<T[]> => {
  const results: T[] = [];
  let currentIndex = 0;

  while (true) {
    const chunks = promises.slice(currentIndex, currentIndex + concurrency);
    if (chunks.length === 0) {
      break;
    }
    Array.prototype.push.apply(results, await Promise.all(chunks.map(c => c())));
    currentIndex += concurrency;
  }
  return results;
};

処理の内容としては並列数分だけ先頭から要素を取り出し、要素の末尾までループで実行し続け、最後まで完了した時点で実行結果を返します。

実行結果の確認

それでは上の関数を使って実際に動かしてみましょう。setTimeoutするだけの関数を配列に入れて実行します。

const timeoutExecution = (time: number): Promise<number> => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(time);
      resolve(time);
    }, time);
  });
};

(async () => {
  const promises = [...Array(10).fill(null)].map((_, i) => timeoutExecution.bind(null, i * 100));
  await concurrentPromise(promises, 5);
})();

実行結果は以下の通りです。

0
100
200
300
400
500
600
700
800
900

文字だけを見ると伝わりませんが、確かに並列処理がされており、一つの並列セットの処理が完了してから次のセットが実行されていました?

引数を取る関数を渡す場合

型定義を見ると引数を取らないPromise型の関数のみ渡せるように見えが、引数を取ることも可能です。

const sum = (x: number, y: number): number => {
  return x + y;
};

const executor = sum.bind(null, 10, 20);
console.log(executor()); // --> 30

このようにbindを通じて引数を設定した変数に代入すれば、引数なしのFunction型に変換できます。

かつてはbindを使うとTypeScriptの型情報を失ってしまっていたのですが、Version 3.2からbindの型チェックが可能となり、tsconfig.jsonに"strictBindCallApply": trueを追加すれば型チェックを行ってくれます。(ただしnullableのチェックはまだ対応していないようです)

何よりコード量が少なく、かつある程度汎用的に利用できるので、同様のユースケースの際にぜひ参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Typeormで環境ごとに接続するDBを切り分ける

目的

  • Typeormを使って、環境ごとに接続するDBを切り分けます。
  • もっといい方法があると思っているので、もしあれば教えていただきたいです。

環境

  • Node: v10.15.2
    • nodeenvを利用し、プロジェクトごとにバージョンを指定することがおすすめです。

利用するパッケージ

typeorm

使用方法

1. packageをinstall

今回はyarnでinstallします。

$ yarn add --dev typeorm

2. 設定ファイルを作成(任意)

環境ごとに orconfig.{環境名}.json を作成します。

  • orconfig.develop.json


{
  "type": "mysql",
  "host": "YOUR_DB_HOST",
  "port": 3306,
  "username": "YOUR_DB_USERNAME",
  "password": "YOUR_DB_PASSWORD",
  "database":"YOUR_DB_PASSWORD",
    // migrationfileで世代管理するためには、ここをfalseにする必要があります。
    "synchronize": false,
    "logging": false,
    "entities": [
      "src/entities/*.ts"
    ],
    "migrations": [
      "src/migrations/*.ts"
    ],
    "subscribers": [
      "src/subscribers/*.ts"
    ],
    "cli": {
      "entitiesDir": "src/entities",
      "migrationsDir": "src/migrations",
      "subscribersDir": "src/subscribers"
    }
}

  • orconfig.production.json


{
  "type": "mysql",
  "host": "YOUR_DB_HOST",
  "port": 3306,
  "username": "YOUR_DB_USERNAME",
  "password": "YOUR_DB_PASSWORD",
  "database":"YOUR_DB_PASSWORD",
    // migrationfileで世代管理するためには、ここをfalseにする必要があります。
    "synchronize": false,
    "logging": false,
    "entities": [
      "src/entities/*.ts"
    ],
    "migrations": [
      "src/migrations/*.ts"
    ],
    "subscribers": [
      "src/subscribers/*.ts"
    ],
    "cli": {
      "entitiesDir": "src/entities",
      "migrationsDir": "src/migrations",
      "subscribersDir": "src/subscribers"
    }
}

3. package.jsonの修正(script追記)

環境ごとのyarnスクリプトを実行し、ExpressプロジェクトにENV_SETTINGS環境変数を渡します。

{
  "scripts": {
    "dev": "tsc && ts-node-dev src/index.ts",
    "start:local": "yarn migration:run && tsc && ts-node src/index.ts",
    "start:develop": "yarn migration:run:develop &&tsc && ENV_SETTINGS=\"develop\" ts-node src/index.ts",
    "start:production": "yarn migration:run:production && tsc && ENV_SETTINGS=\"production\" ts-node src/index.ts",
    "//": "use -n option. it defines migration file name. example: yarn migration -n createTableUser",
    "migration": "ts-node ./node_modules/.bin/typeorm migration:generate -f ormconfig.local.json",
    "migration:run": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.local.json",
    "migration:run:develop": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.develop.json",
    "migration:run:production": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.production.json",
    "migration:revert": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.local.json",
    "migration:revert:develop": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.develop.json",
    "migration:revert:production": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.production.json"
  }
}

4. 環境変数によって読み込むconfigファイルを切り分ける

  • index.ts
import {createConnection} from "typeorm";
import App from './app';

// 環境変数にセットされているENV_SETTINGSを格納する
var env: string = (process.env.ENV_SETTINGS) ? process.env.ENV_SETTINGS : "local";
// 環境ごとにconfigファイルを読み込む
export const connectOption = require(`../ormconfig.${env}.json`);

createConnection(connectOption)
  .then(async connection => {
    const app = new App();
    app.start();
  })
  .catch(error => console.log(error));

(補足)やりたかったこと

  • ormconfigの設定ファイルはormconfig.tsのみにして、各値をconfig/{環境}.tsにまとめ、export/importで行いたかったのですが、migration:runの時にエラーが発生し、断念しております。 (https://typeorm.io/#/using-ormconfig)

before

├── config
│   ├── develop.ts
│   ├── production.ts
│   └── undefined.ts
├── ormconfig.develop.json
├── ormconfig.local.json
├── ormconfig.production.json
├── package.json
├── src
│   ├── app.ts
│   ├── index.ts

after

├── config
│   ├── develop.ts
│   ├── production.ts
│   └── undefined.ts
├── ormconfig.ts
├── package.json
├── src
│   ├── app.ts
│   ├── index.ts
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeORMで環境ごとに接続するDBを切り分ける

目的

  • Typeormを使って、環境ごとに接続するDBを切り分けます。
  • 補足のところに書きましたが、もっといい方法があると思っているので、もしあれば教えていただきたいです。

環境

  • Node: v10.15.2
    • nodeenvを利用し、プロジェクトごとにバージョンを指定することがおすすめです。

利用するパッケージ

typeorm

使用方法

1. packageをinstall

今回はyarnでinstallします。

$ yarn add --dev typeorm

2. 設定ファイルを作成(任意)

環境ごとに orconfig.{環境名}.json を作成します。

  • orconfig.develop.json


{
  "type": "mysql",
  "host": "YOUR_DB_HOST",
  "port": 3306,
  "username": "YOUR_DB_USERNAME",
  "password": "YOUR_DB_PASSWORD",
  "database":"YOUR_DB_PASSWORD",
    // migrationfileで世代管理するためには、ここをfalseにする必要があります。
    "synchronize": false,
    "logging": false,
    "entities": [
      "src/entities/*.ts"
    ],
    "migrations": [
      "src/migrations/*.ts"
    ],
    "subscribers": [
      "src/subscribers/*.ts"
    ],
    "cli": {
      "entitiesDir": "src/entities",
      "migrationsDir": "src/migrations",
      "subscribersDir": "src/subscribers"
    }
}

  • orconfig.production.json


{
  "type": "mysql",
  "host": "YOUR_DB_HOST",
  "port": 3306,
  "username": "YOUR_DB_USERNAME",
  "password": "YOUR_DB_PASSWORD",
  "database":"YOUR_DB_PASSWORD",
    // migrationfileで世代管理するためには、ここをfalseにする必要があります。
    "synchronize": false,
    "logging": false,
    "entities": [
      "src/entities/*.ts"
    ],
    "migrations": [
      "src/migrations/*.ts"
    ],
    "subscribers": [
      "src/subscribers/*.ts"
    ],
    "cli": {
      "entitiesDir": "src/entities",
      "migrationsDir": "src/migrations",
      "subscribersDir": "src/subscribers"
    }
}

3. package.jsonの修正(script追記)

環境ごとのyarnスクリプトを実行し、ExpressプロジェクトにENV_SETTINGS環境変数を渡します。

{
  "scripts": {
    "dev": "tsc && ts-node-dev src/index.ts",
    "start:local": "yarn migration:run && tsc && ts-node src/index.ts",
    "start:develop": "yarn migration:run:develop &&tsc && ENV_SETTINGS=\"develop\" ts-node src/index.ts",
    "start:production": "yarn migration:run:production && tsc && ENV_SETTINGS=\"production\" ts-node src/index.ts",
    "//": "use -n option. it defines migration file name. example: yarn migration -n createTableUser",
    "migration": "ts-node ./node_modules/.bin/typeorm migration:generate -f ormconfig.local.json",
    "migration:run": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.local.json",
    "migration:run:develop": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.develop.json",
    "migration:run:production": "ts-node ./node_modules/.bin/typeorm  migration:run -f ormconfig.production.json",
    "migration:revert": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.local.json",
    "migration:revert:develop": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.develop.json",
    "migration:revert:production": "ts-node ./node_modules/.bin/typeorm migration:revert -f ormconfig.production.json"
  }
}

4. 環境変数によって読み込むconfigファイルを切り分ける

  • index.ts
import {createConnection} from "typeorm";
import App from './app';

// 環境変数にセットされているENV_SETTINGSを格納する
var env: string = (process.env.ENV_SETTINGS) ? process.env.ENV_SETTINGS : "local";
// 環境ごとにconfigファイルを読み込む
export const connectOption = require(`../ormconfig.${env}.json`);

createConnection(connectOption)
  .then(async connection => {
    const app = new App();
    app.start();
  })
  .catch(error => console.log(error));

(補足)やりたかったこと

  • ormconfigの設定ファイルはormconfig.tsのみにして、各値をconfig/{環境}.tsにまとめ、export/importで行いたかったのですが、migration:runの時にエラーが発生し、断念しております。 (https://typeorm.io/#/using-ormconfig)

before

├── config
│   ├── develop.ts
│   ├── production.ts
│   └── undefined.ts
├── ormconfig.develop.json
├── ormconfig.local.json
├── ormconfig.production.json
├── package.json
├── src
│   ├── app.ts
│   ├── index.ts

after

├── config
│   ├── develop.ts
│   ├── production.ts
│   └── undefined.ts
├── ormconfig.ts
├── package.json
├── src
│   ├── app.ts
│   ├── index.ts
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

wp-adminでWP APIをJSから扱う

wp-adminからWP APIをJSからコールする方法を試したので覚書。

WP APIをコールする際の注意点

認証が必要なAPIは、X-WP-Nonceをヘッダーで送る必要がある。

wp-api.jsのimport

nonceの生成などをよしなにしてくれるのでimportしておく。
ついでにPOSTリクエストにjqueryを使うのでこちらもいれておく。

add_action('wp_enqueue_scripts', 'add_scripts' );
add_action('admin_enqueue_scripts', 'add_scripts' );

function add_scripts() {
  if ( is_user_logged_in() ) {
    wp_enqueue_script( 'jquery' );
    wp_enqueue_script( 'wp-api' );
    wp_enqueue_script( "custom_js", plugins_url( 'libs/scripts.js', __FILE__ ), array( 'jquery', 'wp-api' ) );
  }
}

libs/scripts.js

リクエスト処理部分。
wp-api.jsを入れていると、window.wpApiSettingsという値がwp-adminでは定義されている様子。
これにWP APIのパスとnonceが入っているので使う。

const settings = window.wpApiSettings
const requestObj = {
    url: settings.root + 'wp/v2/posts',
    method: 'POST',
    beforeSend: function ( xhr ) {
      xhr.setRequestHeader( 'X-WP-Nonce', settings.nonce );
    },
    body: { /* post params */}
}
jQuery.ajax( requestObj )
  .then( result => console.log( result ) )

wp-api.jsをちゃんと使えばjQueryもいらない気はする。
ただ、Backbone.js触ったことがないのでそれはまた時間ができたときにでも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】分割代入(ES2015)

分割代入(Destructuring assignment)

配列の要素/プロパティを個々の変数に代入するために使用します。
ES2015の前と後で比較してみます。

従来の分割代入(ES2015以前)

配列の各値を取得するために、各要素にアクセスして代入する必要がありました。

let data = [1, 2, 4, 8, 16]
let x0 = data[0] //1
let x1 = data[1] //2
let x2 = data[2] //4
let x3 = data[3] //8
let x4 = data[4] //16

分割代入(ES2015)

以下は、従来の分割代入と同じ結果になります。

let data = [1, 2, 4, 8, 16]
let [x0, x1, x2, x3, x4, x5] = data
//各変数の値は上記と同値のため、省略します。

x0にdata[0]の値をいれて、それ以外を1つの変数に配列としていれる方法もあります。

let data = [1, 2, 4, 8, 16]
let [x0, ...others] = data
console.log(x0)     //1
console.log(others) //[2, 4, 8, 16]

このような代入の仕方もあります。

let data = [1, 2, 4, 8, 16]
let [...data2] = data //data2 = [1, 2, 4, 8, 16]

一見、data2にdataの各要素をいれているだけに見えるため
data2 = dataと何が違うのか分かりづらいです。
配列は参照型であるため、data2 = dataとすると、以下のようになります。

let data = [1, 2, 4, 8, 16]
let data2 = data
data[0] = 100
console.log(data2) //[100, 2, 4, 8, 16]

先程の記載で代入すると、dataの要素が変更されてもdata2に影響はありません。

let data = [1, 2, 4, 8, 16]
let [...data2] = data //data2 = [1, 2, 4, 8, 16]
data[0] = 100
console.log(data2) //[1, 2, 4, 8, 16]

分割代入(オブジェクト)

オブジェクトのプロパティを変数にいれることができます。

let profile = {name: "Thomas", age: 23, sex: "man"}
let {name, sex, like = "JavaScript"} = profile

console.log(name) //Thomas
console.log(sex)  //man
console.log(like) //JavaScript
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IE11にてBlueprintのPopoverの背景色がおかしくなる問題の対処

はじめに

タイトルの通り、Popoverのcontentの背景色がなぜか真っ白になったり・ツラついたりする現象に遭遇したので、対処方法を共有します。

原因

IE11とPopoverが展開するcontainerのスタイルの指定が相性が悪いため。
デフォルトでは、containerには transform: translate3d(xxx, xxx, xxx) なスタイルが適用されています。

対処方法

Popoverコンポーネントのmodifiers propsに以下のようなオブジェクトを渡すことにより、この問題を回避できます。
※IE11の場合のみ、falseにするという条件分岐を入れてもいいかと思います

const hoge = {
  computeStyle: {
   gpuAcceleration: false 
  }
}

参考

https://popper.js.org/popper-documentation.html#modifiers..computeStyle.gpuAcceleration

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ストアドプロシージャを jQuery + PHPで呼び出してみた

はじめに

・トランザクション処理をストアドプロシージャにまとめることでソースをすっきりさせたい衝動にかられ、ストアドプロシージャを呼び出してみた。
・いままでクライアント側にSQLをベタ打ちしていてセキュリティーの問題がやばいみたいなので...

開発環境

・サーバー構成
 WindowsServr2012R2
 Apache + SQLServer2012 + PHP
 
・クライアント構成
 JavaScript + jQuery

実装コード

・クライアントからのAPIの呼び出し

classDatabase.js
//=========================================
// Database クラス
// データベースに関する命令をまとめる
//=========================================
const DatabaseAsyncOn = true;
const DatabaseAsyncOff = false;

function Database(accountName, userName, async) {
    this.accountName = "dev"; // accountName;
    this.userName = "dev"; // userName;
    this.async = false; // async;
}
Database.prototype = {
    //=========================================
    // SELECT メソッド(prepareを使用したもの)
    // DBからSQLを使ってデータをGETする
    //=========================================
    getProcedures: function(mappingName, prepare) {
        var returnData = null;
        $.ajax({
            type: "POST",
            url: "../../common/php/getProcedures.php",
            data: {
                "accountName": this.accountName,
                "userName": this.userName,
                "mappingName": mappingName,
                "prepare[]": prepare
            },
            dataType: "json",
            async: this.async,
            // 成功したときの処理
            success: function(data) {
                returnData = data;
                console.log('\n\nmappingName: ' + mappingName);
                console.log('parametter: ' + prepare);
                console.log('data: ' + data);
            },
            // エラー処理
            error: function(data) {
                returnData = false;
                console.log('\n\nmappingName: ' + mappingName);
                console.log('parametter: ' + prepare);
                console.log('data: ' + data);
            }
        });
        return returnData;
    }
};
main.js
    // APIの呼び出し
    // マッピング:200402
    self.midokuMember = self.database.getProcedures(
            'mapping200402',[
                self.companyID,
                eventNo,
                today
            ]
    );

サーバー側
 ストアドプロシージャ呼び出し用のAPI

setProcedures.php
<?php
  $accountName = $_POST["accountName"];
  $userName    = $_POST["userName"];
  $mappingName = $_POST["mappingName"];
  $prepare     = $_POST["prepare"];
  $org_timezone = date_default_timezone_get();
  date_default_timezone_set('Asia/Tokyo');
  $dsn = 'sqlsrv:server=【IPアドレス】;database=' . $accountName;
  $password = '【DB接続パスワード】';
  try {
    $dbh = new PDO($dsn, $userName, $password);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE , PDO::FETCH_ASSOC);
    //////////////////////////////////////////////
    // セット処理
    //////////////////////////////////////////////
    // ステートメント準備
    $stringPrepare = "{CALL ".$mappingName."(";
    foreach ($prepare as $key => $value) {
      # code...
      $stringPrepare = $stringPrepare."?,";
    }
    if (count($prepare) != 0) {
      $stringPrepare = substr($stringPrepare, 0, -1);   // 最後尾の文字を削除する
    }
    $stringPrepare = $stringPrepare.")}";
    error_log($stringPrepare);
    $stmt = $dbh->prepare($stringPrepare);
    for ($i=0; $i < count($prepare); $i++) {
      # code...
      $stmt->bindParam($i+1, $prepare[$i], PDO::PARAM_STR);
    }
    $stmt -> execute();
    echo "\nPDO::errorInfo():\n";
    print_r($dbh->errorInfo());
    $dbh = null;
    date_default_timezone_set($org_timezone);
  }
  catch (PDOException $e) {
    echo 'Error : ',  $e->getMessage(), "\n";
    die('MSSQL Serer Connect Error');
  }
?>

呼び出し元のストアドプロシージャ

mapping200409.sql
USE [dev]
GO
/****** Object:  StoredProcedure [dbo].[mapping200402]    Script Date: 2018/12/26 12:10:09 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[mapping200402]
  @companyID nvarchar(4),
  @eventNo int,
  @today nvarchar(10)
AS
BEGIN TRY
    BEGIN TRANSACTION        --トランザクションの開始
        BEGIN
            -- マッピング2:未読者の取得
            SELECT MK_smartPhone.companyID,
                   MK_smartPhone.staffID,
                   MK_staff.name
            FROM   MK_smartPhone INNER JOIN MK_staff ON 
                   MK_smartPhone.companyID = MK_staff.companyID AND 
                   MK_smartPhone.staffID = MK_staff.staffID LEFT OUTER JOIN
                   MK_history ON 
                   MK_smartPhone.companyID = MK_history.companyID AND 
                   MK_smartPhone.staffID = dbo.MK_history.staffID
            WHERE  (MK_smartPhone.name <> N'card') AND 
                   ((MK_history.category = 2) AND 
                   (MK_history.flag = 0) AND 
                   (MK_history.companyID = @companyID) AND 
                   (MK_history.externNo = @eventNo) AND 
                   (MK_staff.retireDate > @today OR
                    MK_staff.retireDate IS NULL OR
                    MK_staff.retireDate = '') OR
                   (MK_history.flag IS NULL))
            GROUP BY MK_smartPhone.companyID,
                     MK_smartPhone.staffID, 
                     MK_staff.name, 
                     MK_history.externNo
            ORDER BY MK_smartPhone.staffID          
        END
    COMMIT TRANSACTION       --トランザクションを確定
END TRY


--例外処理
BEGIN CATCH
    ROLLBACK TRANSACTION     --トランザクションを取り消し
    PRINT ERROR_MESSAGE()    --エラー内容を戻す
    PRINT 'ROLLBACK TRANSACTION'
END CATCH

RETURN

まとめ

・SQLを別ファイルに分離してソースの読みやすさ管理のしやすさの向上が図れた。
・言語ごとに得意分野があるのでそれぞれ適材適所で使用していきたい。※違う言語が紛れているとソースが読みづらくなる

 下記の様な使い分けがよいと改めて思った。
①JavaScript・・・画面周り、APIの呼び出し(ビジネスロジックは書かない)
②PHP・・・ビジネスロジックだけを書く
③ストアドプロシージャ・・・トランザクションの処理(ビジネスロジックは書かない)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jestで直前にいじったテストファイルの結果だけ見たい

概要

Jestを実行するとテストファイルが全部実行されたりして辿るのが大変ですよね。

そう思って公式ドキュメントを見ていた所いい感じのオプションがあったのでご紹介します。

ざっくり2種類あります。

--watch

Jest CLI Options · Jest

--watch
Watch files for changes and rerun tests related to changed files. If you want to re-run all tests when a file has changed, use the --watchAll option instead.

(Google翻訳)
ファイルの変更を監視し、変更されたファイルに関連するテストを再実行します。ファイルが変更されたときにすべてのテストを再実行したい場合は、代わりに--watchAllオプションを使用してください。

これをつけるとJestが監視状態で待機してくれます。この状態のままテストファイル、またはテスト対象ファイルを弄ると関連するものだけ実行してくれます。

何回も試したい場合はこっちがいいかもですね。

--onlyChanged

Jest CLI Options · Jest

--onlyChanged
別名: -o。 現在のリポジトリでそのファイルに変更があったかに基づいて、どのテストを実行するのかを識別しようとします。 git/hgのリポジトリでテストを実行した場合のみ動作し、静的な依存グラフが必要です (言い換えると動的な依存グラフは必要ありません)。

これをつけるといい感じに直前にいじったやつだけ出力してくれます。いいですね。

ちょっとだけ確認したいときはこっちのほうが早いです。

console.logも見たい場合

jest --silent=false --verbose false --onlyChanged

jestは標準ではconsole.logが出力されないようになってしまっているので、必要であれば--silent=false --verbose falseも付けるのがおすすめです。

もちろん、--watchにこの引数を付けてもOKです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 開発環境構築

はじめに

この記事では、Reactの開発環境構築手順について、説明していきたいと思います。
構築の流れとして

  1. Node.jsのインストール
  2. パッケージマネージャーYarnのインストール
  3. creat-react-appのインストール
  4. 構築した環境でHello Worldを表示させてみる

1. Node.jsのインストール

まずは、下記のURLをクリックします。
https://nodejs.org/ja/

URLを開くとLTS版と最新版とあります。
簡単に説明すると、LTS版(Long Time Supportの略)は長期的サポートが受けれるもの
一方、最新版はサポート期間が短いが、最新の物を利用できるものです。

この記事ではLTS版をダウンロードして進めていきます。

nodeをダウンロードして、インストールできたら、ターミナルで下記のコマンドを入力して、Nodeがインストールしているかを確認します。Nodeのバージョンが表示されていれば大丈夫です。

$ node -v
v10.15.3

2. Yarnをインストール

nodeのパッケージマネージャであるYarnをインストールしていきます。
npmというパッケージマネージャがNodeをインストールした時点でありますが、npmよりもYarnの方がより高速で信頼度の高いものになっているので、Yarnをインストールしていきます。
Yarnをインストールする場合は、ターミナルで下記のコマンドを実行してください。

$npm install --global yarn
/usr/local/bin/yarn -> /usr/local/lib/node_modules/yarn/bin/yarn.js
/usr/local/bin/yarnpkg -> /usr/local/lib/node_modules/yarn/bin/yarn.js
+ yarn@1.15.2
updated 1 package in 1.698s

Yarnがインストールされているかは下記のコマンドを実行して、バージョンが表示されていれば大丈夫です。

$yarn --version
1.15.2

3. creat-react-appのインストール

従来のreactを用いた開発では、Babelやwebpackなど様々なパッケージをマニュアルでインストールする必要がったため、ものすごく手間がかかっていました。ですが、creat-react-appをインストールすることで、これらの問題が解消でき、簡単に必要なパッケージをインストールすることができます。
下記のコマンドを実行することでインストールできます。

$yarn global add creat-react-app

4. 構築した環境でHello Worldを表示させてみる

ターミナルから下記のコマンドを実行して、アプリケーションを作成します。
作成する場所は任意で構いません。

$create-react-app helloworld

下記のようなメッセージが表示されていればOKです。

Initialized a git repository.

Success! Created helloworld at /Users/******/helloworld

作成したプロジェクト配下に移動し、下記のstartコマンドを実行します。

$cd helloworld
$yarn start

実行後、ブラウザが起動し、以下の画面が表示されていればOKです。
スクリーンショット 2019-04-29 12.28.38.png

ここまでできたら、helloworld/src配下にあるApp.jsを開きましょう。
App.jsを開くと以下のようなコードが書かれていると思います。

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

App.jsの内容を全て消して、以下のようにソースを書いてみましょう。

import React from 'react';

class App extends React.Component{
  render()
  {
    return (
    <div>
      <h1>Hello World</h1>
    </div>
    );
  }
}

export default App;

修正してブラウザに以下のように表示されていればOKです。
スクリーンショット 2019-04-29 12.49.03.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでローマ字をカタカナに変換する関数

概要

入力を正規化する必要があったので、ローマ字からカタカナに変換する関数を作成しました。
IMEと似た感じで変換されます(表にないものはそのまま出ます)。

aiueoアイウエオ
hankachiハンカチ
tonari no kyaku ha yoku kakikuu kyakudaトナリ ノ キャク ハ ヨク カキクウ キャクダ
toukyou tokkyo kyokakyokuトウキョウ トッキョ キョカキョク
qawsedrftgyhujikolpqアwセdrftgyフジコlp
高輪Gateway高輪ガテワy

コード

TSで書いているので、JSで使う場合型情報を落としてください。
促音やへの対応は行っています。
抜けや不具合があったらご報告ください。

※ Google IMEで調べたところ、wが連続したときだけ他と違う挙動を示しましたが、面倒だったので他と同じようにただの促音になります。


interface ITree {
  [key: string]: ITree | string;
}

const tree: ITree = {
  a: 'ア', i: 'イ', u: 'ウ', e: 'エ', o: 'オ',
  k: {
    a: 'カ', i: 'キ', u: 'ク', e: 'ケ', o: 'コ',
    y: { a: 'キャ', i: 'キィ', u: 'キュ', e: 'キェ', o: 'キョ' },
  },
  s: {
    a: 'サ', i: 'シ', u: 'ス', e: 'セ', o: 'ソ',
    h: { a: 'シャ', i: 'シ', u: 'シュ', e: 'シェ', o: 'ショ' },
    y: { a: 'キャ', i: 'キィ', u: 'キュ', e: 'キェ', o: 'キョ' },
  },
  t: {
    a: 'タ', i: 'チ', u: 'ツ', e: 'テ', o: 'ト',
    h: { a: 'テャ', i: 'ティ', u: 'テュ', e: 'テェ', o: 'テョ' },
    y: { a: 'チャ', i: 'チィ', u: 'チュ', e: 'チェ', o: 'チョ' },
    s: { a: 'ツァ', i: 'ツィ', u: 'ツ', e: 'ツェ', o: 'ツォ' },
  },
  c: {
    a: 'カ', i: 'シ', u: 'ク', e: 'セ', o: 'コ',
    h: { a: 'チャ', i: 'チ', u: 'チュ', e: 'チェ', o: 'チョ' },
    y: { a: 'チャ', i: 'チィ', u: 'チュ', e: 'チェ', o: 'チョ' },
  },
  n: {
    a: 'ナ', i: 'ニ', u: 'ヌ', e: 'ネ', o: 'ノ', n: 'ン',
    y: { a: 'ニャ', i: 'ニィ', u: 'ニュ', e: 'ニェ', o: 'ニョ' },
  },
  h: {
    a: 'ハ', i: 'ヒ', u: 'フ', e: 'ヘ', o: 'ホ',
    y: { a: 'ヒャ', i: 'ヒィ', u: 'ヒュ', e: 'ヒェ', o: 'ヒョ' },
  },
  f: {
    a: 'ファ', i: 'フィ', u: 'フ', e: 'フェ', o: 'フォ',
    y: { a: 'フャ', u: 'フュ', o: 'フョ' },
  },
  m: {
    a: 'マ', i: 'ミ', u: 'ム', e: 'メ', o: 'モ',
    y: { a: 'ミャ', i: 'ミィ', u: 'ミュ', e: 'ミェ', o: 'ミョ' },
  },
  y: { a: 'ヤ', i: 'イ', u: 'ユ', e: 'イェ', o: 'ヨ' },
  r: {
    a: 'ラ', i: 'リ', u: 'ル', e: 'レ', o: 'ロ',
    y: { a: 'リャ', i: 'リィ', u: 'リュ', e: 'リェ', o: 'リョ' },
  },
  w: { a: 'ワ', i: 'ウィ', u: 'ウ', e: 'ウェ', o: 'ヲ' },
  g: {
    a: 'ガ', i: 'ギ', u: 'グ', e: 'ゲ', o: 'ゴ',
    y: { a: 'ギャ', i: 'ギィ', u: 'ギュ', e: 'ギェ', o: 'ギョ' },
  },
  z: {
    a: 'ザ', i: 'ジ', u: 'ズ', e: 'ゼ', o: 'ゾ',
    y: { a: 'ジャ', i: 'ジィ', u: 'ジュ', e: 'ジェ', o: 'ジョ' },
  },
  j: {
    a: 'ジャ', i: 'ジ', u: 'ジュ', e: 'ジェ', o: 'ジョ',
    y: { a: 'ジャ', i: 'ジィ', u: 'ジュ', e: 'ジェ', o: 'ジョ' },
  },
  d: {
    a: 'ダ', i: 'ヂ', u: 'ヅ', e: 'デ', o: 'ド',
    h: { a: 'デャ', i: 'ディ', u: 'デュ', e: 'デェ', o: 'デョ' },
    y: { a: 'ヂャ', i: 'ヂィ', u: 'ヂュ', e: 'ヂェ', o: 'ヂョ' },
  },
  b: {
    a: 'バ', i: 'ビ', u: 'ブ', e: 'ベ', o: 'ボ',
    y: { a: 'ビャ', i: 'ビィ', u: 'ビュ', e: 'ビェ', o: 'ビョ' },
  },
  v: {
    a: 'ヴァ', i: 'ヴィ', u: 'ヴ', e: 'ヴェ', o: 'ヴォ',
    y: { a: 'ヴャ', i: 'ヴィ', u: 'ヴュ', e: 'ヴェ', o: 'ヴョ' },
  },
  p: {
    a: 'パ', i: 'ピ', u: 'プ', e: 'ペ', o: 'ポ',
    y: { a: 'ピャ', i: 'ピィ', u: 'ピュ', e: 'ピェ', o: 'ピョ' },
  },
  x: {
    a: 'ァ', i: 'ィ', u: 'ゥ', e: 'ェ', o: 'ォ',
    y: {
      a: 'ャ', i: 'ィ', u: 'ュ', e: 'ェ', o: 'ョ',
    },
    t: {
      u: 'ッ',
      s: {
        u: 'ッ',
      },
    },
  },
  l: {
    a: 'ァ', i: 'ィ', u: 'ゥ', e: 'ェ', o: 'ォ',
    y: {
      a: 'ャ', i: 'ィ', u: 'ュ', e: 'ェ', o: 'ョ',
    },
    t: {
      u: 'ッ',
      s: {
        u: 'ッ',
      },
    },
  },
};

export function convertRomanToKana(original: string) {
  const str = original.replace(/[A-Za-z]/, s => String.fromCharCode(s.charCodeAt(0) - 65248)).toLowerCase(); // 全角→半角→小文字
  let result = '';
  let tmp = '';
  let index = 0;
  const len = str.length;
  let node = tree;
  const push = (char: string, toRoot = true) => {
    result += char;
    tmp = '';
    node = toRoot ? tree : node;
  };
  while (index < len) {
    const char = str.charAt(index);
    if (char.match(/[a-z]/)) { // 英数字以外は考慮しない
      if (char in node) {
        const next = node[char];
        if (typeof next === 'string') {
          push(next);
        } else {
          tmp += original.charAt(index);
          node = next;
        }
        index++;
        continue;
      }
      const prev = str.charAt(index - 1);
      if (prev && (prev === 'n' || prev === char)) { // 促音やnへの対応
        push(prev === 'n' ? 'ン' : 'ッ', false);
      }
      if (node !== tree && char in tree) { // 今のノードがルート以外だった場合、仕切り直してチェックする
        push(tmp);
        continue;
      }
    }
    push(tmp + char);
    index++;
  }
  tmp = tmp.replace(/n$/, 'ン'); // 末尾のnは変換する
  push(tmp);
  return result;
}

参考

変換表はこちらの記事を元に叩き台を作成しました。
https://qiita.com/le_panda_noir/items/5be2639f77905879432a
書かれている通り、表を作成するのは地味に面倒でした…(IMEのカナ変換に生まれて初めて感謝しました)。

全角→半角変換はこちらの記事を参考にしました。
https://qiita.com/yamikoo@github/items/5dbcc77b267a549bdbae

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React内に埋め込むhtmlでイベントハンドリングする方法

htmlからreact componentに変換したほうがよい気がするがよいものが見つからなく急ぎだったのでとりあえず下記の方法で対応した。

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentDidMount() {
    window.handleClick = this.handleClick;
  }

  handleClick() {
    console.log("clicked");
  }

  componentWillUnmount() {
    window.handleClick = null;
  }

  render() {
    let inputhtml = "<a onclick=window.handleClick()>HelloWorld</a>";
    return (
      <div>
        <div dangerouslySetInnerHTML={{ __html: inputhtml }} />
      </div>
    );
  }
}

参考

How to parse a String containing HTML to React Components

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JWTライブラリの危機的な脆弱性について調べた

JWT 脆弱性 で検索すると 【翻訳】JSON Web Tokenライブラリの危機的な脆弱性 って記事(4年ぐらい前の記事)が出てきて読んでいたのですが、よくわからなかったので手を動かしながら調べてみました。
※ 翻訳の元記事はこちら

node-jsonwebtoken, pyjwt, namshi/jose, php-jwt, jsjwtを非対称の鍵(RS256, RS384, RS512, ES256, ES384, ES512)で使っている場合、最新のバージョンに更新してください。

とのことなので、バージョン更新で直ったようなので修正を読んでいきます。
また、 js を利用して開発する機会が多いので node-jsonwebtoken の修正を確認してみます。

確認したコードは下記にあります。
https://github.com/OshiroSeiya/jsonwebtoken-audit-check

脆弱性の概要を知る

none アルゴリズムの話は飛ばします。元記事の内容を見たほうが良いです。

RS256 アルゴリズムを利用して JWT を発行している場合に攻撃者が HS256 アルゴリズムを利用して発行した JWT のチェックが通ってしまう問題があります。
RS256 は JWT を発行するのは秘密鍵を用いて発行し、チェックには公開鍵を利用します。
一報 HS256 は JWT を発行する時とチェックに利用する鍵が同じになります。

認証側のチェックが下記のようになっていた場合
※ TOKEN: 発行されたJWT
※ KEY: はチェックの際に利用する鍵です

jwt.verify(TOKEN, KEY)

利用するアルゴリズムが TOKEN の方に入っているためチェックの際に RS256 で検証するべきなのか HS256 で検証するべきなのか判断します。
RS256 でTOKENを発行する仕組みになっている場合、攻撃者が HS256 のアルゴリズムで公開鍵(利用用途的に公開されているので誰でも取得可能)を用いて TOKEN を発行するとチェックする際に HS256 を用いてチェックされ KEY は同じ公開鍵を利用するためチェックが通ってしまうことになり、改ざんが可能になるということみたいです。

対応されたライブラリのPRなどを見る

修正は下記のようです。
https://github.com/auth0/node-jsonwebtoken/pull/69
https://github.com/auth0/node-jsonwebtoken/commit/7017e74db9b194448ff488b3e16468ada60c4ee5
https://github.com/auth0/node-jsonwebtoken/pull/71

この修正が入る以前と以後のライブラリを利用して挙動の確認とどういう対策が行われたのかを確認してきます。

修正前: 4.1.0 (https://github.com/auth0/node-jsonwebtoken/tree/b69d441c6e5e4b2efaafde682b4b9670ac3bcb51)
修正後: 4.2.2 (https://github.com/auth0/node-jsonwebtoken/tree/e46ca6634447cf6a5b7f08298aa2f2450b8df704)

修正前のバージョンで問題が起きることを確認する

4.2.0で追加されたコードは下記でした。
https://github.com/auth0/node-jsonwebtoken/pull/69/files

  if (!options.algorithms) {
    options.algorithms = ~secretOrPublicKey.toString().indexOf('BEGIN CERTIFICATE') ?
                        [ 'RS256','RS384','RS512','ES256','ES384','ES512' ] :
                        [ 'HS256','HS384','HS512' ];
  }
  var header = jws.decode(jwtString).header;
  if (!~options.algorithms.indexOf(header.alg)) {
    return done(new JsonWebTokenError('invalid signature'));
  }

要するに jwt.verify メソッドを利用する時の secretOrPublicKey の値に BEGIN CERTIFICATE が含まれる場合は [ 'RS256','RS384','RS512','ES256','ES384','ES512' ] のみ利用できるようになるということですね。
※ 4.2.1で BEGIN PUBLIC KEY が含まれる場合の処理も追加されていますがやりたいことは同じなので飛ばします。
※ 4.2.2で BEGIN RSA PUBLIC KEY が含まれる場合の処理が追加されていますがやりたいことは同じなので飛ばします。

4.2.0で追加されたテストを見ます。
https://github.com/auth0/node-jsonwebtoken/pull/69/files#diff-f676fb748f383a87d0f55bec4f266023

テストコードを書きながらためしたほうが僕は理解しやすいので下記のようにコードを書きました。
※ 公開鍵と秘密鍵はテストで利用されていたものと同じものを利用しています。
https://github.com/OshiroSeiya/jsonwebtoken-audit-check/blob/master/src/4.1.0/index.test.js

const fs = require('fs');
const path = require('path');
const jwt = require('jsonwebtoken');

// 公開鍵
const PUB = fs.readFileSync(path.join(__dirname, 'pub.pem'), 'utf8');
// 秘密鍵
const PRIV = fs.readFileSync(path.join(__dirname, 'priv.pem'), 'utf8');
// payload
const payload = {
  foo: "bar"
};

test('サーバー側で署名した TOKEN がチェックを通過し payload が取得できる', () => {
  // サーバー側で秘密鍵を利用し、アルゴリズムを RS256 で署名した TOKEN
  const RS256_TOKEN = jwt.sign(payload, PRIV, {algorithm: 'RS256'});

  expect(jwt.verify(RS256_TOKEN, PUB)).toEqual(payload);
});

test('攻撃者が署名した TOKEN がチェックを通過し payload が取得できる', () => {
  // 攻撃者がRS256の公開鍵を利用し、アルゴリズムを HS256 で署名したTOKEN
  // ※HS256はdefaultのアルゴリズムですがわかりやすいように設定
  const HS256_TOKEN = jwt.sign(payload, PUB, {algorithm: 'HS256'});

  expect(jwt.verify(HS256_TOKEN, PUB)).toEqual(payload);
});
PASS  ./index.test.js
  √ サーバー側で署名した TOKEN がチェックを通過し payload が取得できる (9ms)
  √ 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.022s
Ran all test suites.

しっかりと攻撃者が署名した TOKEN がチェックを通過し payload が取得することができています。

修正後のバージョンで問題が起きないことを確認する

先程作ったテストを 4.2.2 に node-jsonwebtoken をアップデートしてテストを実行してみます。

 FAIL  ./index.test.js
  √ サーバー側で署名した TOKEN がチェックを通過し payload が取得できる (9ms)
  × 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる (4ms)

  ● 攻撃者が署名した TOKEN がチェックを通過し payload が取得できる

    JsonWebTokenError: invalid signature

      24 |   const HS256_TOKEN = jwt.sign(payload, PUB, {algorithm: 'HS256'});
      25 | 
    > 26 |   expect(jwt.verify(HS256_TOKEN, PUB)).toEqual(payload);
         |              ^
      27 | });
      28 | 

      at Object.<anonymous>.module.exports.verify (node_modules/jsonwebtoken/index.js:141:17)
      at Object.verify (index.test.js:26:14)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.199s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

結果、攻撃者が署名した TOKEN はしっかりとエラーになっていることが確認できました。

まとめ

記載されていた通り、 node-jsonwebtoken の新しいバージョンであれば問題は起きなくなっていました。
ライブラリで対応(利用できるアルゴリズムを絞る)されてはいますが、仕様的には今後も起きる可能性はなくはないと思われます。
ライブラリを利用するときは対策されているか確認してみると良さそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gulp4再入門 gulpfileの分割とnodeモジュールの利用

先に結論だけ

私のポートフォリオサイトのメンテナンスを行い、タスクランナーをgulp4へ更新しました。その結果、gulpfileが8行になりました。

/gulpfile.js/index.js
"use strict";

const revision = require("gulptask-revision")("./dist/");
exports.revision = revision;

const { s3_deploy, s3_staging } = require("./awsPublish");
exports.s3_deploy = s3_deploy;
exports.s3_staging = s3_staging;

gulp4は以前のバージョンと比べ、gulpfileの管理工数を減らすことができます。

はじめに

2018年12月にgulp4が正式リリースされました。

Version 4.0 Now Default

gulp4をしばらく使ってみた結果、gulpfileの管理が簡略化できました。この記事はその手法を共有するためのものです。

この記事が想定する読者

この記事はgulp3以前を利用しているユーザーを想定しています。
そのため、gulpのインストールやタスクの構築などは記事の対象としません。

マイグレーションガイド

gulp3を利用している方は、まずgulp4への移行を行います。
公式ドキュメント Quick Start

こちらの記事でgulp3から4へのマイグレーションの方法が詳しく解説されています。
Gulp4がリリースされたのでgulpfile.js をアップデートした

gulp4の利点

gulp3以前には、

  • gulpfileの肥大化問題
  • gulpプラグインの更新問題

という2つの問題があり、これがgulpfileの管理を困難にしていました。

gulp4へ移行することによって、この2つの問題を解消できます。

前提 : gulp4におけるタスクとは

上記2つの問題を解決するための前提として、gulp4におけるタスクの構造を理解しておく必要があります。この構造が、2つの問題を解くことに繋がります。

もっとも単純な形のgulpタスクを、公式ドキュメントから引用します。

function simpleTask(cb) {
  ...
  cb();
}

gulp4におけるタスクは関数です。処理後に、第一引数に渡されたコールバック関数を実行して処理完了を通知することでタスクとなります。

gulpのタスクは非同期処理を行うことを前提としています。そのため、タスクには処理が完了したことを通知する方法を備えている必要があります。

  • streamをreturnする。
  • Promiseをreturnする。
  • async関数にする。
  • 処理の完了後にコールバック関数を実行する。

などの形にすることで、関数はタスクとして機能します。

gulp.task()について

gulp3までで使われていたgulp.task()関数は非推奨になりました。この関数はgulpfileの互換性維持のために用意されたもので、新規での利用は推奨されません。

gulpfileの肥大化問題

gulp3以前では、gulpfile.jsは巨大な単一ファイルとして扱われてきました。数百行から時には千行を超えるファイルの管理は難しいものでした。

gulp4ではこのgulpfileの肥大化を抑制できます。

パブリックタスク / プライベートタスク

公式ドキュメントから、タスクの作成のサンプルを引用します。

gulpfile.js
const { series } = require('gulp');

function clean(cb) {
  ...
  cb();
}

function build(cb) {
  ...
  cb();
}

exports.build = build;
exports.default = series(clean, build);

exportされていない関数がプライベートタスク、exportされている関数がパブリックタスクになります。

これらのタスクをターミナルから実行してみます。

$ gulp clean
Task never defined: clean

$ gulp build
Starting 'build'...

$ gulp
Starting 'default'...

exportしていないcleanタスクは実行できませんでした。プライベートタスクは定義されたjsファイルの外からは呼び出すことができません。

プライベートタスクを利用することで、タスク製作者は外部から呼び出すべきタスクを明示できます。gulp3では、すべてのタスクが外部から呼び出し可能でした。そのため、タスクを細分化してメンテナンスを簡単にしようとすると、利用者がどのタスクを使っていいのかわからなくなるという問題がありました。プライベートタスクはこうした問題を解決します。

exportsとは

exportsはnode.jsにおけるmodule.exportsへのショートカットです。named exportをより短く記述するための機能です。

node.js Modules

node.js modules exports

つまりgulp4におけるパブリックタスクとは「module.exportsされ、非同期処理の完了を通知する関数」と言えます。

タスクファイルの分割

gulp4におけるパブリックタスクは exportされた関数です。そのため、gulpfile.jsの外部に記述された関数もrequireで読み込めばタスクとして利用できます。つまりgulpfile.jsが分割できます。

例としてtaskA.jsgulpfile.jsの2つのファイルがあるとします。いずれのファイルもpackage.jsonと同じディレクトリにあるとします。

taskA.js
const { series } = require('gulp');

function taskA1(cb) {
  ...
  cb();
}
function taskA2(cb) {
  ...
  cb();
}

exports.taskA = series(taskA1, taskA2);
gulpfile.js
const { taskA } = require("./taskA");
exports.taskA = taskA;

gulpfile.jsはtaskA.jsからタスクを読み込み、再度exportしてパブリックタスクにしています。この状態でターミナルから$ gulp taskAとタスクを実行できます。
また、gulpfile.jsとターミナルの両方からプライベートタスクtaskA1およびtaskA2は呼び出せません。

分割ファイルの配置方法

gulpでは、コード分割の際のファイルの配置方法に関する仕組みを提供しています。
公式ドキュメント

package.json
/ gulpfile.js
  ┠ index.js
  ┗ taskA.js

直感的に理解しにくいのですが、gulpfile.jsという名前のディレクトリを配置し、その中にindex.jsを配置します。するとgulpは./gulpfile.js/index.jsを従来のgulpfile.jsと同様にルートファイルとして認識します。

このルールに従わないファイルのrequireも現状では問題なく機能します。しかし、関連するファイルが分散してしまうのを避けるため、こうしたルールにしたがってファイルを配置することをオススメします。

ファイル分割の恩恵

gulpfile.jsは肥大化しやすいファイルです。これを分割することでメンテナンス性が向上します。また、プライベートタスクをファイル分割と併用することで、親ファイルからアクセス可能なタスクを制限できます。

タスクのモジュール化

タスクを記述したjsファイルをnodeモジュール化し、そのモジュールをgulpfileに読み込むことも可能です。

例として、このようなパッケージを作成してGitHubにpushします。名前はsample-taskとします。

package.json
{
  "name": "sample-task",
  "main": "./index.js",
  ...
  "dependencies": {
    "gulp": "^4.0.1",
    "gulp-rev": "^9.0.0", // <- タスク内で利用するgulpプラグインをdependenciesに追加
    ...
  },
}
index.js
"use strict";

const { series, src, dest } = require("gulp");
const rev = require("gulp-rev");
const path = require("path");
const distPath = path.resolve("./dist/");

function taskA(){
  return src(...)
    .pipe(...)
    .pipe(dest(...);
};

function taskB(){
  return src(...)
    .pipe(...)
    .pipe(dest(...);
};

exports.sampleTask = series(taskA, taskB);

別のプロジェクトから、sample-taskモジュールをnpmでインストールします。

$ npm install https://github.com/[GitHubのユーザーID]/sample-task.git

sample-taskモジュールはgulpfile.jsから読み込めます。

gulpfile.js
const { sampleTask } = require("sample-task");
exports.sampleTask = sampleTask;

requireで読み込んだタスクは以下のように利用します。

  • 再度exportすることでバプリックタスクとして利用する。
  • seriesやparallelに組み込み、プライベートタスクとして利用する。

モジュール化されたタスクは、自身のpackage.jsonで依存するプラグインを管理できます。タスクを読み込む側では、依存プラグインの管理の必要はありません。

タスクモジュールに引数を与える

ファイル分割 / モジュール化したタスクに、タスク実行時に変数を与えることはできません。子タスクに変数を与えたい場合「変数を受け取りタスクを返す関数」を使います。

index.js
"use strict";

const { series, src, dest } = require("gulp");

module.exports = (arg1, arg2, arg3) => {
  function taskA(){
    return src(arg1)
      .pipe(...)
      .pipe(dest(arg2);
  };

  function taskB(){
    return src(arg1)
      .pipe(...)
      .pipe(dest(arg3);
  };

  return series(taskA, taskB);
}

このモジュールをgulpfile.jsから読み込みます。

gulpfile.js
const sampleTask = require("./index")("./path/to/src", "./path/to/dist1", "./path/to/dist2");
exports.sampleTask = sampleTask;

require(モジュールのパスかID)(引数)の形で変数を渡すことができます。

タスクモジュールの例

Webサイト用のgulpfileでよく利用するタスクを切り出し、モジュール化してみました。皆様がモジュール化を行う場合の参考資料としてご利用ください。

gulptask-revision

GitHub リポジトリ

gulp-revを利用して、出力されたファイルにリビジョンを振る一連のタスクをモジュール化したものです。

$ npm install https://github.com/MasatoMakino/gulptask-revision.git -D

でインストールして

gulpfile.js
const rev = require("gulptask-revision")("変換するディレクトリのパス");

と読み込んで利用します。

gulptask-imagemin

GitHub リポジトリ

gulp-imageminを利用して、画像ファイルの最適化を行います。
gulp-image-resizeが別途imagemagickのインストールを必要とします。macOSの場合、Homebrew経由でインストールが可能です。

brew install imagemagick

参考記事

macにImageMagickをインストールし、convertコマンドで画像を縮小する。

モジュールの利用方法は

$ npm install https://github.com/MasatoMakino/gulptask-imagemin.git -D

でインストールして

gulpfile.js
const images = require("gulptask-imagemin")("画像ソースのディレクトリ", "出力先ディレクトリ");

と読み込んで利用します。
watchのタスクとしても動作します。

gulpプラグインの更新問題

gulpの維持管理を難しくするもうひとつの要因として、gulpプラグインの更新問題があります。依存するプラグインの更新が途絶えた場合、プラグインをフォークして自力でメンテナンスするか、そのタスクをgulpから切り離すかの選択を迫られます。

gulpというレイヤーが増える分、依存するモジュールは増え、更新停止のリスクは増加します。ならば最初からnodeモジュールを直接実行したほうがメンテナンスのリスクは少なくなります。

nodeモジュールがそのまま走る

gulp4のタスクは前述の通りただの関数です。そのためタスク内でnodeモジュールがそのまま走ります。

公式ドキュメントからサンプルを引用します。

gulpfile.js
const { rollup } = require('rollup');

// Rollup's promise API works great in an `async` task
exports.default = async function() {
  const bundle = await rollup.rollup({
    input: 'src/index.js'
  });

  return bundle.write({
    file: 'output/bundle.js',
    format: 'iife'
  });
}

rollup.jsのバンドル処理はasync/awaitに対応しています。そのため、タスクの関数をasyncにすることで簡単に取り込めます。

const del = require('delete');

exports.default = function(cb) {
  // Use the `delete` module directly, instead of using gulp-rimraf
  del(['output/*.js'], cb);
}

async/awaitに対応せず、非同期処理の完了後にコールバックを呼ぶタイプのモジュールもあります。その場合タスクの引数のコールバックをそのままモジュール側の関数に渡してしまうことで取り込めます。

公式ドキュメントでも、ファイル変換を伴わない処理ではgulpプラグインよりもnodeモジュールの使用を推奨しています。

Plugins should always transform files. Use a (non-plugin) Node module or library for any other operations.

依存するモジュールの量と層を減らすことで、プラグインの更新停止リスクは減少します。

個人的な感想

破壊的変更

gulp4は3以前と比べるとまったくの別物です。これだけ大規模かつ破壊的な変更をしながらgulpプラグインの互換性がほぼ維持されていることに驚きました。

gulp4のオススメ度

個人的なgulp4への移行のオススメ度を、状況別にまとめると

  • gulp3を利用している : 文句なしにオススメです。
  • すでに他のツールでワークフローができ上がっている : 既存のプロジェクトに導入する必要はありません。
  • 新規のプロジェクトに導入する : 部分的な導入も可能なので検討の価値ありです。

となります。

タスクモジュールの粒度

タスクをモジュール化した場合、対象とする作業はプラグインよりも巨大になり、その分汎用性は低くなります。タスクをどの程度の粒度で切り出すべきなのかは今後の課題です。

タスクモジュールの副作用

モジュール化されたタスクは、以前のgulpプラグイン同様に依存レイヤーを増やすことになります。更新停止のリスクや、破壊的更新による影響の増大がモジュール化の副作用として考えられます。

参考記事

JavaScriptのモジュールシステムの歴史と現状

exports / requireとそのほかのモジュールシステムの歴史がまとめられています。gulpタスクの構造を理解することができました。

JavaScriptをやっていると npm/yarn/gulp/grunt/webpack など、たくさんのツールがあって混乱したので、それぞれの役割と違いをざっくりとまとめた

現状のgulpの立ち位置を、そのほかのツールと比較して再確認できる素晴らしい記事です。

以上、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gulp4 lastRun関数の挙動

はじめに

gulp4にはlastRunという関数が追加されました。gulp.src関数と組み合わせて使うことで、更新のあったファイルのみを後続のpipeに流すことができます。

公式ドキュメント

この関数の挙動を理解するために、テスト用のコードを書いて実験をしてみました。

この記事の前提

想定する読者

この記事は、以下の方を想定しています。

  • gulp3をすでに利用したことがある
  • gulp4への移行を考えている

gulpのインストールやセットアップに関する内容は取り扱いません。

実行環境

この記事の実験用コードは以下のモジュールを利用しています。バージョンが異なる場合、この記事とは異なる結果になることがあります。

package.json
  "devDependencies": {
    "del": "^4.1.1",
    "gulp": "^4.0.1",
    "gulp-imagemin": "^5.0.3"
  },

実験用コード

今回使用した実験用コードはこちらのリポジトリにアップロードしています。

https://github.com/MasatoMakino/behavior_of_gulp_lastrun

このコードには 3 つの gulp タスクが登録されています。

  • gulp clean : 出力先フォルダーの中身をすべて削除する。
  • gulp images : ソースフォルダー内の画像を圧縮する。
  • gulp watch : ソースフォルダー内の画像の変更を監視し、gulp imagesタスクを実行する。

images タスクの内容は以下の通りです。

gulpfile.js
const images = () => {
  const lastRunResult = lastRun(images);
  console.log(lastRunResult);
  return src("./src/img/**/*.jpg", { since: lastRunResult })
    .pipe(imagemin())
    .pipe(dest("./dist/img/"));
};

lastRunの結果をコンソール出力し、srcにはオプションでlastRunの結果を渡します。

実行結果

タスクを直接実行

gulp imagesで画像圧縮タスクを直接起動した場合のログは以下の通りです。

$ gulp images
[12:21:50] Using gulpfile ~/Documents/projects/behavior_of_gulp_lastrun/gulpfile.js
[12:21:50] Starting 'images'...
undefined
[12:21:51] gulp-imagemin: Minified 1 image (saved 9.91 kB - 60.6%)
[12:21:51] Finished 'images' after 555 ms
Done in 2.56s.

lastRun(images)の実行結果はundefinedになり、すべての画像が処理の対象になりました。

タスクを watch 経由で実行

gulp watchでファイルの更新を監視した場合のログは以下の通りです。

$ gulp watch
[12:16:56] Using gulpfile ~/Documents/projects/behavior_of_gulp_lastrun/gulpfile.js
[12:16:56] Starting 'watch'...
[12:18:21] Starting 'images'...
undefined
[12:18:22] gulp-imagemin: Minified 0 images
[12:18:22] Finished 'images' after 791 ms
[12:19:03] Starting 'images'...
1556507901322
[12:19:03] gulp-imagemin: Minified 1 image (saved 9.46 kB - 61.1%)
[12:19:03] Finished 'images' after 56 ms

watch開始後1回目のタスク実行時、lastRun(images)の結果はundefinedになり、すべての画像が処理の対象になりました。
2回目のタスク実行時、lastRun(images)の結果は1556507901322になり、変更した1ファイルのみが処理の対象になりました。1556507901322は前回実行時のタイムスタンプです。

watch タスクを実行中に出力先を削除

gulp watchを実行中にgulp cleanで出力先のファイルをすべて削除しました。
そのあとsrcフォルダー内の画像ファイルを編集しました。結果は以下の通りです。

[12:30:44] Starting 'images'...
1556508367927
[12:30:44] gulp-imagemin: Minified 1 image (saved 10.3 kB - 55.8%)
[12:30:44] Finished 'images' after 65 ms

lastRun(images)の処理は継続され、編集した1ファイルのみがdistフォルダーに出力されました。出力先のファイルが削除されたことには反応しません。

まとめ

  • タスクを直接実行した場合、lastRunは常にundefinedを返し、すべてのファイルが処理対象になる。
  • タスクをwatch経由で実行した場合、lastRunは2回目以降から前回タスク実行時のタイムスタンプを返す。
  • watch実行中に、出力先のファイルを操作されても反応しない。

呼び出し元がwatchでない場合、lastRunは無視されます。たとえばデプロイ用のseries/parallelタスク内に今回のimagesタスクを組み込んでも、すべてのファイルが処理対象になるので出力漏れを起こすことはありません。

また、lastRun は出力先のファイルを考慮しません。gulp-changedプラグインのように、出力先ファイルの更新時間の確認はしません。初回実行時は出力先に処理済みファイルがあったとしても、無視してすべてのファイルを処理して上書きします。

たとえば数百の画像ファイルの最適化処理などの、以下のようなタスクにはgulp-changedプラグインは有効です。

  • 数十秒 ~ 数百秒の時間がかかる
  • 処理済みファイルを再利用して部分的にスキップができる

gulp4においてもgulp-changedは有用なプラグインです。どのような性質の処理なのかを考慮して、lastRunと使い分けてください。

以上、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript(ES6)の関数、オブジェクト、クラスのまとめ

文法まとめに引き続き、関数、オブジェクト、クラスのまとめ。

今回クラス設計にふれて、内容がオブジェクト指向に関わって来ましたが、だんだん複雑になって来ますね。
そこで、開眼! JavaScriptを読む事にしました。こちらは後程まとめますが、JavaScript特有のクセを理解するのに役立ちそうです。

以下、とりあえず今回は、文法まとめの続きです。

関数

関数の定義

const 定数名 = function(引数名) {
  //まとめたい処理を書く
  return 戻り値
};

アロー関数

ES6からの新しい関数定義方法です。
function()() =>と短く書く事が出来ます。
この書き方をアロー関数と呼びます。

const 定数名 = (引数名) => {
  //まとめたい処理を書く
 return 戻り値
};

オブジェクト

文法まとめにも書きましたが、

// オブジェクトの定義
const 定数名 = { プロパティ名1: 1, プロパティ名2: atai2 }

// オブジェクトの呼び出し
console.log(定数名.プロパティ名)

オブジェクトの値に関数を持たせる

// オブジェクトの値に関数を定義
const 定数名 = {
  プロパティ名: () => {
    処理
  };

// オブジェクトの値に定義した関数の呼び出し
定数名.プロパティ名();

クラス

オブジェクトをたくさん作るための設計図です。
例えば、下記のように本のタイトルと著者の情報を持ったオブジェクトを使いたい場合、本の情報が増える度に無限にオブジェクトを定義する事になります。

const book1 = { title: 'こころ', author: '夏目漱石' };
const book2 = { title: '人間失格', author: '太宰治' };
const book3 = { title: '銀河鉄道の夜', author: '宮沢賢治' };
.
.
.
本の情報が増える度に定義し続ける事になる

同じ構造のオブジェクトが複数回必要になる場合は、クラスを1度定義し、クラスからオブジェクトを生成します。
クラスから生成したオブジェクトは、インスタンスと呼びます。

クラスの定義

class クラス名 {

}

インスタンスの生成

 
class クラス名 {

}

const 定数名 = new クラス名();

コンストラクタ

クラスにはコンストラクタと呼ばれる機能があります。
コンストラクタは、インスタンスが生成された時に実行されます。
また、コンストラクタの中でthis.プロパティ名 = 値;と書くとインスタンスにプロパティと値を追加出来ます。

class クラス名 {
  constructor(引数) {
    this.プロパティ名 = ;
  }
}

メソッド

// メソッドの定義
class クラス名 {
  メソッド名() {
    処理
  }
}

// メソッド呼び出し
const 定数名 = new クラス名
定数名.メソッド名();

アンキパン欲しい・・・
現実逃避はさておき、地道にアウトプットして覚えるしかない。自作アプリに組み込むなどして、実際に使っていかないと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript(ES6)の基本文法まとめ

JavaScriptを覚える必要が出て来たので、勉強開始。
忘れないうちに自分用にアウトプット。

今回やった事

JavaScriptを使う上で、Vue.jsやReactなどのフレームワークの使用が主流となっているようですが、最初は、ピュアなjsを覚えた方が良さそうなので、JavaScriptのES6を覚える事にしました。今回は、文法とクラスに関してまとめます。

  • ES6とはJavaScriptのバージョン名で、これから始めるのであればES6推奨らしい。

文法まとめ

データ出力

console.log();

文字列

値に文字列を使用したい場合は、使用したい文字列をシングルクォーテーション、ダブルクォーテーションで囲って使用します。

'文字列1'
"文字列2"

let 変数の宣言

letの後ろに変数名を書いて宣言する。
その後に=で代入。

let 変数名 = ;

算術演算子

// 加算(足し算)
console.log(1 + 2);
// 減算(引き算)
console.log(5 - 3)
// 乗算(掛け算)
console.log(5 * 4)
// 除算(割り算)
console.log(9 / 3)
// 剰余(割った余り)
console.log(9 % 2)

加算、減算の省略

既に値の入っている変数intに対し、加算、減算を行った結果を代入する場合下記のように書きます。

int += 1
int -= 1

ただし、1を足したり、引いたりする場合は下記のように省略して書く事が出来ます。

int ++
int --

この書き方はループ処理で条件の値を更新する時に使うので、覚えておく必要があります。

比較演算子

// 等しい時trueを返す
a === b
// 等しくない時trueを返す
a !== b

配列

要素を []で囲います。要素の間は,で区切ります。

[1, 2, 3]

オブジェクト

配列とは違い{}で囲います。プロパティと値の間は:で区切ります。
要素の間は,で区切ります。

{ プロパティ1: 値1, プロパティ2: 値2, プロパティ3: 値3 }

if 条件分岐

if (条件式) {
  処理1;
} else if {
  処理2;
} else {
  処理3;
}

swich 条件分岐

swich(条件の値) {
  case 1:
    処理1
  case 2:
    処理2
  case 2:
    処理2
  default:
    default処理 //caseのどの条件にも当てはまらない時の処理を書く
    break;
}

while 繰り返し処理

while (条件式) {
  処理
}

for 繰り返し

for (変数の定義; 条件式; 変数の更新) {
  処理
}

このままアウトプットし続けて、なんとか覚えたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

東京公共交通オープンデータから鉄道網のForce-Graphを作ってみる

東京公共交通オープンデータチャレンジ

東京公共交通オープンデータチャレンジでは、首都圏の主要な公共交通機関データがオープンデータとして公開されており、「第3回東京公共交通オープンデータチャレンジ」にエントリーすれば、アクセストークンが発行されAPIを使用することができます。
GW中にサイトを知り、とりあえずエントリーしてみました。

概要

地方在住のため、首都圏の鉄道網をあまり知りません。
このため、情報整理と東京オープンデータチャレンジのAPI、D3.jsの勉強を兼ねて首都圏鉄道網のForce-Graphを作ってみました。

内容・手順

1.ODPT Train APIでデータ整理

「ODPT Train API」で路線と駅の情報を収集し、javaでデータを整理しました。
ソースコードを以下に示します。
なお、httpクライアントはOkHttp、JSONの処理はGsonを使用しています。
「ODPT Train API」のレスポンス(JSON)から路線の「路線名」、「固有識別子」、「路線の駅」を抽出してRaillineインスタンスを生成し、路線内の駅の「駅名」、「固有識別子」、「関連する路線の識別子」からStationインスタンスを生成しています。駅は路線毎に固有識別子が割り当てられているようなので、「駅名(和名)」で情報を集約しています。

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class Test {
    private static final String URL_TOKYO_CH="https://api-tokyochallenge.odpt.org/api/v4/";
    private static final String KEY_TOKYO_CH="アクセストークン";

    @SuppressWarnings({ "unused", "rawtypes", "unchecked" })
    public static void main(String[] args){
        OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder();
        okHttpBuilder.connectTimeout(20, TimeUnit.SECONDS);
        okHttpBuilder.readTimeout(20, TimeUnit.SECONDS);
        okHttpBuilder.writeTimeout(20, TimeUnit.SECONDS);
        OkHttpClient client=okHttpBuilder.build();
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        List<Map> list=trainAPI(client,gson,"odpt:Railway");
        Map<String,Station> stations=new HashMap<String,Station>();
        List<RailLine> raillines=new ArrayList<RailLine>();
        for(Map map : list){
            RailLine line=new RailLine();
            line.name_ja=((Map)map.get("odpt:railwayTitle")).get("ja").toString();
            line.name_en=((Map)map.get("odpt:railwayTitle")).get("en").toString();
            line.sameAs=map.get("owl:sameAs").toString();
            line.operator=map.get("odpt:operator").toString();
            List<Map> ll=(List<Map>)map.get("odpt:stationOrder");
            for(Map o : ll){
                String st=((Map)o.get("odpt:stationTitle")).get("ja").toString();
                line.stations.add(st);
                if(stations.containsKey(st)){
                    Station s=stations.get(st);
                    s.lines.add(line.name_ja);
                }else{
                    Station s=new Station();
                    s.sameAs=o.get("owl:sameAs").toString();
                    s.name_ja=((Map)o.get("odpt:stationTitle")).get("ja").toString();
                    s.name_en=((Map)o.get("odpt:stationTitle")).get("en").toString();
                    s.lines.add(line.sameAs);
                    stations.put(s.name_ja, s);
                }
            }
            raillines.add(line);
        }
        Map<String,Object> ret=new HashMap<String,Object>();
        ret.put("stations", stations);
        ret.put("raillines", raillines);
        File f=new File("railway.json");
        BufferedWriter bw=null;
        try{
            bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f),"UTF-8"));
            bw.write(gson.toJson(ret));
            bw.flush();
            bw.close();
            bw=null;
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(bw!=null){
                try{bw.close();}catch(Exception e){}
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static List<Map> trainAPI(OkHttpClient client,Gson gson,String odpc){
        String url=URL_TOKYO_CH+odpc+"?acl:consumerKey="+KEY_TOKYO_CH;
        System.out.println(url);
        try{
            Request request = new Request.Builder()
                    .url(url)
                    .get()
                    .build();
            Response response = client.newCall(request).execute();
            return gson.fromJson(response.body().string(), List.class);
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }
    }

    static class Station{
        public String name_ja;
        public String name_en;
        public String sameAs;
        public List<String> lines=new ArrayList<String>();
    }
    static class RailLine{
        public String name_ja;
        public String name_en;
        public String sameAs;
        public String operator;
        public List<String> stations=new ArrayList<String>();
    }
}

2.出力したJSON

上記のコードを実行すると、以下のJSONファイルが出力されます。
これを眺めていると、「こういう路線があって、こういう駅があるんだー」となんとなく首都圏の鉄道網について理解が増した気がします。

{
  "raillines": [
    {
      "name_ja": "東京さくらトラム(都電荒川線)",
      "name_en": "Tokyo Sakura Tram (Arakawa Line)",
      "sameAs": "odpt.Railway:Toei.Arakawa",
      "operator": "odpt.Operator:Toei",
      "stations": [
        "三ノ輪橋",
        "荒川一中前",
        "荒川区役所前",
        "荒川二丁目",
        "荒川七丁目",
        "町屋駅前",
        "町屋二丁目",
        "東尾久三丁目",
        "熊野前",
        "宮ノ前",
        "小台",
        "荒川遊園地前",
        "荒川車庫前",
        "梶原",
        "栄町",
        "王子駅前",
        "飛鳥山",
        "滝野川一丁目",
        "西ヶ原四丁目",
        "新庚申塚",
        "庚申塚",
        "巣鴨新田",
        "大塚駅前",
        "向原",
        "東池袋四丁目",
        "都電雑司ヶ谷",
        "鬼子母神前",
        "学習院下",
        "面影橋",
        "早稲田"
      ]
    },
/***** 省略 *******/
  "stations": {
    "世良田": {
      "name_ja": "世良田",
      "name_en": "Serada",
      "sameAs": "世良田",
      "lines": [
        "odpt.Railway:Tobu.Isesaki"
      ]
    },
    "東所沢": {
      "name_ja": "東所沢",
      "name_en": "Higashi-Tokorozawa",
      "sameAs": "東所沢",
      "lines": [
        "odpt.Railway:JR-East.Musashino"
      ]
    },
/***** 省略 *******/

3.D3.jsでForce-Graphを表示

路線・駅の情報ををD3.jsに読み込み、Force-Graphを生成してみました。
初めてD3.jsでForce-Graphを作りましたが、javaでGraphLayoutを書く時と比べて、すごく簡単に作れるのに驚きました。
D3.jsすごい。

<!DOCTYPE html>
<html>
<head>
    <title>tokyo-challenge-test</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.2/d3.min.js"></script>
</head>
<body>
<svg></svg>
<script type="text/javascript">
    let width = 1200;
    let height = 800;
    const loadData = () => {
        d3.json("railway.json").then(function(json) {
            createMap(json);
        });
    };
    const createMap=(json)=>{
        const rail=json.raillines;
        const station=json.stations;
        let nodes=[];
        let links=[];
        let check={};
        let idv=0;
        for(let i=0;i<rail.length;i++){
            let sts=rail[i].stations;
            let tmp=[];
            for(let j=0;j<sts.length;j++){
                if(!check[sts[j]]){
                    let p={id:idv++,label:station[sts[j]].name_ja,val:1};
                    tmp.push(p);
                    nodes.push(p);
                    check[sts[j]]=p;
                }else{
                    check[sts[j]].val=check[sts[j]].val+1;
                    tmp.push(check[sts[j]]);
                }
            }
            for(let i=1;i<tmp.length;i++){
                let l={source:tmp[i-1].id,target:tmp[i].id};
                links.push(l);
            }
        }
        const svg = d3.select("svg").attr("width",width).attr("height",height);
        const link = d3.select("svg")
            .selectAll("line")
            .data(links)
            .enter()
            .append("line")
            .attr("stroke-width", 1)
            .attr("stroke", "#ccc");
        const node = d3.select("svg")
            .selectAll("g")
            .data(nodes)
            .enter()
            .append("circle")
            .attr("r",function(d){return d.val*5;})
            .attr("fill", "orange")
            .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
        const label = d3.select("svg")
            .selectAll("g")
            .data(nodes)
            .enter()
            .append("text")
            .attr("text-anchor", "middle")
            .attr("dominant-baseline", "middle")
            .style("fill", "steelblue")
            .style("font-size", "9px")
            .text(function(d){return d.label;});
        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink())
            .force("center", d3.forceCenter(600, 450))
            .force("charge", d3.forceManyBody().strength(-8))
            .force("x", d3.forceX().strength(0.05).x(width / 2))
            .force("y", d3.forceY().strength(0.05).y(height / 2));

        simulation.nodes(nodes).on("tick", ticked);
        simulation.force("link").links(links);
        function ticked() {
            link.attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });
            node.attr("cx", function(d) { return d.x; })
                .attr("cy", function(d) { return d.y; });
            label.attr("x", function(d) { return d.x; })
                .attr("y", function(d) { return d.y; });
        }
        function dragstarted(d) {
            if(!d3.event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }
         function dragged(d) {
            d.fx = d3.event.x;
            d.fy = d3.event.y;
        }
        function dragended(d) {
            if(!d3.event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }
        const zoom = d3.zoom()
            .scaleExtent([1/4,4])
            .on('zoom', function(){
                node.attr("transform", d3.event.transform);
                link.attr("transform", d3.event.transform);
                label.attr("transform", d3.event.transform);
            });
        svg.call(zoom);
    }
    loadData();
</script>
</body>
</html>

最後に

駅・路線が多すぎて、よくわからないグラフになりましたが、上野駅よりも新宿駅、渋谷駅の方が乗り入れている路線が多いのは意外でした。
もう少し表示を工夫したり、各駅間の距離や運賃等のデータを追加して遊んでみたいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでwindowのグローバル変数を呼ぶときの話

概要

twitterのシェアボタンwidjetをあるオブジェクトがロードされてから表示するようにしたら、
widjetにならなかったので、twttr.widgets.load()で呼び出そうとしたら、
うまく呼び出せなくて、呼び出すことができた方法のメモです。

Twitterウィジェット

https://publish.twitter.com/#
上記のTwitter公式から生成したものですが、下記のようにwidjets.jsを読み込んで、class="twitter-share-button"を指定すると、aタグがtwitterのシェアボタンウィジェットになります。

<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

しかし、最初から表示されてるオブジェクトにしか適用されず、Vueのv-ifディレクティブを使用したりして動的にボタンを表示する場合は適用されません。
この場合、widjets.jsが読み込まれた際にグローバル変数に追加されるtwttrを利用して、twttr.widgets.load()をしてウィジェットを描画する必要があります。

非同期で読み込んだFacebook, Twitterボタンを再描画する

Vueの単一ファイルコンポーネントの場合

このとき、Vueの単一ファイルコンポーネントの場合、下記のようにscriptタグ内でtwttr.widgets.load()を呼ぼうとするとtwttrが存在しないと言われてしまいます。
どうやら、グローバル変数に直接アクセスができないことが問題のようでした。

twitter.vue
...
<script>
export default {
  name: 'Twitter',
  data () {
    return {
    }
  },
  methods: {
    reloadWidgets () {
      twttr.widgets.load()
    }
  }
}
</script>
...

ですので、この場合は下記のようにwindowからアクセスすることで、twttrにアクセスすることができ、twttr.widgets.load()を呼ぶことができます.

twitter.vue
...
<script>
export default {
  name: 'Twitter',
  data () {
    return {
    }
  },
  methods: {
    reloadWidgets () {
      window.twttr.widgets.load()
    }
  }
}
</script>
...

あとがき

色々試行錯誤していたため、書いてる最中にwindowをスクリプトタグから呼べるんじゃないかと気づきました。
試行錯誤中に試してはいたのですが、他の要因からうまくできなかったため、この記事を書いている最中は下記のように、
main.jsVue.prototype.$twttrに変数を割り当ててインスタンスプロパティを設定する方法を紹介しようとしていました。
この方法だと、コンポーネント内からthis.$twttrで参照することができます。

main.js
Vue.prototype.$twttr = window.twttr

ちなみに、スマホから見るとなぜかwidjetsがundefinedと言われて、うまくウィジェット化できなかったため、
実際の実装ではhttps://twitter.com/intent/tweetを利用して、見た目は自作しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jest(v24.6 )の公式ドキュメント読んで基本を勉強してみた

Jestの公式ドキュメントの(自分的には)基本の部分を読んで勉強した内容を投稿させていただきます:bow:

私が個人的に勉強した部分を書いていくだけなので、詳細に説明しているというわけでもなく、網羅的にやったというわけでもなく、本当にただの私のブログですので、注意してください。
間違いなどありましたら、ご指摘お願いします

また、この記事中で私が試してみたコードは全て こちら にあります

Introduction

Getting Started

https://jestjs.io/docs/en/getting-started

jestのInstall

yarn add -D jest
# または
npm i -D yarn

2つの数を追加する関数のテストを書いてみる

sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
sum.test.js
const sum = require("./sum");

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

この状態で、yarn run jest

または、 npx jest とする、

image.png

jestを使ったテストができました


または、以下のようにpackage.jsonnpm script を追加して実行してもOK

package.json
{
  "scripts": {
    "test": "jest"
  }
}

Running from command line

設定ファイルとしてconfig.jsonを使用し、実行後にネイティブOSの通知を表示しながら、my-testに一致するファイルに対してJestを実行する方法は次のとおりです。

jest my-test --notify --config=config.json

Additional Configuration

以下のコマンドで基本設定ファイルを生成できる

yarn run jest --init

Using Babel

babelを使う設定

yarn add -D babel-jest @babel/core @babel/preset-env
.babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
sum.js
  function sum(a, b) {
    return a + b;
  }
- module.exports = sum;
+ export default sum;
sum.test.js
- const sum = require("./sum");
+ import sum from "./sum";

  test("adds 1 + 2 to equal 3", () => {
    expect(sum(1, 2)).toBe(3);
  });

image.png

babelを使用して、import、exportを使っていても問題なくテストができました。

Using Matchers

https://jestjs.io/docs/en/using-matchers

Common Matchers

test("two plus two is four", () => {
  // expect(2 + 2)と.toBe(4)で等しいことをテストしてくれます
  expect(2 + 2).toBe(4);
});

test("object assignment", () => {
  const data = { one: 1 };
  data["two"] = 2;
  // オブジェクトの値を確認したい場合は、toBeの代わりにtoEqualを使用する
  expect(data).toEqual({ one: 1, two: 2 });
});

test("numbers is not zero", () => {
  // 反対をテストすることもできます
  expect(2 + 1).not.toBe(0);
});

Truthiness

テストでは、undefinednull、およびfalseを区別する必要がある場合がありますが、これらを異なる方法で扱いたくない場合があります。 Jestにはあなたが欲しいものについてあなたが明確になることを可能にするヘルパーが含まれています。

  • toBeNull -> null にのみ一致
  • toBeUndefined -> undefined
  • toBeDefined -> toBeUndefinedの反対
  • toBeTruthy -> ifでtrueと見なすものすべてに一致する
  • toBeFalsy -> ifでfalseと見なすものすべてに一致する
test("null", () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test("zero", () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

Numbers

test("two plus two", () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // 等しいか確認
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

test("adding floating point numbers", () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3); <- 丸め誤差が原因でうまくテストできない
  expect(value).toBeCloseTo(0.3); // This works.
});

Strings

// toMatchを使って文字列を正規表現と照合することができる

test("there is no I in team", () => {
  expect("team").not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect("Christoph").toMatch(/stop/);
});

Arrays and iterables

// toContainを使用して、配列または反復可能オブジェクトに特定の項目が含まれているかどうかを確認できる

const shoppingList = [
  "diapers",
  "kleenex",
  "trash bags",
  "paper towels",
  "beer"
];

test("the shopping list has beer on it", () => {
  expect(shoppingList).toContain("beer");
  expect(new Set(shoppingList)).toContain("beer");
});

Exceptions

// 特定の関数が呼び出されたときにエラーをスローすることをテストしたい場合は、toThrowを使用する

function compileAndroidCode() {
  throw new Error("you are using the wrong JDK");
}

test("compiling android goes as expected", () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(Error);

  // 正確なエラーメッセージや正規表現を使うこともできます
  expect(compileAndroidCode).toThrow("you are using the wrong JDK");
  expect(compileAndroidCode).toThrow(/JDK/);
});

Testing Asynchronous Code

https://jestjs.io/docs/en/asynchronous

Callbacks

function fetchData(callback) {
  // 非同期処理を実行 -> しかし1秒待ってくれずにテストが終了してしまうので、このテストはうまくいかない
  setTimeout(() => callback("peanut butter"), 1000);
}

test("the data is peanut butter", () => {
  function callback(data) {
    // コールバック関数の引数の文字列が「peanut butter」かテスト
    expect(data).toBe("peanut butter");
  }

  fetchData(callback);
});

このテストを想定通りに動かすには、テストを空の引数を持つ関数に入れる代わりに、doneという単一の引数を使用する。テストを終了する前に、Jestはdoneコールバックが呼び出されるまで待機します

function fetchData(callback) {
  setTimeout(() => callback("peanut butter"), 1000);
}

test("the data is peanut butter", done => {
  function callback(data) {
    expect(data).toBe("peanut butter");
    done();
  }

  fetchData(callback);
});

Promises

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve("peanut butter"), 1000);
  });
}

function fetchDataErr() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("error"), 1000);
  });
}

test("the data is peanut butter", () => {
  // 必ずreturnしましょう、でないと、promiseの解決を待たずにテストが終了してしまう
  return fetchData().then(data => {
    expect(data).toBe("peanut butter");
  });
});

test("the fetch fails with an error", () => {
  // アサーションが1回行われることを確認しないと、間違ってresolveが実行されてしまった場合に
  // catchに処理が到達せず、テストになりません
  expect.assertions(1);
  // 必ずreturnしましょう、でないと、promiseの解決を待たずにテストが終了してしまう
  return fetchDataErr().catch(e => expect(e).toMatch("error"));
});

test("the data is peanut butter", () => {
  // resolvesマッチャーを使うことで、同じようにテストしてくれます
  return expect(fetchData()).resolves.toBe("peanut butter");
});

test("the fetch fails with an error", () => {
  // rejectsマッチャーを使うことで、同じようにテストしてくれます
  return expect(fetchDataErr()).rejects.toMatch("error");
});

Async/Await

// async, awaitを使って、同じようにテストすることもできます

test("the data is peanut butter", async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe("peanut butter");
});

test("the fetch fails with an error", async () => {
  expect.assertions(1);
  try {
    await fetchDataErr();
  } catch (e) {
    expect(e).toMatch("error");
  }
});

// resolves, rejectsマッチャーでも同じようにテストできます

test("the data is peanut butter", async () => {
  await expect(fetchData()).resolves.toBe("peanut butter");
});

test("the fetch fails with an error", async () => {
  await expect(fetchDataErr()).rejects.toMatch("error");
});

Setup and Teardown

https://jestjs.io/docs/en/setup-teardown

テストを実行する前、テストの実行後に実行してくれる関数

// 各テストの実行前に実行される関数
beforeEach(() => {
  console.log("beforeEach");
});

// 各テストの実行後に実行される関数
afterEach(() => {
  console.log("afterEach");
});

// ファイルの先頭で一回だけ実行される関数
beforeAll(() => {
  console.log("beforeAll");
});

// ファイルの最後で一回だけ実行される関数
afterAll(() => {
  console.log("afterAll");
});

test("city database has Vienna", () => {
  expect(true).toBeTruthy();
});

test("city database has San Juan", () => {
  expect(true).toBeTruthy();
});

// コンソールの出力↓
// beforeAll
// beforeEach
// afterEach
// beforeEach
// afterEach
// afterAll
// ファイルの先頭で一回だけ実行される関数
beforeAll(() => {
  // promiseが返る処理の場合はreturnしないといけない
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("beforeAll");
      resolve();
    }, 1000);
  });
});

// ファイルの最後で一回だけ実行される関数
afterAll(() => {
  // promiseが返る処理の場合はreturnしないといけない
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("afterAll");
      resolve();
    }, 1000);
  });
});

Scoping

describeブロックを使用してテストをまとめてグループ化することもできます。記述ブロック内にある場合、前後のブロックはその記述ブロック内のテストにのみ適用されます。

beforeAll(() => console.log("1 - beforeAll"));
afterAll(() => console.log("1 - afterAll"));
beforeEach(() => console.log("1 - beforeEach"));
afterEach(() => console.log("1 - afterEach"));
test("", () => console.log("1 - test"));
describe("Scoped / Nested block", () => {
  beforeAll(() => console.log("2 - beforeAll"));
  afterAll(() => console.log("2 - afterAll"));
  beforeEach(() => console.log("2 - beforeEach"));
  afterEach(() => console.log("2 - afterEach"));
  test("", () => console.log("2 - test"));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

Mock Functions

https://jestjs.io/docs/en/mock-functions

Using a mock function

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

test("using a mock function sample", () => {
  const mockCallback = jest.fn(x => 42 + x);
  forEach([0, 1], mockCallback);

  // モック関数が2回呼び出されたか
  expect(mockCallback.mock.calls.length).toBe(2);

  // 関数の最初の呼び出しの最初の引数が0か
  expect(mockCallback.mock.calls[0][0]).toBe(0);

  // 関数への2番目の呼び出しの最初の引数は1か
  expect(mockCallback.mock.calls[1][0]).toBe(1);

  // 関数への最初の呼び出しの戻り値は42か
  expect(mockCallback.mock.results[0].value).toBe(42);
});

Mock Return Values

テスト中にモック関数を使ってコードにテスト値を挿入することもできます。

test("mock return values sample", () => {
  const myMock = jest.fn();
  console.log(myMock()); // undefined

  myMock
    .mockReturnValueOnce(10)
    .mockReturnValueOnce("x")
    .mockReturnValue(true);

  console.log(myMock(), myMock(), myMock(), myMock()); // 10 'x' true true
});

Mocking Modules

users.js
import axios from "axios";

class Users {
  static all() {
    return axios.get("/users.json").then(resp => resp.data);
  }
}

export default Users;

↑のaxios.getをモックします

.getに対してmockResolvedValueを指定して、データを返すようにする↓

users.test.js
import axios from "axios";
import Users from "./users";

jest.mock("axios");

test("should fetch users", () => {
  const users = [{ name: "Bob" }];
  const resp = { data: users };
  axios.get.mockResolvedValue(resp);

  return Users.all().then(data => expect(data).toEqual(users));
});

Mock Implementations

戻り値を指定してモック関数の実装を完全に置き換える機能

test("mock implementations sample 1", () => {
  const myMockFn = jest.fn(cb => cb(null, true));

  myMockFn((err, val) => console.log(val));
});

mockImplementationメソッドは、他のモジュールから作成されたモック関数のデフォルト実装を定義する必要がある場合に便利です。

foo.js
module.exports = function() {
  // some implementation;
};
test("mock implementations sample 2", () => {
  jest.mock("./foo");
  const foo = require("./foo");
  foo.mockImplementation(() => 42);
  console.log(foo()); // 42
});

複数呼び出しで異なる結果とする場合

test("mock implementations sample 3", () => {
  const myMockFn = jest
    .fn()
    .mockImplementationOnce(cb => cb(null, true))
    .mockImplementationOnce(cb => cb(null, false));

  myMockFn((err, val) => console.log(val)); // true

  myMockFn((err, val) => console.log(val)); // false
});

mockImplementationOnceで定義された実装を使い果たした後は、デフォルトが実行される


通常チェーン化されたメソッドがある(したがって常にこれを返す必要がある)場合は、すべてのモックにも存在する.mockReturnThis()関数の形でこれを単純化するためのおすすめのAPIがあります。

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// ↑と同じ意味
const otherObj = {
  myMethod: jest.fn(function() {
    return this;
  }),
};

thisが返ってきていることが確認できました

test("mock implementations sample 5", () => {
  const myObj = {
    myMethod: jest.fn().mockReturnThis()
  };
  console.log(myObj.myMethod() === myObj); // true
});

Mock Names

テストでの出力時に表示されるモック関数名を指定でき、エラーが見やすくなる

  const myMockFn = jest
    .fn()
    .mockReturnValue("default")
    .mockImplementation(scalar => 42 + scalar)
    .mockName("add42");

Custom Matchers

test("custom matchers sample 1", () => {
  const mockFunc = jest.fn();
  mockFunc();
  expect(mockFunc).toBeCalled(); // モック関数が少なくとも一度呼び出されました

  let [arg1, arg2] = [1, 2];
  mockFunc(arg1, arg2);
  expect(mockFunc).toBeCalledWith(arg1, arg2); // 指定された引数を使用して、モック関数が少なくとも1回呼び出されました

  expect(mockFunc).lastCalledWith(arg1, arg2); // モック関数への最後の呼び出しは指定された引数で呼ばれました

  expect(mockFunc).toMatchSnapshot(); // すべての呼び出しとモックの名前はスナップショットとして書き込まれます
});

以下のように書いても↑のようにテストしてくれます

test("custom matchers sample 2", () => {
  const mockFunc = jest.fn().mockName("sampleFunc");
  mockFunc();
  expect(mockFunc.mock.calls.length).toBeGreaterThan(0); // モック関数が少なくとも一度呼び出されました

  let [arg1, arg2] = [1, 2];
  mockFunc(arg1, arg2);
  expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]); // 指定された引数を使用して、モック関数が少なくとも1回呼び出されました

  // モック関数への最後の呼び出しは指定された引数で呼ばれました
  expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
    arg1,
    arg2
  ]);

  expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(1); // モック関数への最後の呼び出しの最初の引数は `1`でした

  expect(mockFunc.getMockName()).toBe("sampleFunc"); // モックネーム
});

Snapshot Testing

https://jestjs.io/docs/en/snapshot-testing

スナップショットテストは、UIが予期せずに変更されないようにしたい場合に非常に便利なツールです。

Snapshot Testing with Jest

  • Reactコンポーネントをテストする例
Link.react.js
import React from "react";

const STATUS = {
  HOVERED: "hovered",
  NORMAL: "normal"
};

export default class Link extends React.Component {
  constructor() {
    super();

    this.state = {
      class: STATUS.NORMAL
    };

    this._onMouseEnter = this._onMouseEnter.bind(this);
    this._onMouseLeave = this._onMouseLeave.bind(this);
  }

  _onMouseEnter() {
    this.setState({ class: STATUS.HOVERED });
  }

  _onMouseLeave() {
    this.setState({ class: STATUS.NORMAL });
  }

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || "#"}
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
        {this.props.children}
      </a>
    );
  }
}
Link.test.js
import React from "react";
import Link from "./Link.react";
import renderer from "react-test-renderer";

it("renders correctly", () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});
yarn add -D react react-test-renderer @babel/preset-react
.babelrc
{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}
yarn run jest

↑を実行すると、./__snapshots__/Link.test.js.snapが自動生成され、以下のようなファイルになっています

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

スナップショットが一致しない場合、テストが失敗し、バグが発見しやすくなります。

Updating Snapshots

意図的にテストの内容を変更した場合

  import React from "react";
  import Link from "./Link.react";
  import renderer from "react-test-renderer";

  it("renders correctly", () => {
    const tree = renderer
-     .create(<Link page="http://www.facebook.com">Facebook</Link>)
+     .create(<Link page="http://www.instagram.com">Instagram</Link>)
      .toJSON();
    expect(tree).toMatchSnapshot();
  });

1.png

スナップショットが一致しなくなったためテストが通らなくなりました

変更が意図的なものであった場合はスナップショットを更新したらOK

yarn run jest --updateSnapshot #または、-u

Interactive Snapshot Mode

失敗したスナップショットは、監視モードでインタラクティブに更新することもできます。

yarn run jest --watch

2.png

uを押すと、失敗したスナップショットを更新してくれるようでした

Inline Snapshots

スナップショット値が自動的にソースコードに書き戻されることを除けば、インラインスナップショットは外部スナップショット(.snapファイル)と同じように動作します。 つまり、正しい値が書き込まれたことを確認するために外部ファイルに切り替える必要なしに、自動的に生成されたスナップショットの利点を享受することができます。

yarn add prettier
  1. 引数を付けずに.toMatchInlineSnapshot()を呼び出してテストを作成
it("renders correctly by prettier", () => {
  const tree = renderer
    .create(<Link page="https://prettier.io">Prettier</Link>)
    .toJSON();
  expect(tree).toMatchInlineSnapshot();
});
  1. ↑の状態で、yarn run jestを実行すると、

3.gif

スナップショットのファイルを作るのではなく、インラインで展開してくれたのを確認できました。

An Async Example

https://jestjs.io/docs/en/tutorial-async

JestでBabelサポートを有効にする必要があります -> https://jestjs.io/docs/en/getting-started#using-babel

user.js
import request from "./request";

export function getUserName(userID) {
  return request("/users/" + userID).then(user => user.name);
}
request.js
const http = require("http");

export default function request(url) {
  return new Promise(resolve => {
    // このモジュールを__mocks__/request.jsでモックします
    http.get({ path: url }, response => {
      let data = "";
      response.on("data", _data => (data += _data));
      response.on("end", () => resolve(data));
    });
  });
}

request.jsをモックします

__mocks__フォルダにモックのファイルを置く

__mocks__/request.js
const users = {
  4: { name: "Mark" },
  5: { name: "Paul" }
};

export default function request(url) {
  return new Promise((resolve, reject) => {
    const userID = parseInt(url.substr("/users/".length), 10);
    process.nextTick(() =>
      users[userID]
        ? resolve(users[userID])
        : reject({
            error: "User with " + userID + " not found."
          })
    );
  });
}
__tests__/user-test.js
jest.mock("../request");

import * as user from "../user";

// promiseが返る場合はreturnする必要がある
it("works with promises", () => {
  expect.assertions(1);
  return user.getUserName(4).then(data => expect(data).toEqual("Mark"));
});

__mocks__/request.jsを使用して、テストがうまく実行されたことを確認できました

1.png

.resolves

.resolvesマッチャーを使った例

it("works with resolves", () => {
  expect.assertions(1);
  return expect(user.getUserName(5)).resolves.toEqual("Paul");
});

async/awaitを使った例

it("works with async/await", async () => {
  expect.assertions(1);
  const data = await user.getUserName(4);
  expect(data).toEqual("Mark");
});

it("works with async/await and resolves", async () => {
  expect.assertions(1);
  await expect(user.getUserName(5)).resolves.toEqual("Paul");
});

Error handling

test("tests error with promises", () => {
  expect.assertions(1);
  return user.getUserName(2).catch(e =>
    expect(e).toEqual({
      error: "User with 2 not found."
    })
  );
});

it("tests error with async/await", async () => {
  expect.assertions(1);
  try {
    await user.getUserName(1);
  } catch (e) {
    expect(e).toEqual({
      error: "User with 1 not found."
    });
  }
});

.rejects

it("tests error with rejects", () => {
  expect.assertions(1);
  return expect(user.getUserName(3)).rejects.toEqual({
    error: "User with 3 not found."
  });
});

it("tests error with async/await and rejects", async () => {
  expect.assertions(1);
  await expect(user.getUserName(3)).rejects.toEqual({
    error: "User with 3 not found."
  });
});

Timer Mocks

https://jestjs.io/docs/en/timer-mocks

setTimeout、setIntervalの時間の経過をコントロールしてテストすることが可能

timerGame.js
"use strict";

function timerGame(callback) {
  console.log("Ready....go!");
  setTimeout(() => {
    console.log("Times up -- stop!");
    callback && callback();
  }, 1000);
}

module.exports = timerGame;
__tests__/timerGame-test.js
"use strict";

jest.useFakeTimers();

test("waits 1 second before ending the game", () => {
  const timerGame = require("../timerGame");
  timerGame();

  // setTimeoutが期待通りに実行されているかテスト
  expect(setTimeout).toHaveBeenCalledTimes(1);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

ここではjest.useFakeTimers();を呼び出して偽のタイマーを有効にします。 これはsetTimeoutと他のタイマー関数をモック関数で模擬します。 1つのファイルまたはディスクリプションブロック内で複数のテストを実行する場合は、jest.useFakeTimers(); 各テストの前に手動で、またはbeforeEachなどのセットアップ機能を使用して呼び出すことができます。 そうしないと、内部使用状況カウンターがリセットされません。

Run All Timers

timerGame.jsは↑と同じ

test("calls the callback after 1 second", () => {
  const timerGame = require("../timerGame");
  const callback = jest.fn();

  timerGame(callback);

  // この時点では、コールバックはまだ呼び出されていない
  expect(callback).not.toBeCalled();

  // すべてのタイマーが実行されるまで早送り
  jest.runAllTimers();

  // callbackが呼び出されている
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Run Pending Timers

再帰的にタイマー処理をする場合

infiniteTimerGame.js
"use strict";

function infiniteTimerGame(callback) {
  console.log("Ready....go!");

  setTimeout(() => {
    console.log("Times up! 10 seconds before the next game starts...");
    callback && callback();

    setTimeout(() => {
      infiniteTimerGame(callback);
    }, 10000);
  }, 1000);
}

module.exports = infiniteTimerGame;
__tests__/infiniteTimerGame-test.js
"use strict";

jest.useFakeTimers();

describe("infiniteTimerGame", () => {
  test("schedules a 10-second timer after 1 second", () => {
    const infiniteTimerGame = require("../infiniteTimerGame");
    const callback = jest.fn();

    infiniteTimerGame(callback);

    // モック関数が呼び出されたことを確認
    expect(setTimeout).toHaveBeenCalledTimes(1);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

    // 現在保留中のタイマーだけを早送りして使い尽くす
    jest.runOnlyPendingTimers();

    // この時点で、1秒タイマーがコールバックを起動している
    expect(callback).toBeCalled();

    // 新しいタイマーが作成されている
    expect(setTimeout).toHaveBeenCalledTimes(2);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
  });
});

Advance Timers by Time

it("calls the callback after 1 second via advanceTimersByTime", () => {
  const timerGame = require("../timerGame");
  const callback = jest.fn();

  timerGame(callback);

  expect(callback).not.toBeCalled();

  // タイマーを1秒進める
  jest.advanceTimersByTime(1000);

  // コールバックが呼び出されている確認
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Manual Mocks

https://jestjs.io/docs/en/manual-mocks

Mocking user modules

user.jsというファイルを作成し、それを__mocks__ディレクトリに配置すると、モックとして定義できます

※テストでそのモジュールが必要な場合は、明示的にjest.mock('./ moduleName')を呼び出す

ES6 Class Mocks

https://jestjs.io/docs/en/es6-class-mocks

Jestを使用して、テストしたいファイルにインポートされているES6クラスをモックすることができます

An ES6 Class Example

sound-player.js
export default class SoundPlayer {
  constructor() {
    this.foo = "bar";
  }

  playSoundFile(fileName) {
    console.log("Playing sound file " + fileName);
  }
}
sound-player-consumer.js
import SoundPlayer from "./sound-player";

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer();
  }

  playSomethingCool() {
    const coolSoundFileName = "song.mp3";
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}

↑をモックにしていきます

ES6クラスのモックを作成する4つの方法

1. Automatic mock

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player"); // モック作成

beforeEach(() => {
  // すべてのインスタンスをクリア
  SoundPlayer.mockClear();
});

it("We can check if the consumer called the class constructor", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("We can check if the consumer called a method on the class instance", () => {
  // mockClear()が機能しているため、まだ関数が実行されていないことを確認できる
  expect(SoundPlayer).not.toHaveBeenCalled();

  const soundPlayerConsumer = new SoundPlayerConsumer();
  // constructor が再度呼び出されている
  expect(SoundPlayer).toHaveBeenCalledTimes(1);

  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();

  const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
  const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  // ↑と同じ内容
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

2. Manual mock

モックの実装をmocksフォルダーに保存して手動モックを作成する

__mocks__/sound-player.js
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
  return { playSoundFile: mockPlaySoundFile };
});

export default mock;
import SoundPlayer, { mockPlaySoundFile } from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player"); // モック作成

beforeEach(() => {
  // すべてのインスタンスをクリア
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

it("We can check if the consumer called the class constructor", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("We can check if the consumer called a method on the class instance", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

3. Calling jest.mock() with the module factory parameter

モジュールファクトリパラメータを使ってjest.mock()を呼び出す

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
const mockPlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

it("We can check if the consumer called a method on the class instance", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

4. Replacing the mock using mockImplementation() or mockImplementationOnce()

既存のモックでmockImplementation()を呼び出すことによって、単一のテストまたはすべてのテストの実装を変更するために、上記のモックをすべて置き換えることができます。

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";

jest.mock("./sound-player");

describe("When SoundPlayer throws an error", () => {
  beforeAll(() => {
    SoundPlayer.mockImplementation(() => {
      return {
        playSoundFile: () => {
          throw new Error("Test error");
        }
      };
    });
  });

  it("Should throw an error when calling playSomethingCool", () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
  });
});

Globals

https://jestjs.io/docs/en/api

テストファイルで、Jestはこれらの各メソッドとオブジェクトをグローバル環境に配置します。使用するために何かを要求したりインポートしたりする必要はありません。

  • afterAll(fn, timeout)
  • afterEach(fn, timeout)
  • beforeAll(fn, timeout)
  • beforeEach(fn, timeout)
  • describe(name, fn)
  • describe.each(table)(name, fn, timeout)
  • describe.only(name, fn)
  • describe.only.each(table)(name, fn)
  • describe.skip(name, fn)
  • describe.skip.each(table)(name, fn)
  • test(name, fn, timeout)
  • test.each(table)(name, fn, timeout)
  • test.only(name, fn, timeout)
  • test.only.each(table)(name, fn)
  • test.skip(name, fn)
  • test.skip.each(table)(name, fn)
  • test.todo(name)

afterAll(fn, timeout)

このファイル内のすべてのテストが完了した後に実行される関数

afterAll(() => {
  console.log("afterAll");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  test 1
//  test 2
//  afterAll

afterEach(fn, timeout)

このファイルの各テストが完了した後に実行される関数

afterEach(() => {
  console.log("afterEach");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  test 1
//  afterEach
//  test 2
//  afterEach

beforeAll(fn, timeout)

このファイルのテストが実行される前に実行する関数

beforeAll(() => {
  console.log("beforeAll");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  beforeAll
//  test 1
//  test 2

beforeEach(fn, timeout)

このファイルの各テストが実行前に実行される関数

beforeEach(() => {
  console.log("beforeEach");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  beforeEach
//  test 1
//  beforeEach
//  test 2

describe(name, fn)

いくつかの関連テストをまとめたブロックを作成する

const myBeverage = {
  delicious: true,
  sour: false
};

describe("my beverage", () => {
  test("is delicious", () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test("is not sour", () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

describe.each(table)(name, fn, timeout)

データセットを渡すことで、複数テストを行える。

例えば足し算するだけのテストを、値だけ変えてテストしてくれることができる

describe.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected);
    });

    test(`returned value not be greater than ${expected}`, () => {
      expect(a + b).not.toBeGreaterThan(expected);
    });

    test(`returned value not be less than ${expected}`, () => {
      expect(a + b).not.toBeLessThan(expected);
    });
  }
);

describe.only(name, fn)

describeブロックを1つだけ実行したい場合は、describe.onlyを使用する

describe.only("my beverage", () => {
  test("is delicious", () => {
    expect(true).toBeTruthy();
  });

  test("is not sour", () => {
    expect(false).toBeFalsy();
  });
});

describe("my other beverage", () => {
  // ... will be skipped
});

describe.only.each(table)(name, fn)

データセットを渡すことで、複数テストを行い、かつdescribeブロックを1つだけ実行したい場合に使う

describe.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected);
    });
  }
);

test("will not be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

describe.skip(name, fn)

特定のブロックをスキップしたい場合に使う

describe("my beverage", () => {
  test("is delicious", () => {
    expect(true).toBeTruthy();
  });

  test("is not sour", () => {
    expect(false).toBeFalsy();
  });
});

describe.skip("my other beverage", () => {
  // ... will be skipped
});

describe.skip.each(table)(name, fn)

一連のデータセットのテストをスキップしたい場合に使う

describe.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected); // will not be ran
    });
  }
);

test("will be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test(name, fn, timeout)

テストを実行する

test("test", () => {
  expect(1 - 1).toBe(0);
});

// エイリアスのitを使ってもOK
it("it", () => {
  expect(1 - 1).toBe(0);
});

test.each(table)(name, fn, timeout)

データセットを渡すことで、複数テストを行える。

test.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  }
);

test.only(name, fn, timeout)

テストを1つだけ実行したい場合に使う

test.only("it is raining", () => {
  expect(1).toBeGreaterThan(0);
});

test("it is not snowing", () => {
  expect(0).toBe(0);
});

test.only.each(table)(name, fn)

データセットを渡すことで、複数テストを行い、テストを1つだけ実行したい場合に使う

test.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  }
);

test("will not be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test.skip(name, fn)

テストをスキップしたいときに使う

test("it is raining", () => {
  expect(1).toBeGreaterThan(0);
});

test.skip("it is not snowing", () => {
  expect(1).toBe(0);
});

test.skip.each(table)(name, fn)

一連のデータセットのテストをスキップしたい場合に使う

test.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected); // will not be ran
  }
);

test("will be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test.todo(name)

テストを書く予定がある場合は、test.toを使用してください。 これらのテストは最後のサマリー出力で強調表示されるので、まだいくつのテストが必要であるかがわかります。

test.todo("add should be associative");

1.png

Expect

https://jestjs.io/docs/en/expect

expect(value)

値をテストするたびに、expect関数を使用する。値について何かをアサートするためにマッチャー関数と共にexpectを使用する。

it("test", () => {
  expect(1 + 2).toBe(3);
});

expect.extend(matchers)

マッチャーを自作できる。toBeWithinRangeを自作した例↓

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false
      };
    }
  }
});

test("numeric ranges", () => {
  expect(100).toBeWithinRange(90, 110);
  expect(101).not.toBeWithinRange(0, 100);
  expect({ apples: 6, bananas: 3 }).toEqual({
    apples: expect.toBeWithinRange(1, 10),
    bananas: expect.not.toBeWithinRange(11, 20)
  });
});

expect.anything()

nullまたは未定義以外のものにマッチする。

toEqualまたはtoBeCalledWithでリテラル値の代わりに使うことができる

モック関数がnull以外の引数で呼び出されることを確認したい場合は、次のようにする

test("map calls its argument with a non-null argument", () => {
  const mock = jest.fn();
  [1].map(x => mock(x));
  expect(mock).toBeCalledWith(expect.anything());
});

expect.any(constructor)

toEqualまたはtoBeCalledWithでリテラル値の代わりに使うことができる

モック関数が番号付きで呼び出されていることを確認したい場合は、次のようにする

function randocall(fn) {
  return fn(Math.floor(Math.random() * 6 + 1));
}

test("randocall calls its callback with a number", () => {
  const mock = jest.fn();
  randocall(mock);
  expect(mock).toBeCalledWith(expect.any(Number));
});

expect.arrayContaining(array)

配列内の全ての要素を含む配列と一致する

describe("Beware of a misunderstanding! A sequence of dice rolls", () => {
  const expected = [1, 2, 3, 4, 5, 6];
  it("matches even with an unexpected number 7", () => {
    expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual(
      expect.arrayContaining(expected) // 1, 2, 3, 4, 5, 6が含まれているので一致
    );
  });
  it("does not match without an expected number 2", () => {
    expect([4, 1, 6, 7, 3, 5, 7, 5, 4, 6]).not.toEqual(
      expect.arrayContaining(expected) // 2が含まれていないので一致しない
    );
  });
});

expect.assertions(number)

テスト中に一定数のアサーションが呼び出されることを確認する

test("assertions sample", () => {
  expect.assertions(2);
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

expect.hasAssertions()

テスト中に少なくとも1つのアサーションが呼び出されることを確認する

test("hasAssertions sample", () => {
  expect.hasAssertions();
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

expect.not.arrayContaining(array)

配列内の値が一致していないことを確認する

test("not arrayContaining sample", () => {
  expect(["Alice", "Bob", "Eve"]).toEqual(
    expect.not.arrayContaining(["Samantha"])
  );
});

expect.not.objectContaining(object)

期待されるプロパティに再帰的にマッチしないかを確認する

test("not objectContaining sample", () => {
  expect({ bar: "baz" }).toEqual(expect.not.objectContaining({ foo: "bar" }));
});

expect.not.stringContaining(string)

文字列を一致しないか確認する

test("not stringContaining sample", () => {
  expect("How are you?").toEqual(expect.not.stringContaining("Hello world!"));
});

expect.not.stringMatching(string | regexp)

受け取った値が文字列ではない、または期待される文字列または正規表現と一致しないか確認する

test("not stringMatching sample", () => {
  expect("How are you?").toEqual(expect.not.stringMatching(/Hello world!/));
});

expect.objectContaining(object)

期待されるプロパティに再帰的にマッチするかを確認する

test("objectContaining sample", () => {
  expect({ bar: "baz", foo: "foo" }).toEqual(
    expect.objectContaining({ bar: "baz" })
  );
});

expect.stringContaining(string)

文字列を含む文字列であるか確認する

test("stringContaining sample", () => {
  expect("How are you?").toEqual(expect.stringContaining("you"));
});

expect.stringMatching(string | regexp)

文字列または正規表現に一致する文字列かを確認する

test("stringMatching sample", () => {
  expect("Roberto").toEqual(expect.stringMatching(/^[BR]ob/));
});

.not

反対をテストする

test("not sample", () => {
  expect("abc").not.toBe("def");
});

.resolves

Promiseのresolveを確認する場合

test("resolves to lemon", () => {
  return expect(Promise.resolve("lemon")).resolves.toBe("lemon");
});

test("resolves to lemon", async () => {
  await expect(Promise.resolve("lemon")).resolves.toBe("lemon");
  await expect(Promise.resolve("lemon")).resolves.not.toBe("octopus");
});

.rejects

Promiseのrejectを確認する場合

test("rejects to octopus", () => {
  return expect(Promise.reject(new Error("octopus"))).rejects.toThrow(
    "octopus"
  );
});

test("rejects to octopus", async () => {
  await expect(Promise.reject(new Error("octopus"))).rejects.toThrow("octopus");
});

.toBe(value)

プリミティブ値などを確認する

const can = {
  name: "pamplemousse",
  ounces: 12
};

describe("the can", () => {
  test("has 12 ounces", () => {
    expect(can.ounces).toBe(12);
  });

  test("has a sophisticated name", () => {
    expect(can.name).toBe("pamplemousse");
  });
});

.toHaveBeenCalled()

モック関数が呼び出されたことを確認

function drinkAll(callback, flavour) {
  if (flavour !== "octopus") {
    callback(flavour);
  }
}

describe("drinkAll", () => {
  test("drinks something lemon-flavoured", () => {
    const drink = jest.fn();
    drinkAll(drink, "lemon");
    expect(drink).toHaveBeenCalled();
  });

  test("does not drink something octopus-flavoured", () => {
    const drink = jest.fn();
    drinkAll(drink, "octopus");
    expect(drink).not.toHaveBeenCalled();
  });
});

.toHaveBeenCalledTimes(number)

モック関数が正確な数呼び出されたことを確認

test("toHaveBeenCalledTimes sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenCalledTimes(2);
});

.toHaveBeenCalledWith(arg1, arg2, ...)

モック関数が特定の引数で呼び出されたことを確認

test("toHaveBeenCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenCalledWith("lemon", 0, ["lemon", "octopus"]);
});

.toHaveBeenLastCalledWith(arg1, arg2, ...)

モック関数が最後に呼び出された時の引数を確認

test("toHaveBeenLastCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenLastCalledWith("octopus", 1, ["lemon", "octopus"]);
});

.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)

モック関数のn番目に呼び出された時の引数を確認

test("toHaveBeenNthCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenNthCalledWith(1, "lemon", 0, ["lemon", "octopus"]);
  expect(drink).toHaveBeenNthCalledWith(2, "octopus", 1, ["lemon", "octopus"]);
});

.toHaveReturned()

モック関数が少なくとも1回正常に戻った(つまり、エラーをスローしなかった)ことを確認

test("toHaveReturned sample", () => {
  const drink = jest.fn(() => true);
  drink();
  expect(drink).toHaveReturned();
});

.toHaveReturnedTimes(number)

モック関数が正常に戻った正確な回数を確認

test("toHaveReturnedTimes sample", () => {
  const drink = jest.fn(() => true);
  drink();
  drink();
  expect(drink).toHaveReturnedTimes(2);
});

.toHaveReturnedWith(value)

モック関数が特定の値を返すか確認

test("toHaveReturnedWith sample", () => {
  const beverage = { name: "La Croix" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage);

  expect(drink).toHaveReturnedWith("La Croix");
});

.toHaveLastReturnedWith(value)

モック関数が最後に返した値が特定の値か確認

test("toHaveLastReturnedWith sample", () => {
  const beverage1 = { name: "La Croix (Lemon)" };
  const beverage2 = { name: "La Croix (Orange)" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage1);
  drink(beverage2);

  expect(drink).toHaveLastReturnedWith("La Croix (Orange)");
});

.toHaveNthReturnedWith(nthCall, value)

モック関数がn番目に返した値が特定の値か確認

test("toHaveNthReturnedWith sample", () => {
  const beverage1 = { name: "La Croix (Lemon)" };
  const beverage2 = { name: "La Croix (Orange)" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage1);
  drink(beverage2);

  expect(drink).toHaveNthReturnedWith(1, "La Croix (Lemon)");
  expect(drink).toHaveNthReturnedWith(2, "La Croix (Orange)");
});

.toBeCloseTo(number, numDigits?)

浮動小数点数を確認

test("toBeCloseTo sample", () => {
  // expect(0.2 + 0.1).toBe(0.3); // 0.3ではなく、0.30000000000000004でテストがうまくいかない
  expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
});

.toBeDefined()

未定義でないことを確認

test("toBeDefined sample", () => {
  function fetchNewFlavorIdea() {
    return true; // もし何も返さなければテストは失敗する
  }
  expect(fetchNewFlavorIdea()).toBeDefined();
});

.toBeFalsy()

falseと評価されるか確認する

test("toBeFalsy sample", () => {
  expect(0).toBeFalsy();
  expect("").toBeFalsy();
  expect(false).toBeFalsy();
});

.toBeGreaterThan(number)

超えるか確認

test("toBeGreaterThan sample", () => {
  expect(11).toBeGreaterThan(10);
});

.toBeGreaterThanOrEqual(number)

以上か確認

test("toBeGreaterThanOrEqual sample", () => {
  expect(10).toBeGreaterThanOrEqual(10);
  expect(11).toBeGreaterThanOrEqual(10);
});

.toBeLessThan(number)

未満か確認

test("toBeLessThan sample", () => {
  expect(9).toBeLessThan(10);
});

.toBeLessThanOrEqual(number)

以下か

test("toBeLessThanOrEqual sample", () => {
  expect(9).toBeLessThanOrEqual(10);
  expect(10).toBeLessThanOrEqual(10);
});

.toBeInstanceOf(Class)

オブジェクトがクラスのインスタンスであることを確認

test("toBeInstanceOf sample", () => {
  class A {}
  expect(new A()).toBeInstanceOf(A);
  expect(() => {}).toBeInstanceOf(Function);
});

.toBeNull()

nullか確認

test("toBeNull sample", () => {
  expect(null).toBeNull();
});

.toBeTruthy()

trueと評価されるか

test("toBeTruthy sample", () => {
  expect(1).toBeTruthy();
  expect("a").toBeTruthy();
  expect(true).toBeTruthy();
});

.toBeUndefined()

undefindeか確認

test("toBeUndefined sample", () => {
  expect({}.hoge).toBeUndefined();
});

.toBeNaN()

NaNか確認

test("toBeNaN sample", () => {
  expect("a" / "b").toBeNaN();
});

.toContain(item)

項目が配列内にあるか確認

test("toContain sample", () => {
  expect([1, 2, 3]).toContain(2);
});

.toContainEqual(item)

特定の構造と値を持つ項目が配列に含まれていることを確認

test("toContainEqual sample", () => {
  expect([
    { delicious: true, sour: false },
    { hoge: false, fuga: false }
  ]).toContainEqual({ delicious: true, sour: false });
});

.toEqual(value)

オブジェクトインスタンスのすべてのプロパティを再帰的に比較

test("toEqual sample", () => {
  expect({ flavor: "grapefruit", ounces: 12 }).toEqual({
    flavor: "grapefruit",
    ounces: 12
  });
});

.toHaveLength(number)

.lengthプロパティが特定の数値か確認

test("toHaveLength sample", () => {
  expect([1, 2, 3]).toHaveLength(3);
  expect("abc").toHaveLength(3);
  expect("").not.toHaveLength(5);
});

.toMatch(regexpOrString)

文字列が正規表現と一致するか確認

test("toMatch sample", () => {
  expect("abcgrapefruitxyz").toMatch(/grapefruit/);
  expect("abcgrapefruitxyz").toMatch(new RegExp("grapefruit"));
});

.toMatchObject(object)

オブジェクトのプロパティを確認

test("toMatchObject sample", () => {
  const houseForSale = {
    bath: true,
    bedrooms: 4,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      area: 20,
      wallColor: "white"
    }
  };
  const desiredHouse = {
    bath: true,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      wallColor: expect.stringMatching(/white|yellow/)
    }
  };
  expect(houseForSale).toMatchObject(desiredHouse);
});

.toHaveProperty(keyPath, value?)

指定のkeyPathがオブジェクトに存在するか確認

test("toHaveProperty sample", () => {
  const houseForSale = {
    bath: true,
    bedrooms: 4,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      area: 20,
      wallColor: "white",
      "nice.oven": true
    },
    "ceiling.height": 2
  };
  expect(houseForSale).toHaveProperty("bath");
  expect(houseForSale).toHaveProperty("bedrooms", 4);
  expect(houseForSale).not.toHaveProperty("pool");
  expect(houseForSale).toHaveProperty("kitchen.area", 20);
  expect(houseForSale).toHaveProperty("kitchen.amenities", [
    "oven",
    "stove",
    "washer"
  ]);
});

.toMatchSnapshot(propertyMatchers?, hint?)

スナップショットが一致している確認

test("toMatchSnapshot sample", () => {
  const myMockFn = jest
    .fn()
    .mockReturnValue("default")
    .mockImplementation(scalar => 42 + scalar)
    .mockName("add42");
  expect(myMockFn).toMatchSnapshot();
});

.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)

__snapshots__フォルダ内ではなく、インラインにスナップショットを書き込んで一致を確認

1.gif

.toStrictEqual(value)

オブジェクトの構造と型が同じであることをテストします。

test("toStrictEqual sample", () => {
  class LaCroix {
    constructor(flavor) {
      this.flavor = flavor;
    }
  }
  expect(new LaCroix("lemon")).toEqual({ flavor: "lemon" });
  expect(new LaCroix("lemon")).not.toStrictEqual({ flavor: "lemon" });
});

.toThrow(error?)

関数が呼び出されたときにスローされることをテストする

test("toThrow sample", () => {
  expect(() => {
    throw new Error();
  }).toThrow();
});

.toThrowErrorMatchingSnapshot(hint?)

エラーをスナップショットで確認

test("toThrowErrorMatchingSnapshot sample", () => {
  expect(() => {
    throw new Error("hogehoge");
  }).toThrowErrorMatchingSnapshot();
});

.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)

エラーをインラインスナップショットで確認

2.gif

Mock Functions

https://jestjs.io/docs/en/mock-function-api

jest.fn()でモック関数を作成できる

mockFn.getMockName()

モック名文字列を返す

  const a = jest.fn();
  console.log(a.getMockName()); // "jest.fn()"
  const b = jest.fn().mockName("hoge");
  console.log(b.getMockName()); // "hoge"

mockFn.mock.calls

このモック関数に対して行われたすべての呼び出しの呼び出し引数を含む配列

  const c = jest.fn();
  c("a", 1);
  c("b", 2);
  console.log(c.mock.calls); // [ [ 'a', 1 ], [ 'b', 2 ] ]

mockFn.mock.results

このモック関数に対して行われたすべての呼び出しの結果を含む配列

  const d = jest.fn(x => x + 1);
  d(1);
  d(2);
  console.log(d.mock.results); // [ { type: 'return', value: 2 }, { type: 'return', value: 3 } ]

mockFn.mock.instances

newを使用してこのモック関数からインスタンス化されたすべてのオブジェクトインスタンスを含む配列

  const e = jest.fn();
  const mock1 = new e();
  const mock2 = new e();
  console.log(e.mock.instances[0] === mock1); // true
  console.log(e.mock.instances[1] === mock2); // true

mockFn.mockClear()

mockFn.mock.callsおよびmockFn.mock.instances配列に格納されているすべての情報をリセットします

  const f = jest.fn();
  console.log(f.mock.calls); // []
  f(1, 2, 3);
  console.log(f.mock.calls); // [ [ 1, 2, 3 ] ]
  f.mockClear();
  console.log(f.mock.calls); // []

mockFn.mockImplementation(fn)

  const h = jest.fn().mockImplementation(scalar => 42 + scalar);
  // or: jest.fn(scalar => 42 + scalar);
  console.log(h(0) === 42); // true
  console.log(h(1) === 43); // true
  console.log(h.mock.calls[0][0] === 0); // true
  console.log(h.mock.calls[1][0] === 1); // true

mockFn.mockImplementationOnce(fn)

複数の関数呼び出しが異なる結果を生み出すように連鎖することができる

  const i = jest
    .fn()
    .mockImplementationOnce(cb => cb(null, true))
    .mockImplementationOnce(cb => cb(null, false));
  i((err, val) => console.log(val)); // true
  i((err, val) => console.log(val)); // false

mockFn.mockName(value)

モック関数を示す文字列を設定する

  const j = jest.fn().mockName("fuga");
  console.log(j.getMockName()); // "fuga"

mockFn.mockReturnThis()

↓と同じ意味

  jest.fn(function() {
    return this;
  });

mockFn.mockReturnValue(value)

モック関数の返り値を設定できる

  const k = jest.fn();
  k.mockReturnValue(42);
  console.log(k()); // 42
  k.mockReturnValue(43);
  console.log(k()); // 43

mockFn.mockReturnValueOnce(value)

モック関数の1回のみの返り値を設定できる

  const l = jest
    .fn()
    .mockReturnValue("default")
    .mockReturnValueOnce("first call")
    .mockReturnValueOnce("second call");

  // 'first call', 'second call', 'default', 'default'
  console.log(l(), l(), l(), l());

mockFn.mockResolvedValue(value)

↓と同じ意味

jest.fn().mockImplementation(() => Promise.resolve(value));

mockFn.mockResolvedValueOnce(value)

↓と同じ意味

jest.fn().mockImplementationOnce(() => Promise.resolve(value));

mockFn.mockRejectedValue(value)

↓と同じ意味

jest.fn().mockImplementation(() => Promise.reject(value));

mockFn.mockRejectedValueOnce(value)

↓と同じ意味

jest.fn().mockImplementationOnce(() => Promise.reject(value));

test("async test", async () => {
  const m = jest.fn().mockResolvedValue(43);
  console.log(await m()); // 43

  const n = jest
    .fn()
    .mockResolvedValue("default")
    .mockResolvedValueOnce("first call")
    .mockResolvedValueOnce("second call");
  console.log(await n()); // first call
  console.log(await n()); // second call
  console.log(await n()); // default
  console.log(await n()); // default

  const o = jest.fn().mockRejectedValue(new Error("Async error"));
  try {
    await o();
  } catch (e) {
    console.log(e.message); // Async error
  }

  const p = jest
    .fn()
    .mockResolvedValueOnce("first call")
    .mockRejectedValueOnce(new Error("Async error"));
  try {
    console.log(await p()); // first call
    await p();
  } catch (e) {
    console.log(e.message); // Async error
  }
});

読んでいただいてありがとうございましたm(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jest(v24.6)の公式ドキュメント読んで基本を勉強してみた

Jestの公式ドキュメントの(自分的には)基本の部分を読んで勉強した内容を投稿させていただきます:bow:

私が個人的に勉強した部分を書いていくだけなので、詳細に説明しているというわけでもなく、網羅的にやったというわけでもなく、本当にただの私のブログですので、注意してください。
間違いなどありましたら、ご指摘お願いします

また、この記事中で私が試してみたコードは全て こちら にあります

Introduction

Getting Started

https://jestjs.io/docs/en/getting-started

jestのInstall

yarn add -D jest
# または
npm i -D yarn

2つの数を追加する関数のテストを書いてみる

sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
sum.test.js
const sum = require("./sum");

test("adds 1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

この状態で、yarn run jest

または、 npx jest とする、

image.png

jestを使ったテストができました


または、以下のようにpackage.jsonnpm script を追加して実行してもOK

package.json
{
  "scripts": {
    "test": "jest"
  }
}

Running from command line

設定ファイルとしてconfig.jsonを使用し、実行後にネイティブOSの通知を表示しながら、my-testに一致するファイルに対してJestを実行する方法は次のとおりです。

jest my-test --notify --config=config.json

Additional Configuration

以下のコマンドで基本設定ファイルを生成できる

yarn run jest --init

Using Babel

babelを使う設定

yarn add -D babel-jest @babel/core @babel/preset-env
.babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
sum.js
  function sum(a, b) {
    return a + b;
  }
- module.exports = sum;
+ export default sum;
sum.test.js
- const sum = require("./sum");
+ import sum from "./sum";

  test("adds 1 + 2 to equal 3", () => {
    expect(sum(1, 2)).toBe(3);
  });

image.png

babelを使用して、import、exportを使っていても問題なくテストができました。

Using Matchers

https://jestjs.io/docs/en/using-matchers

Common Matchers

test("two plus two is four", () => {
  // expect(2 + 2)と.toBe(4)で等しいことをテストしてくれます
  expect(2 + 2).toBe(4);
});

test("object assignment", () => {
  const data = { one: 1 };
  data["two"] = 2;
  // オブジェクトの値を確認したい場合は、toBeの代わりにtoEqualを使用する
  expect(data).toEqual({ one: 1, two: 2 });
});

test("numbers is not zero", () => {
  // 反対をテストすることもできます
  expect(2 + 1).not.toBe(0);
});

Truthiness

テストでは、undefinednull、およびfalseを区別する必要がある場合がありますが、これらを異なる方法で扱いたくない場合があります。 Jestにはあなたが欲しいものについてあなたが明確になることを可能にするヘルパーが含まれています。

  • toBeNull -> null にのみ一致
  • toBeUndefined -> undefined
  • toBeDefined -> toBeUndefinedの反対
  • toBeTruthy -> ifでtrueと見なすものすべてに一致する
  • toBeFalsy -> ifでfalseと見なすものすべてに一致する
test("null", () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test("zero", () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});

Numbers

test("two plus two", () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // 等しいか確認
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

test("adding floating point numbers", () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3); <- 丸め誤差が原因でうまくテストできない
  expect(value).toBeCloseTo(0.3); // This works.
});

Strings

// toMatchを使って文字列を正規表現と照合することができる

test("there is no I in team", () => {
  expect("team").not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect("Christoph").toMatch(/stop/);
});

Arrays and iterables

// toContainを使用して、配列または反復可能オブジェクトに特定の項目が含まれているかどうかを確認できる

const shoppingList = [
  "diapers",
  "kleenex",
  "trash bags",
  "paper towels",
  "beer"
];

test("the shopping list has beer on it", () => {
  expect(shoppingList).toContain("beer");
  expect(new Set(shoppingList)).toContain("beer");
});

Exceptions

// 特定の関数が呼び出されたときにエラーをスローすることをテストしたい場合は、toThrowを使用する

function compileAndroidCode() {
  throw new Error("you are using the wrong JDK");
}

test("compiling android goes as expected", () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(Error);

  // 正確なエラーメッセージや正規表現を使うこともできます
  expect(compileAndroidCode).toThrow("you are using the wrong JDK");
  expect(compileAndroidCode).toThrow(/JDK/);
});

Testing Asynchronous Code

https://jestjs.io/docs/en/asynchronous

Callbacks

function fetchData(callback) {
  // 非同期処理を実行 -> しかし1秒待ってくれずにテストが終了してしまうので、このテストはうまくいかない
  setTimeout(() => callback("peanut butter"), 1000);
}

test("the data is peanut butter", () => {
  function callback(data) {
    // コールバック関数の引数の文字列が「peanut butter」かテスト
    expect(data).toBe("peanut butter");
  }

  fetchData(callback);
});

このテストを想定通りに動かすには、テストを空の引数を持つ関数に入れる代わりに、doneという単一の引数を使用する。テストを終了する前に、Jestはdoneコールバックが呼び出されるまで待機します

function fetchData(callback) {
  setTimeout(() => callback("peanut butter"), 1000);
}

test("the data is peanut butter", done => {
  function callback(data) {
    expect(data).toBe("peanut butter");
    done();
  }

  fetchData(callback);
});

Promises

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve("peanut butter"), 1000);
  });
}

function fetchDataErr() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("error"), 1000);
  });
}

test("the data is peanut butter", () => {
  // 必ずreturnしましょう、でないと、promiseの解決を待たずにテストが終了してしまう
  return fetchData().then(data => {
    expect(data).toBe("peanut butter");
  });
});

test("the fetch fails with an error", () => {
  // アサーションが1回行われることを確認しないと、間違ってresolveが実行されてしまった場合に
  // catchに処理が到達せず、テストになりません
  expect.assertions(1);
  // 必ずreturnしましょう、でないと、promiseの解決を待たずにテストが終了してしまう
  return fetchDataErr().catch(e => expect(e).toMatch("error"));
});

test("the data is peanut butter", () => {
  // resolvesマッチャーを使うことで、同じようにテストしてくれます
  return expect(fetchData()).resolves.toBe("peanut butter");
});

test("the fetch fails with an error", () => {
  // rejectsマッチャーを使うことで、同じようにテストしてくれます
  return expect(fetchDataErr()).rejects.toMatch("error");
});

Async/Await

// async, awaitを使って、同じようにテストすることもできます

test("the data is peanut butter", async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe("peanut butter");
});

test("the fetch fails with an error", async () => {
  expect.assertions(1);
  try {
    await fetchDataErr();
  } catch (e) {
    expect(e).toMatch("error");
  }
});

// resolves, rejectsマッチャーでも同じようにテストできます

test("the data is peanut butter", async () => {
  await expect(fetchData()).resolves.toBe("peanut butter");
});

test("the fetch fails with an error", async () => {
  await expect(fetchDataErr()).rejects.toMatch("error");
});

Setup and Teardown

https://jestjs.io/docs/en/setup-teardown

テストを実行する前、テストの実行後に実行してくれる関数

// 各テストの実行前に実行される関数
beforeEach(() => {
  console.log("beforeEach");
});

// 各テストの実行後に実行される関数
afterEach(() => {
  console.log("afterEach");
});

// ファイルの先頭で一回だけ実行される関数
beforeAll(() => {
  console.log("beforeAll");
});

// ファイルの最後で一回だけ実行される関数
afterAll(() => {
  console.log("afterAll");
});

test("city database has Vienna", () => {
  expect(true).toBeTruthy();
});

test("city database has San Juan", () => {
  expect(true).toBeTruthy();
});

// コンソールの出力↓
// beforeAll
// beforeEach
// afterEach
// beforeEach
// afterEach
// afterAll
// ファイルの先頭で一回だけ実行される関数
beforeAll(() => {
  // promiseが返る処理の場合はreturnしないといけない
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("beforeAll");
      resolve();
    }, 1000);
  });
});

// ファイルの最後で一回だけ実行される関数
afterAll(() => {
  // promiseが返る処理の場合はreturnしないといけない
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("afterAll");
      resolve();
    }, 1000);
  });
});

Scoping

describeブロックを使用してテストをまとめてグループ化することもできます。記述ブロック内にある場合、前後のブロックはその記述ブロック内のテストにのみ適用されます。

beforeAll(() => console.log("1 - beforeAll"));
afterAll(() => console.log("1 - afterAll"));
beforeEach(() => console.log("1 - beforeEach"));
afterEach(() => console.log("1 - afterEach"));
test("", () => console.log("1 - test"));
describe("Scoped / Nested block", () => {
  beforeAll(() => console.log("2 - beforeAll"));
  afterAll(() => console.log("2 - afterAll"));
  beforeEach(() => console.log("2 - beforeEach"));
  afterEach(() => console.log("2 - afterEach"));
  test("", () => console.log("2 - test"));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

Mock Functions

https://jestjs.io/docs/en/mock-functions

Using a mock function

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

test("using a mock function sample", () => {
  const mockCallback = jest.fn(x => 42 + x);
  forEach([0, 1], mockCallback);

  // モック関数が2回呼び出されたか
  expect(mockCallback.mock.calls.length).toBe(2);

  // 関数の最初の呼び出しの最初の引数が0か
  expect(mockCallback.mock.calls[0][0]).toBe(0);

  // 関数への2番目の呼び出しの最初の引数は1か
  expect(mockCallback.mock.calls[1][0]).toBe(1);

  // 関数への最初の呼び出しの戻り値は42か
  expect(mockCallback.mock.results[0].value).toBe(42);
});

Mock Return Values

テスト中にモック関数を使ってコードにテスト値を挿入することもできます。

test("mock return values sample", () => {
  const myMock = jest.fn();
  console.log(myMock()); // undefined

  myMock
    .mockReturnValueOnce(10)
    .mockReturnValueOnce("x")
    .mockReturnValue(true);

  console.log(myMock(), myMock(), myMock(), myMock()); // 10 'x' true true
});

Mocking Modules

users.js
import axios from "axios";

class Users {
  static all() {
    return axios.get("/users.json").then(resp => resp.data);
  }
}

export default Users;

↑のaxios.getをモックします

.getに対してmockResolvedValueを指定して、データを返すようにする↓

users.test.js
import axios from "axios";
import Users from "./users";

jest.mock("axios");

test("should fetch users", () => {
  const users = [{ name: "Bob" }];
  const resp = { data: users };
  axios.get.mockResolvedValue(resp);

  return Users.all().then(data => expect(data).toEqual(users));
});

Mock Implementations

戻り値を指定してモック関数の実装を完全に置き換える機能

test("mock implementations sample 1", () => {
  const myMockFn = jest.fn(cb => cb(null, true));

  myMockFn((err, val) => console.log(val));
});

mockImplementationメソッドは、他のモジュールから作成されたモック関数のデフォルト実装を定義する必要がある場合に便利です。

foo.js
module.exports = function() {
  // some implementation;
};
test("mock implementations sample 2", () => {
  jest.mock("./foo");
  const foo = require("./foo");
  foo.mockImplementation(() => 42);
  console.log(foo()); // 42
});

複数呼び出しで異なる結果とする場合

test("mock implementations sample 3", () => {
  const myMockFn = jest
    .fn()
    .mockImplementationOnce(cb => cb(null, true))
    .mockImplementationOnce(cb => cb(null, false));

  myMockFn((err, val) => console.log(val)); // true

  myMockFn((err, val) => console.log(val)); // false
});

mockImplementationOnceで定義された実装を使い果たした後は、デフォルトが実行される


通常チェーン化されたメソッドがある(したがって常にこれを返す必要がある)場合は、すべてのモックにも存在する.mockReturnThis()関数の形でこれを単純化するためのおすすめのAPIがあります。

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// ↑と同じ意味
const otherObj = {
  myMethod: jest.fn(function() {
    return this;
  }),
};

thisが返ってきていることが確認できました

test("mock implementations sample 5", () => {
  const myObj = {
    myMethod: jest.fn().mockReturnThis()
  };
  console.log(myObj.myMethod() === myObj); // true
});

Mock Names

テストでの出力時に表示されるモック関数名を指定でき、エラーが見やすくなる

  const myMockFn = jest
    .fn()
    .mockReturnValue("default")
    .mockImplementation(scalar => 42 + scalar)
    .mockName("add42");

Custom Matchers

test("custom matchers sample 1", () => {
  const mockFunc = jest.fn();
  mockFunc();
  expect(mockFunc).toBeCalled(); // モック関数が少なくとも一度呼び出されました

  let [arg1, arg2] = [1, 2];
  mockFunc(arg1, arg2);
  expect(mockFunc).toBeCalledWith(arg1, arg2); // 指定された引数を使用して、モック関数が少なくとも1回呼び出されました

  expect(mockFunc).lastCalledWith(arg1, arg2); // モック関数への最後の呼び出しは指定された引数で呼ばれました

  expect(mockFunc).toMatchSnapshot(); // すべての呼び出しとモックの名前はスナップショットとして書き込まれます
});

以下のように書いても↑のようにテストしてくれます

test("custom matchers sample 2", () => {
  const mockFunc = jest.fn().mockName("sampleFunc");
  mockFunc();
  expect(mockFunc.mock.calls.length).toBeGreaterThan(0); // モック関数が少なくとも一度呼び出されました

  let [arg1, arg2] = [1, 2];
  mockFunc(arg1, arg2);
  expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]); // 指定された引数を使用して、モック関数が少なくとも1回呼び出されました

  // モック関数への最後の呼び出しは指定された引数で呼ばれました
  expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
    arg1,
    arg2
  ]);

  expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(1); // モック関数への最後の呼び出しの最初の引数は `1`でした

  expect(mockFunc.getMockName()).toBe("sampleFunc"); // モックネーム
});

Snapshot Testing

https://jestjs.io/docs/en/snapshot-testing

スナップショットテストは、UIが予期せずに変更されないようにしたい場合に非常に便利なツールです。

Snapshot Testing with Jest

  • Reactコンポーネントをテストする例
Link.react.js
import React from "react";

const STATUS = {
  HOVERED: "hovered",
  NORMAL: "normal"
};

export default class Link extends React.Component {
  constructor() {
    super();

    this.state = {
      class: STATUS.NORMAL
    };

    this._onMouseEnter = this._onMouseEnter.bind(this);
    this._onMouseLeave = this._onMouseLeave.bind(this);
  }

  _onMouseEnter() {
    this.setState({ class: STATUS.HOVERED });
  }

  _onMouseLeave() {
    this.setState({ class: STATUS.NORMAL });
  }

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || "#"}
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
        {this.props.children}
      </a>
    );
  }
}
Link.test.js
import React from "react";
import Link from "./Link.react";
import renderer from "react-test-renderer";

it("renders correctly", () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});
yarn add -D react react-test-renderer @babel/preset-react
.babelrc
{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}
yarn run jest

↑を実行すると、./__snapshots__/Link.test.js.snapが自動生成され、以下のようなファイルになっています

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

スナップショットが一致しない場合、テストが失敗し、バグが発見しやすくなります。

Updating Snapshots

意図的にテストの内容を変更した場合

  import React from "react";
  import Link from "./Link.react";
  import renderer from "react-test-renderer";

  it("renders correctly", () => {
    const tree = renderer
-     .create(<Link page="http://www.facebook.com">Facebook</Link>)
+     .create(<Link page="http://www.instagram.com">Instagram</Link>)
      .toJSON();
    expect(tree).toMatchSnapshot();
  });

1.png

スナップショットが一致しなくなったためテストが通らなくなりました

変更が意図的なものであった場合はスナップショットを更新したらOK

yarn run jest --updateSnapshot #または、-u

Interactive Snapshot Mode

失敗したスナップショットは、監視モードでインタラクティブに更新することもできます。

yarn run jest --watch

2.png

uを押すと、失敗したスナップショットを更新してくれるようでした

Inline Snapshots

スナップショット値が自動的にソースコードに書き戻されることを除けば、インラインスナップショットは外部スナップショット(.snapファイル)と同じように動作します。 つまり、正しい値が書き込まれたことを確認するために外部ファイルに切り替える必要なしに、自動的に生成されたスナップショットの利点を享受することができます。

yarn add prettier
  1. 引数を付けずに.toMatchInlineSnapshot()を呼び出してテストを作成
it("renders correctly by prettier", () => {
  const tree = renderer
    .create(<Link page="https://prettier.io">Prettier</Link>)
    .toJSON();
  expect(tree).toMatchInlineSnapshot();
});
  1. ↑の状態で、yarn run jestを実行すると、

3.gif

スナップショットのファイルを作るのではなく、インラインで展開してくれたのを確認できました。

An Async Example

https://jestjs.io/docs/en/tutorial-async

JestでBabelサポートを有効にする必要があります -> https://jestjs.io/docs/en/getting-started#using-babel

user.js
import request from "./request";

export function getUserName(userID) {
  return request("/users/" + userID).then(user => user.name);
}
request.js
const http = require("http");

export default function request(url) {
  return new Promise(resolve => {
    // このモジュールを__mocks__/request.jsでモックします
    http.get({ path: url }, response => {
      let data = "";
      response.on("data", _data => (data += _data));
      response.on("end", () => resolve(data));
    });
  });
}

request.jsをモックします

__mocks__フォルダにモックのファイルを置く

__mocks__/request.js
const users = {
  4: { name: "Mark" },
  5: { name: "Paul" }
};

export default function request(url) {
  return new Promise((resolve, reject) => {
    const userID = parseInt(url.substr("/users/".length), 10);
    process.nextTick(() =>
      users[userID]
        ? resolve(users[userID])
        : reject({
            error: "User with " + userID + " not found."
          })
    );
  });
}
__tests__/user-test.js
jest.mock("../request");

import * as user from "../user";

// promiseが返る場合はreturnする必要がある
it("works with promises", () => {
  expect.assertions(1);
  return user.getUserName(4).then(data => expect(data).toEqual("Mark"));
});

__mocks__/request.jsを使用して、テストがうまく実行されたことを確認できました

1.png

.resolves

.resolvesマッチャーを使った例

it("works with resolves", () => {
  expect.assertions(1);
  return expect(user.getUserName(5)).resolves.toEqual("Paul");
});

async/awaitを使った例

it("works with async/await", async () => {
  expect.assertions(1);
  const data = await user.getUserName(4);
  expect(data).toEqual("Mark");
});

it("works with async/await and resolves", async () => {
  expect.assertions(1);
  await expect(user.getUserName(5)).resolves.toEqual("Paul");
});

Error handling

test("tests error with promises", () => {
  expect.assertions(1);
  return user.getUserName(2).catch(e =>
    expect(e).toEqual({
      error: "User with 2 not found."
    })
  );
});

it("tests error with async/await", async () => {
  expect.assertions(1);
  try {
    await user.getUserName(1);
  } catch (e) {
    expect(e).toEqual({
      error: "User with 1 not found."
    });
  }
});

.rejects

it("tests error with rejects", () => {
  expect.assertions(1);
  return expect(user.getUserName(3)).rejects.toEqual({
    error: "User with 3 not found."
  });
});

it("tests error with async/await and rejects", async () => {
  expect.assertions(1);
  await expect(user.getUserName(3)).rejects.toEqual({
    error: "User with 3 not found."
  });
});

Timer Mocks

https://jestjs.io/docs/en/timer-mocks

setTimeout、setIntervalの時間の経過をコントロールしてテストすることが可能

timerGame.js
"use strict";

function timerGame(callback) {
  console.log("Ready....go!");
  setTimeout(() => {
    console.log("Times up -- stop!");
    callback && callback();
  }, 1000);
}

module.exports = timerGame;
__tests__/timerGame-test.js
"use strict";

jest.useFakeTimers();

test("waits 1 second before ending the game", () => {
  const timerGame = require("../timerGame");
  timerGame();

  // setTimeoutが期待通りに実行されているかテスト
  expect(setTimeout).toHaveBeenCalledTimes(1);
  expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

ここではjest.useFakeTimers();を呼び出して偽のタイマーを有効にします。 これはsetTimeoutと他のタイマー関数をモック関数で模擬します。 1つのファイルまたはディスクリプションブロック内で複数のテストを実行する場合は、jest.useFakeTimers(); 各テストの前に手動で、またはbeforeEachなどのセットアップ機能を使用して呼び出すことができます。 そうしないと、内部使用状況カウンターがリセットされません。

Run All Timers

timerGame.jsは↑と同じ

test("calls the callback after 1 second", () => {
  const timerGame = require("../timerGame");
  const callback = jest.fn();

  timerGame(callback);

  // この時点では、コールバックはまだ呼び出されていない
  expect(callback).not.toBeCalled();

  // すべてのタイマーが実行されるまで早送り
  jest.runAllTimers();

  // callbackが呼び出されている
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Run Pending Timers

再帰的にタイマー処理をする場合

infiniteTimerGame.js
"use strict";

function infiniteTimerGame(callback) {
  console.log("Ready....go!");

  setTimeout(() => {
    console.log("Times up! 10 seconds before the next game starts...");
    callback && callback();

    setTimeout(() => {
      infiniteTimerGame(callback);
    }, 10000);
  }, 1000);
}

module.exports = infiniteTimerGame;
__tests__/infiniteTimerGame-test.js
"use strict";

jest.useFakeTimers();

describe("infiniteTimerGame", () => {
  test("schedules a 10-second timer after 1 second", () => {
    const infiniteTimerGame = require("../infiniteTimerGame");
    const callback = jest.fn();

    infiniteTimerGame(callback);

    // モック関数が呼び出されたことを確認
    expect(setTimeout).toHaveBeenCalledTimes(1);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

    // 現在保留中のタイマーだけを早送りして使い尽くす
    jest.runOnlyPendingTimers();

    // この時点で、1秒タイマーがコールバックを起動している
    expect(callback).toBeCalled();

    // 新しいタイマーが作成されている
    expect(setTimeout).toHaveBeenCalledTimes(2);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
  });
});

Advance Timers by Time

it("calls the callback after 1 second via advanceTimersByTime", () => {
  const timerGame = require("../timerGame");
  const callback = jest.fn();

  timerGame(callback);

  expect(callback).not.toBeCalled();

  // タイマーを1秒進める
  jest.advanceTimersByTime(1000);

  // コールバックが呼び出されている確認
  expect(callback).toBeCalled();
  expect(callback).toHaveBeenCalledTimes(1);
});

Manual Mocks

https://jestjs.io/docs/en/manual-mocks

Mocking user modules

user.jsというファイルを作成し、それを__mocks__ディレクトリに配置すると、モックとして定義できます

※テストでそのモジュールが必要な場合は、明示的にjest.mock('./ moduleName')を呼び出す

ES6 Class Mocks

https://jestjs.io/docs/en/es6-class-mocks

Jestを使用して、テストしたいファイルにインポートされているES6クラスをモックすることができます

An ES6 Class Example

sound-player.js
export default class SoundPlayer {
  constructor() {
    this.foo = "bar";
  }

  playSoundFile(fileName) {
    console.log("Playing sound file " + fileName);
  }
}
sound-player-consumer.js
import SoundPlayer from "./sound-player";

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer();
  }

  playSomethingCool() {
    const coolSoundFileName = "song.mp3";
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}

↑をモックにしていきます

ES6クラスのモックを作成する4つの方法

1. Automatic mock

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player"); // モック作成

beforeEach(() => {
  // すべてのインスタンスをクリア
  SoundPlayer.mockClear();
});

it("We can check if the consumer called the class constructor", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("We can check if the consumer called a method on the class instance", () => {
  // mockClear()が機能しているため、まだ関数が実行されていないことを確認できる
  expect(SoundPlayer).not.toHaveBeenCalled();

  const soundPlayerConsumer = new SoundPlayerConsumer();
  // constructor が再度呼び出されている
  expect(SoundPlayer).toHaveBeenCalledTimes(1);

  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();

  const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
  const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
  // ↑と同じ内容
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
  expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
});

2. Manual mock

モックの実装をmocksフォルダーに保存して手動モックを作成する

__mocks__/sound-player.js
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
  return { playSoundFile: mockPlaySoundFile };
});

export default mock;
import SoundPlayer, { mockPlaySoundFile } from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
jest.mock("./sound-player"); // モック作成

beforeEach(() => {
  // すべてのインスタンスをクリア
  SoundPlayer.mockClear();
  mockPlaySoundFile.mockClear();
});

it("We can check if the consumer called the class constructor", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

it("We can check if the consumer called a method on the class instance", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

3. Calling jest.mock() with the module factory parameter

モジュールファクトリパラメータを使ってjest.mock()を呼び出す

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";
const mockPlaySoundFile = jest.fn();
jest.mock("./sound-player", () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

it("We can check if the consumer called a method on the class instance", () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = "song.mp3";
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

4. Replacing the mock using mockImplementation() or mockImplementationOnce()

既存のモックでmockImplementation()を呼び出すことによって、単一のテストまたはすべてのテストの実装を変更するために、上記のモックをすべて置き換えることができます。

import SoundPlayer from "./sound-player";
import SoundPlayerConsumer from "./sound-player-consumer";

jest.mock("./sound-player");

describe("When SoundPlayer throws an error", () => {
  beforeAll(() => {
    SoundPlayer.mockImplementation(() => {
      return {
        playSoundFile: () => {
          throw new Error("Test error");
        }
      };
    });
  });

  it("Should throw an error when calling playSomethingCool", () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
  });
});

Globals

https://jestjs.io/docs/en/api

テストファイルで、Jestはこれらの各メソッドとオブジェクトをグローバル環境に配置します。使用するために何かを要求したりインポートしたりする必要はありません。

  • afterAll(fn, timeout)
  • afterEach(fn, timeout)
  • beforeAll(fn, timeout)
  • beforeEach(fn, timeout)
  • describe(name, fn)
  • describe.each(table)(name, fn, timeout)
  • describe.only(name, fn)
  • describe.only.each(table)(name, fn)
  • describe.skip(name, fn)
  • describe.skip.each(table)(name, fn)
  • test(name, fn, timeout)
  • test.each(table)(name, fn, timeout)
  • test.only(name, fn, timeout)
  • test.only.each(table)(name, fn)
  • test.skip(name, fn)
  • test.skip.each(table)(name, fn)
  • test.todo(name)

afterAll(fn, timeout)

このファイル内のすべてのテストが完了した後に実行される関数

afterAll(() => {
  console.log("afterAll");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  test 1
//  test 2
//  afterAll

afterEach(fn, timeout)

このファイルの各テストが完了した後に実行される関数

afterEach(() => {
  console.log("afterEach");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  test 1
//  afterEach
//  test 2
//  afterEach

beforeAll(fn, timeout)

このファイルのテストが実行される前に実行する関数

beforeAll(() => {
  console.log("beforeAll");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  beforeAll
//  test 1
//  test 2

beforeEach(fn, timeout)

このファイルの各テストが実行前に実行される関数

beforeEach(() => {
  console.log("beforeEach");
});

test("test 1", () => {
  console.log("test 1");
});

test("test 2", () => {
  console.log("test 2");
});

// 出力
//  beforeEach
//  test 1
//  beforeEach
//  test 2

describe(name, fn)

いくつかの関連テストをまとめたブロックを作成する

const myBeverage = {
  delicious: true,
  sour: false
};

describe("my beverage", () => {
  test("is delicious", () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test("is not sour", () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});

describe.each(table)(name, fn, timeout)

データセットを渡すことで、複数テストを行える。

例えば足し算するだけのテストを、値だけ変えてテストしてくれることができる

describe.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected);
    });

    test(`returned value not be greater than ${expected}`, () => {
      expect(a + b).not.toBeGreaterThan(expected);
    });

    test(`returned value not be less than ${expected}`, () => {
      expect(a + b).not.toBeLessThan(expected);
    });
  }
);

describe.only(name, fn)

describeブロックを1つだけ実行したい場合は、describe.onlyを使用する

describe.only("my beverage", () => {
  test("is delicious", () => {
    expect(true).toBeTruthy();
  });

  test("is not sour", () => {
    expect(false).toBeFalsy();
  });
});

describe("my other beverage", () => {
  // ... will be skipped
});

describe.only.each(table)(name, fn)

データセットを渡すことで、複数テストを行い、かつdescribeブロックを1つだけ実行したい場合に使う

describe.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected);
    });
  }
);

test("will not be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

describe.skip(name, fn)

特定のブロックをスキップしたい場合に使う

describe("my beverage", () => {
  test("is delicious", () => {
    expect(true).toBeTruthy();
  });

  test("is not sour", () => {
    expect(false).toBeFalsy();
  });
});

describe.skip("my other beverage", () => {
  // ... will be skipped
});

describe.skip.each(table)(name, fn)

一連のデータセットのテストをスキップしたい場合に使う

describe.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    test(`returns ${expected}`, () => {
      expect(a + b).toBe(expected); // will not be ran
    });
  }
);

test("will be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test(name, fn, timeout)

テストを実行する

test("test", () => {
  expect(1 - 1).toBe(0);
});

// エイリアスのitを使ってもOK
it("it", () => {
  expect(1 - 1).toBe(0);
});

test.each(table)(name, fn, timeout)

データセットを渡すことで、複数テストを行える。

test.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  }
);

test.only(name, fn, timeout)

テストを1つだけ実行したい場合に使う

test.only("it is raining", () => {
  expect(1).toBeGreaterThan(0);
});

test("it is not snowing", () => {
  expect(0).toBe(0);
});

test.only.each(table)(name, fn)

データセットを渡すことで、複数テストを行い、テストを1つだけ実行したい場合に使う

test.only.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  }
);

test("will not be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test.skip(name, fn)

テストをスキップしたいときに使う

test("it is raining", () => {
  expect(1).toBeGreaterThan(0);
});

test.skip("it is not snowing", () => {
  expect(1).toBe(0);
});

test.skip.each(table)(name, fn)

一連のデータセットのテストをスキップしたい場合に使う

test.skip.each([[1, 1, 2], [1, 2, 3], [2, 1, 3]])(
  ".add(%i, %i)",
  (a, b, expected) => {
    expect(a + b).toBe(expected); // will not be ran
  }
);

test("will be ran", () => {
  expect(1 / 0).toBe(Infinity);
});

test.todo(name)

テストを書く予定がある場合は、test.toを使用してください。 これらのテストは最後のサマリー出力で強調表示されるので、まだいくつのテストが必要であるかがわかります。

test.todo("add should be associative");

1.png

Expect

https://jestjs.io/docs/en/expect

expect(value)

値をテストするたびに、expect関数を使用する。値について何かをアサートするためにマッチャー関数と共にexpectを使用する。

it("test", () => {
  expect(1 + 2).toBe(3);
});

expect.extend(matchers)

マッチャーを自作できる。toBeWithinRangeを自作した例↓

expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be within range ${floor} - ${ceiling}`,
        pass: true
      };
    } else {
      return {
        message: () =>
          `expected ${received} to be within range ${floor} - ${ceiling}`,
        pass: false
      };
    }
  }
});

test("numeric ranges", () => {
  expect(100).toBeWithinRange(90, 110);
  expect(101).not.toBeWithinRange(0, 100);
  expect({ apples: 6, bananas: 3 }).toEqual({
    apples: expect.toBeWithinRange(1, 10),
    bananas: expect.not.toBeWithinRange(11, 20)
  });
});

expect.anything()

nullまたは未定義以外のものにマッチする。

toEqualまたはtoBeCalledWithでリテラル値の代わりに使うことができる

モック関数がnull以外の引数で呼び出されることを確認したい場合は、次のようにする

test("map calls its argument with a non-null argument", () => {
  const mock = jest.fn();
  [1].map(x => mock(x));
  expect(mock).toBeCalledWith(expect.anything());
});

expect.any(constructor)

toEqualまたはtoBeCalledWithでリテラル値の代わりに使うことができる

モック関数が番号付きで呼び出されていることを確認したい場合は、次のようにする

function randocall(fn) {
  return fn(Math.floor(Math.random() * 6 + 1));
}

test("randocall calls its callback with a number", () => {
  const mock = jest.fn();
  randocall(mock);
  expect(mock).toBeCalledWith(expect.any(Number));
});

expect.arrayContaining(array)

配列内の全ての要素を含む配列と一致する

describe("Beware of a misunderstanding! A sequence of dice rolls", () => {
  const expected = [1, 2, 3, 4, 5, 6];
  it("matches even with an unexpected number 7", () => {
    expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual(
      expect.arrayContaining(expected) // 1, 2, 3, 4, 5, 6が含まれているので一致
    );
  });
  it("does not match without an expected number 2", () => {
    expect([4, 1, 6, 7, 3, 5, 7, 5, 4, 6]).not.toEqual(
      expect.arrayContaining(expected) // 2が含まれていないので一致しない
    );
  });
});

expect.assertions(number)

テスト中に一定数のアサーションが呼び出されることを確認する

test("assertions sample", () => {
  expect.assertions(2);
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

expect.hasAssertions()

テスト中に少なくとも1つのアサーションが呼び出されることを確認する

test("hasAssertions sample", () => {
  expect.hasAssertions();
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
});

expect.not.arrayContaining(array)

配列内の値が一致していないことを確認する

test("not arrayContaining sample", () => {
  expect(["Alice", "Bob", "Eve"]).toEqual(
    expect.not.arrayContaining(["Samantha"])
  );
});

expect.not.objectContaining(object)

期待されるプロパティに再帰的にマッチしないかを確認する

test("not objectContaining sample", () => {
  expect({ bar: "baz" }).toEqual(expect.not.objectContaining({ foo: "bar" }));
});

expect.not.stringContaining(string)

文字列を一致しないか確認する

test("not stringContaining sample", () => {
  expect("How are you?").toEqual(expect.not.stringContaining("Hello world!"));
});

expect.not.stringMatching(string | regexp)

受け取った値が文字列ではない、または期待される文字列または正規表現と一致しないか確認する

test("not stringMatching sample", () => {
  expect("How are you?").toEqual(expect.not.stringMatching(/Hello world!/));
});

expect.objectContaining(object)

期待されるプロパティに再帰的にマッチするかを確認する

test("objectContaining sample", () => {
  expect({ bar: "baz", foo: "foo" }).toEqual(
    expect.objectContaining({ bar: "baz" })
  );
});

expect.stringContaining(string)

文字列を含む文字列であるか確認する

test("stringContaining sample", () => {
  expect("How are you?").toEqual(expect.stringContaining("you"));
});

expect.stringMatching(string | regexp)

文字列または正規表現に一致する文字列かを確認する

test("stringMatching sample", () => {
  expect("Roberto").toEqual(expect.stringMatching(/^[BR]ob/));
});

.not

反対をテストする

test("not sample", () => {
  expect("abc").not.toBe("def");
});

.resolves

Promiseのresolveを確認する場合

test("resolves to lemon", () => {
  return expect(Promise.resolve("lemon")).resolves.toBe("lemon");
});

test("resolves to lemon", async () => {
  await expect(Promise.resolve("lemon")).resolves.toBe("lemon");
  await expect(Promise.resolve("lemon")).resolves.not.toBe("octopus");
});

.rejects

Promiseのrejectを確認する場合

test("rejects to octopus", () => {
  return expect(Promise.reject(new Error("octopus"))).rejects.toThrow(
    "octopus"
  );
});

test("rejects to octopus", async () => {
  await expect(Promise.reject(new Error("octopus"))).rejects.toThrow("octopus");
});

.toBe(value)

プリミティブ値などを確認する

const can = {
  name: "pamplemousse",
  ounces: 12
};

describe("the can", () => {
  test("has 12 ounces", () => {
    expect(can.ounces).toBe(12);
  });

  test("has a sophisticated name", () => {
    expect(can.name).toBe("pamplemousse");
  });
});

.toHaveBeenCalled()

モック関数が呼び出されたことを確認

function drinkAll(callback, flavour) {
  if (flavour !== "octopus") {
    callback(flavour);
  }
}

describe("drinkAll", () => {
  test("drinks something lemon-flavoured", () => {
    const drink = jest.fn();
    drinkAll(drink, "lemon");
    expect(drink).toHaveBeenCalled();
  });

  test("does not drink something octopus-flavoured", () => {
    const drink = jest.fn();
    drinkAll(drink, "octopus");
    expect(drink).not.toHaveBeenCalled();
  });
});

.toHaveBeenCalledTimes(number)

モック関数が正確な数呼び出されたことを確認

test("toHaveBeenCalledTimes sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenCalledTimes(2);
});

.toHaveBeenCalledWith(arg1, arg2, ...)

モック関数が特定の引数で呼び出されたことを確認

test("toHaveBeenCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenCalledWith("lemon", 0, ["lemon", "octopus"]);
});

.toHaveBeenLastCalledWith(arg1, arg2, ...)

モック関数が最後に呼び出された時の引数を確認

test("toHaveBeenLastCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenLastCalledWith("octopus", 1, ["lemon", "octopus"]);
});

.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)

モック関数のn番目に呼び出された時の引数を確認

test("toHaveBeenNthCalledWith sample", () => {
  const drink = jest.fn();
  ["lemon", "octopus"].map(drink);
  expect(drink).toHaveBeenNthCalledWith(1, "lemon", 0, ["lemon", "octopus"]);
  expect(drink).toHaveBeenNthCalledWith(2, "octopus", 1, ["lemon", "octopus"]);
});

.toHaveReturned()

モック関数が少なくとも1回正常に戻った(つまり、エラーをスローしなかった)ことを確認

test("toHaveReturned sample", () => {
  const drink = jest.fn(() => true);
  drink();
  expect(drink).toHaveReturned();
});

.toHaveReturnedTimes(number)

モック関数が正常に戻った正確な回数を確認

test("toHaveReturnedTimes sample", () => {
  const drink = jest.fn(() => true);
  drink();
  drink();
  expect(drink).toHaveReturnedTimes(2);
});

.toHaveReturnedWith(value)

モック関数が特定の値を返すか確認

test("toHaveReturnedWith sample", () => {
  const beverage = { name: "La Croix" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage);

  expect(drink).toHaveReturnedWith("La Croix");
});

.toHaveLastReturnedWith(value)

モック関数が最後に返した値が特定の値か確認

test("toHaveLastReturnedWith sample", () => {
  const beverage1 = { name: "La Croix (Lemon)" };
  const beverage2 = { name: "La Croix (Orange)" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage1);
  drink(beverage2);

  expect(drink).toHaveLastReturnedWith("La Croix (Orange)");
});

.toHaveNthReturnedWith(nthCall, value)

モック関数がn番目に返した値が特定の値か確認

test("toHaveNthReturnedWith sample", () => {
  const beverage1 = { name: "La Croix (Lemon)" };
  const beverage2 = { name: "La Croix (Orange)" };
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage1);
  drink(beverage2);

  expect(drink).toHaveNthReturnedWith(1, "La Croix (Lemon)");
  expect(drink).toHaveNthReturnedWith(2, "La Croix (Orange)");
});

.toBeCloseTo(number, numDigits?)

浮動小数点数を確認

test("toBeCloseTo sample", () => {
  // expect(0.2 + 0.1).toBe(0.3); // 0.3ではなく、0.30000000000000004でテストがうまくいかない
  expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
});

.toBeDefined()

未定義でないことを確認

test("toBeDefined sample", () => {
  function fetchNewFlavorIdea() {
    return true; // もし何も返さなければテストは失敗する
  }
  expect(fetchNewFlavorIdea()).toBeDefined();
});

.toBeFalsy()

falseと評価されるか確認する

test("toBeFalsy sample", () => {
  expect(0).toBeFalsy();
  expect("").toBeFalsy();
  expect(false).toBeFalsy();
});

.toBeGreaterThan(number)

超えるか確認

test("toBeGreaterThan sample", () => {
  expect(11).toBeGreaterThan(10);
});

.toBeGreaterThanOrEqual(number)

以上か確認

test("toBeGreaterThanOrEqual sample", () => {
  expect(10).toBeGreaterThanOrEqual(10);
  expect(11).toBeGreaterThanOrEqual(10);
});

.toBeLessThan(number)

未満か確認

test("toBeLessThan sample", () => {
  expect(9).toBeLessThan(10);
});

.toBeLessThanOrEqual(number)

以下か

test("toBeLessThanOrEqual sample", () => {
  expect(9).toBeLessThanOrEqual(10);
  expect(10).toBeLessThanOrEqual(10);
});

.toBeInstanceOf(Class)

オブジェクトがクラスのインスタンスであることを確認

test("toBeInstanceOf sample", () => {
  class A {}
  expect(new A()).toBeInstanceOf(A);
  expect(() => {}).toBeInstanceOf(Function);
});

.toBeNull()

nullか確認

test("toBeNull sample", () => {
  expect(null).toBeNull();
});

.toBeTruthy()

trueと評価されるか

test("toBeTruthy sample", () => {
  expect(1).toBeTruthy();
  expect("a").toBeTruthy();
  expect(true).toBeTruthy();
});

.toBeUndefined()

undefindeか確認

test("toBeUndefined sample", () => {
  expect({}.hoge).toBeUndefined();
});

.toBeNaN()

NaNか確認

test("toBeNaN sample", () => {
  expect("a" / "b").toBeNaN();
});

.toContain(item)

項目が配列内にあるか確認

test("toContain sample", () => {
  expect([1, 2, 3]).toContain(2);
});

.toContainEqual(item)

特定の構造と値を持つ項目が配列に含まれていることを確認

test("toContainEqual sample", () => {
  expect([
    { delicious: true, sour: false },
    { hoge: false, fuga: false }
  ]).toContainEqual({ delicious: true, sour: false });
});

.toEqual(value)

オブジェクトインスタンスのすべてのプロパティを再帰的に比較

test("toEqual sample", () => {
  expect({ flavor: "grapefruit", ounces: 12 }).toEqual({
    flavor: "grapefruit",
    ounces: 12
  });
});

.toHaveLength(number)

.lengthプロパティが特定の数値か確認

test("toHaveLength sample", () => {
  expect([1, 2, 3]).toHaveLength(3);
  expect("abc").toHaveLength(3);
  expect("").not.toHaveLength(5);
});

.toMatch(regexpOrString)

文字列が正規表現と一致するか確認

test("toMatch sample", () => {
  expect("abcgrapefruitxyz").toMatch(/grapefruit/);
  expect("abcgrapefruitxyz").toMatch(new RegExp("grapefruit"));
});

.toMatchObject(object)

オブジェクトのプロパティを確認

test("toMatchObject sample", () => {
  const houseForSale = {
    bath: true,
    bedrooms: 4,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      area: 20,
      wallColor: "white"
    }
  };
  const desiredHouse = {
    bath: true,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      wallColor: expect.stringMatching(/white|yellow/)
    }
  };
  expect(houseForSale).toMatchObject(desiredHouse);
});

.toHaveProperty(keyPath, value?)

指定のkeyPathがオブジェクトに存在するか確認

test("toHaveProperty sample", () => {
  const houseForSale = {
    bath: true,
    bedrooms: 4,
    kitchen: {
      amenities: ["oven", "stove", "washer"],
      area: 20,
      wallColor: "white",
      "nice.oven": true
    },
    "ceiling.height": 2
  };
  expect(houseForSale).toHaveProperty("bath");
  expect(houseForSale).toHaveProperty("bedrooms", 4);
  expect(houseForSale).not.toHaveProperty("pool");
  expect(houseForSale).toHaveProperty("kitchen.area", 20);
  expect(houseForSale).toHaveProperty("kitchen.amenities", [
    "oven",
    "stove",
    "washer"
  ]);
});

.toMatchSnapshot(propertyMatchers?, hint?)

スナップショットが一致している確認

test("toMatchSnapshot sample", () => {
  const myMockFn = jest
    .fn()
    .mockReturnValue("default")
    .mockImplementation(scalar => 42 + scalar)
    .mockName("add42");
  expect(myMockFn).toMatchSnapshot();
});

.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)

__snapshots__フォルダ内ではなく、インラインにスナップショットを書き込んで一致を確認

1.gif

.toStrictEqual(value)

オブジェクトの構造と型が同じであることをテストします。

test("toStrictEqual sample", () => {
  class LaCroix {
    constructor(flavor) {
      this.flavor = flavor;
    }
  }
  expect(new LaCroix("lemon")).toEqual({ flavor: "lemon" });
  expect(new LaCroix("lemon")).not.toStrictEqual({ flavor: "lemon" });
});

.toThrow(error?)

関数が呼び出されたときにスローされることをテストする

test("toThrow sample", () => {
  expect(() => {
    throw new Error();
  }).toThrow();
});

.toThrowErrorMatchingSnapshot(hint?)

エラーをスナップショットで確認

test("toThrowErrorMatchingSnapshot sample", () => {
  expect(() => {
    throw new Error("hogehoge");
  }).toThrowErrorMatchingSnapshot();
});

.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)

エラーをインラインスナップショットで確認

2.gif

Mock Functions

https://jestjs.io/docs/en/mock-function-api

jest.fn()でモック関数を作成できる

mockFn.getMockName()

モック名文字列を返す

  const a = jest.fn();
  console.log(a.getMockName()); // "jest.fn()"
  const b = jest.fn().mockName("hoge");
  console.log(b.getMockName()); // "hoge"

mockFn.mock.calls

このモック関数に対して行われたすべての呼び出しの呼び出し引数を含む配列

  const c = jest.fn();
  c("a", 1);
  c("b", 2);
  console.log(c.mock.calls); // [ [ 'a', 1 ], [ 'b', 2 ] ]

mockFn.mock.results

このモック関数に対して行われたすべての呼び出しの結果を含む配列

  const d = jest.fn(x => x + 1);
  d(1);
  d(2);
  console.log(d.mock.results); // [ { type: 'return', value: 2 }, { type: 'return', value: 3 } ]

mockFn.mock.instances

newを使用してこのモック関数からインスタンス化されたすべてのオブジェクトインスタンスを含む配列

  const e = jest.fn();
  const mock1 = new e();
  const mock2 = new e();
  console.log(e.mock.instances[0] === mock1); // true
  console.log(e.mock.instances[1] === mock2); // true

mockFn.mockClear()

mockFn.mock.callsおよびmockFn.mock.instances配列に格納されているすべての情報をリセットします

  const f = jest.fn();
  console.log(f.mock.calls); // []
  f(1, 2, 3);
  console.log(f.mock.calls); // [ [ 1, 2, 3 ] ]
  f.mockClear();
  console.log(f.mock.calls); // []

mockFn.mockImplementation(fn)

  const h = jest.fn().mockImplementation(scalar => 42 + scalar);
  // or: jest.fn(scalar => 42 + scalar);
  console.log(h(0) === 42); // true
  console.log(h(1) === 43); // true
  console.log(h.mock.calls[0][0] === 0); // true
  console.log(h.mock.calls[1][0] === 1); // true

mockFn.mockImplementationOnce(fn)

複数の関数呼び出しが異なる結果を生み出すように連鎖することができる

  const i = jest
    .fn()
    .mockImplementationOnce(cb => cb(null, true))
    .mockImplementationOnce(cb => cb(null, false));
  i((err, val) => console.log(val)); // true
  i((err, val) => console.log(val)); // false

mockFn.mockName(value)

モック関数を示す文字列を設定する

  const j = jest.fn().mockName("fuga");
  console.log(j.getMockName()); // "fuga"

mockFn.mockReturnThis()

↓と同じ意味

  jest.fn(function() {
    return this;
  });

mockFn.mockReturnValue(value)

モック関数の返り値を設定できる

  const k = jest.fn();
  k.mockReturnValue(42);
  console.log(k()); // 42
  k.mockReturnValue(43);
  console.log(k()); // 43

mockFn.mockReturnValueOnce(value)

モック関数の1回のみの返り値を設定できる

  const l = jest
    .fn()
    .mockReturnValue("default")
    .mockReturnValueOnce("first call")
    .mockReturnValueOnce("second call");

  // 'first call', 'second call', 'default', 'default'
  console.log(l(), l(), l(), l());

mockFn.mockResolvedValue(value)

↓と同じ意味

jest.fn().mockImplementation(() => Promise.resolve(value));

mockFn.mockResolvedValueOnce(value)

↓と同じ意味

jest.fn().mockImplementationOnce(() => Promise.resolve(value));

mockFn.mockRejectedValue(value)

↓と同じ意味

jest.fn().mockImplementation(() => Promise.reject(value));

mockFn.mockRejectedValueOnce(value)

↓と同じ意味

jest.fn().mockImplementationOnce(() => Promise.reject(value));

test("async test", async () => {
  const m = jest.fn().mockResolvedValue(43);
  console.log(await m()); // 43

  const n = jest
    .fn()
    .mockResolvedValue("default")
    .mockResolvedValueOnce("first call")
    .mockResolvedValueOnce("second call");
  console.log(await n()); // first call
  console.log(await n()); // second call
  console.log(await n()); // default
  console.log(await n()); // default

  const o = jest.fn().mockRejectedValue(new Error("Async error"));
  try {
    await o();
  } catch (e) {
    console.log(e.message); // Async error
  }

  const p = jest
    .fn()
    .mockResolvedValueOnce("first call")
    .mockRejectedValueOnce(new Error("Async error"));
  try {
    console.log(await p()); // first call
    await p();
  } catch (e) {
    console.log(e.message); // Async error
  }
});

読んでいただいてありがとうございましたm(_ _)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む