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

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

Webpacker を使っている Rails アプリに Vue のユニットテストを導入する

真っ白な環境に Vue のユニットテストを導入する記事を以前書いた。

numb86-tech.hatenablog.com

今回は、Webpacker を用いて Vue を使用している Rails アプリに、Vue のユニットテストを導入する方法を書いていく。
rails newでプロジェクトを作成するところからやっていく。

前述の記事と同様、テストフレームワークJestアサーションライブラリにはpower-assertを使う。

以下のバージョンで動作確認した。

npmパッケージのバージョンは以下。

  • @rails/webpacker@3.5.5
  • @vue/test-utils@1.0.0-beta.26
  • babel-jest@23.6.0
  • babel-preset-power-assert@2.0.0
  • jest@23.6.0
  • power-assert@1.6.1
  • vue-jest@3.0.1

まずはプロジェクトを作り、そこに移動する。

$ rails new PROJECT_NAME --webpack
$ cd PROJECT_NAME

Vueの利用を開始。

$ bundle exec rails webpacker:install:vue

こうすると Vue を使えるようになるだけでなく、app/javascript/app.vueというサンプルコンポーネントも作られる。
今回はこのコンポーネントを対象にテストを書く。

まずはユニットテストに必要なライブラリをインストール。

$ yarn add -D jest babel-jest vue-jest @vue/test-utils

package.jsonに、Jest の設定を追加する。

diff --git a/package.json b/package.json
index 2bd7463..90196a1 100644
--- a/package.json
+++ b/package.json
@@ -13,5 +13,14 @@
     "jest": "^23.6.0",
     "vue-jest": "^3.0.1",
     "webpack-dev-server": "2.11.2"
+  },
+  "jest": {
+    "roots": [
+      "app/javascript"
+    ],
+    "transform": {
+      "^.+\\.js$": "babel-jest",
+      "^.+\\.vue$": "vue-jest"
+    }
   }
 }

rootsで、対象とするテストファイルの場所を指定している。
app/javascriptと指定したので、このディレクトリ以下の、ファイル名の末尾が.test.js.spec.jsになっているファイル、そして__tests__ディレクトリに入っているファイルが、対象になる。

transformでは、テストが可能な形にトランスパイルする設定を記述している。先程インストールしたbabel-jestvue-jestはここで使う。

JSファイルを Babel でトランスパイルするように設定しているので、Babel の設定も書く必要がある。
プロジェクト開始時に既に.babelrcは作られているので、そこにテスト用の設定を追加する。

diff --git a/.babelrc b/.babelrc
index ded31c0..0428c4b 100644
--- a/.babelrc
+++ b/.babelrc
@@ -14,5 +14,11 @@
     "syntax-dynamic-import",
     "transform-object-rest-spread",
     ["transform-class-properties", { "spec": true }]
-  ]
+  ],
+
+  "env": {
+    "test": {
+      "presets": ["env"]
+    }
+  }
 }

次に、package.jsonを編集して、$ yarn testでテストが実行されるようにした。

diff --git a/package.json b/package.json
index 90196a1..e1cb36e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,9 @@
 {
   "name": "jest-blog",
   "private": true,
+  "scripts": {
+    "test": "jest"
+  },
   "dependencies": {
     "@rails/webpacker": "3.5",
     "vue": "^2.5.17",

あとはapp/javascript以下にテストファイルを書けばいい。

app/javascript/__tests__/app.test.jsを書いてみた。
Jest などと一緒に最初にインストールした@vue/test-utilsは、Vue のユニットテストを書くための公式ライブラリ。

import {shallowMount} from '@vue/test-utils';

import App from '../app.vue';

const assert = require('assert');

describe('Assert', () => {
  describe('Vue', () => {
    it('shallowMount', () => {
      const wrapper = shallowMount(App);
      assert.equal(wrapper.text(), 'Hello Vue!');
    });
  });
});

これで$ yarn testを実行すれば、テストが実行される。

既にテストを書いていける状態になったが、power-assertを入れてテストを書きやすくする。

必要なライブラリをインストール。

$ yarn add -D power-assert babel-preset-power-assert

トランスパイルが必要なので、.babelrcenv.testに設定を追加する。

diff --git a/.babelrc b/.babelrc
index 0428c4b..47cfe92 100644
--- a/.babelrc
+++ b/.babelrc
@@ -18,7 +18,7 @@
 
   "env": {
     "test": {
-      "presets": ["env"]
+      "presets": ["env", "power-assert"]
     }
   }
 }

しかしこれで$ yarn testを実行したらエラーになった。

Requires Babel "^7.0.0-0", but was loaded with "6.26.3".

これを書いている時点でのbabel-preset-power-assertの最新バージョンは3.0.0
そしてこのバージョンは Babel のv7で使える。
だが、Rails5.2の Webpacker 環境で使われている Babel は、デフォルトではv6らしい。

そのため2.xbabel-preset-power-assertを使う必要がある。

Babel7 is incompatible with Babel6.
For Babel6, you need to use the 2.x release of babel-preset-power-assert.

https://github.com/power-assert-js/babel-preset-power-assert

バージョンを指定して改めてインストールする。

$ yarn add -D babel-preset-power-assert@2.0.0

これで、正しく動くようになった。

わざとテストを失敗させてみる。

      assert.equal(wrapper.text(), 'Hello Vue')
                   |       |                   
                   |       "Hello Vue!"        
                   VueWrapper{isFunctionalComponent:undefined,_emitted:#Object#,_emittedByOrder:#Array#}

詳細な情報が表示され、テストを効率的に書けるようになった。

『RubyでつくるRuby ゼロから学びなおすプログラミング言語入門』を読んだ

タイトル通り、Rubyを使ってRubyインタプリタを作っていく面白いテーマの本。
プログラミング初心者も対象読者に入っているため、非常にコンパクトにまとまっており、140ページ程度でインタプリタを作れる。

RubyでつくるRuby ゼロから学びなおすプログラミング言語入門(紙書籍+PDF版)www.lambdanote.com

Rubyの入門から始まっていくが、本当に最低限の機能しか扱わない。
クラスやオブジェクトという概念は全く出てこないし、メソッドもほとんど出てこない。
初心者のために内容を厳選したということだろうが、かなり思い切った構成になっている。

しかし、そんな「最低限の内容」だけでインタプリタ自作のような複雑で面白いことをやれるのが、興味深かった。

出てくる内容が、変数、条件分岐、配列、ユーザー定義関数、といったものばかりで、「本当に初心者向けだなー」と思いながら読んでいた。
そしてその「初心者向け」の機能だけでインタプリタを作っていくのは本当に面白かった。
言語機能について説明した後、それを使う機会(インタプリタ実装)がすぐに訪れるのも、入門書の構成として良いと思った。

Rubyの言語機能の説明もそこそこに木構造の説明に入っていく本書は、プログラミング入門書としてはかなり独特だと思う。
全編に渡り木構造や再帰を使っており、以前読んだ『プログラミングの基礎』に似ているかもしれない。

薄くてカラフルなのも、入門書として素晴らしいと思う。全ページフルカラーになっている。

特に、抽象構文木をカラフルな図で表現してくれるのは素晴らしかった。
これのおかげで、扱っているデータの構造をビジュアルで理解できるし、実装すべき処理もすぐにイメージできる。

本書の担当編集者であり、出版元のラムダノート株式会社の創業者でもある鹿野さんが、プログラムによって木を描いていたらしい。 note.golden-lucky.net

こういった工夫のおかげで、プログラミング初心者でも学習を進めやすい内容になっていると思う。

とはいえ、本当の初心者よりも、インタプリタや抽象構文木、言語処理系といったものに興味のある人が本書のメインターゲットだと思う。
すごく簡単に、インタプリタや言語の実装という世界に触れることが出来る。

かなり初歩的な内容ではあるが、文字通り初歩、最初の第一歩にはなる。
パースとか抽象構文木とか、何となくで理解していた用語の概要をきちんと知ることが出来た。
「パースする」といったときに何が行われているかを、何となくイメージできるようになった。

実際の言語処理系はもっと複雑なものだろうけど、やっていることをイメージできる、概略を掴める、というのが最初の一歩として重要だと思う。

本書では構文解析は行わず、著者が作成し gem として提供されているライブラリを使う。
読者は、構文解析済みの抽象構文木を対象に、それを処理するためのプログラムを書いていく。
他にも要所要所でそのライブラリに作業を丸投げするが、それは決して本質を損ねるようなものではない。
むしろ「インタプリタづくり」という本質に集中できるよう、本書のテーマにおいては非本質的な諸々の作業を肩代わりしてくれていると言える。

以下、各章のメモ。

第1章

  • 本書では、Rubyを学ぶための題材としてRubyインタプリタを作る
  • 本書で作るのは、最低限の機能のみを持った、MinRubyと名付けられた言語のインタプリタ
  • Rubyインタプリタとは、Rubyで書かれたプログラムを実行するプログラム
  • RubyインタプリタもまたRubyで書かれたプログラムなので、動かすためにRubyインタプリタが必要
  • そのため、本物のRubyインタプリタをコンピュータにインストールし、それによってMinRubyインタプリタを動かし、そのインタプリタが四則演算などのMinRubyプログラムを実行する

第2章

  • 変数、分岐、ループなどの基本的な概念について学ぶ

第3章

  • インタプリタを作るために、木というデータ構造を学ぶ。なぜなら、プログラムのような複雑なものを扱うのに適したデータ構造だから。
  • Rubyの「配列」で木を表現し、Rubyの「関数」で木を操作する

第4章

  • まずは四則演算のみをサポートしたインタプリタを作る
  • プログラムはただの文字列なので、まずはそれを木構造に変換する必要がある。この変換のこと構文解析、あるいはパースと呼び、それによって得られた木を構文木と呼ぶ。
  • 括弧や空白などの不要な情報を省いた構文木を抽象構文木という
  • プログラムを受け取る → 構文解析して抽象構文木にする → 関数で木を操作する というのが処理の流れ
  • 四則演算の場合、木の葉についてはその値を返し、木の節については2つの部分木の実行結果を演算子に従って計算する

第5章

  • 複数の計算式を並べたものを複文という
  • プログラムが計算式と大きく違うのは、複文になっていること
  • だからインタプリタも複文に対応させる必要がある
  • whileを使って複文に対応する
  • インタプリタに変数を覚えさせるのは、Rubyのハッシュによって実装する

第6章

  • 実装しようとしている言語をターゲット言語、インタプリタの実装に使っている言語をホスト言語という
  • インタプリタの本質は、ターゲット言語の言語機能をホスト言語の言語機能に丸投げして、そちらで処理すること
  • 本書の場合はMinRuby(ターゲット言語)の処理をRuby(ホスト言語)で実装している
  • 構文解析の段階で他の言語機能を使ったプログラムに置き換えて扱うものを、糖衣構文という

第7章

  • 関数の実装は、変数と同じようにハッシュを使ってインタプリタに覚えさせる
  • 「言語仕様」とインタプリタの「実装」は一致するとは限らない。プログラミング言語を学ぶときはそれを注意して、「実装」ではなく「言語仕様」を学ぶことが大切。

第8章

  • 関数の実装を拡張して、ユーザー定義関数をサポートする

第9章

  • 言語Xのインタプリタに必要な機能を言語Xだけで実装できれば、インタプリタを使ってそのインタプリタ自身を動かせるようになる。これをブートストラップという。
  • 前章まででMinRubyインタプリタに様々な機能を実装してきたので、あとは配列とハッシュを実装すればブートストラップが可能になる
  • データ構造をつくる記述を構築子という
  • MinRubyの配列の構築子とハッシュの構築子は、Rubyの言語機能をそのまま使う