TypeScript で p5js のスケッチを書くための雛形を作った
Nature of Code -Processingではじめる自然現象のシミュレーション-
- 作者: ダニエル・シフマン,Daniel Shiffman,尼岡利崇,鈴木由美,株式会社Bスプラウト
- 出版社/メーカー: ボーンデジタル
- 発売日: 2014/09/16
- メディア: 大型本
- この商品を含むブログを見る
なんで p5js ?
個人的にとっつきやすいから。
なんで TypeScript ?
VSCode で補完が効くのと、凡ミスを防ぐため。最近は、特に理由がなければ基本的に TypeScript で書いている。
FuseBox を使う
ビルド環境の構築に時間をかけたくなかったので、なるだけ設定を書かずに済むモジュールバンドラを探した。最低限、コンテンツの配信(ウェブサーバー)と、ファイルを変更したらビルド&リロードしてくれたらOK 。 いろいろ探したら、FuseBox がよさそうだった。TypeScript ファーストなのが決め手。
設定はこんなかんじ。
const { FuseBox, WebIndexPlugin } = require("fuse-box"); const fuse = FuseBox.init({ homeDir: "src", output: "dist/$name.js", plugins: [ WebIndexPlugin({path: "."}) ] }); fuse.bundle('app') .instructions(`> sketch.ts`) .watch() .hmr({reload: true}); fuse.dev(); fuse.run();
node fuse
で起動できる。必要に応じて、npm scripts
に移動する。
スケッチ( src/sketch.ts
)
こんなかんじ。draw
に必要なコードを書き足していけばOK。
バンドルの過程で、setup
と draw
が関数スコープに入るので、明示的に window
オブジェクトのプロパティとして生やす必要がある。
/// <reference types="p5/global" /> import 'p5'; function setup() { createCanvas(720, 720); noCursor(); } function draw() { clear(); ellipse(50, 50, 80, 80); } window['setup'] = setup; window['draw'] = draw;
Puppeteer でページを自動操作するスクリプトを書く時に使うスニペット
画面が複数あったり、フォームの項目が多い場合に毎回最初から実行して試行錯誤するのは大変。以下のようなスニペットで、 REPL 環境を用意することで、DevTools でセレクタを調べたり、動きを確かめつつ、スクリプトに書き足していく。
const puppeteer = require('puppeteer'); const readline = require('readline-promise'); const rlp = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); (async () => { const browser = await puppeteer.launch({ headless: false, devtools: true, }); await page.goto('https://example.com'); while (true) { try { const statement = await rlp.questionAsync('> '); const result = await eval(statement); console.log(result); if (statement.trim().indexOf('browser.close()') === 0) { break; } } catch (e) { console.error(e); } } })();
ブラウザが閉じられたか判定する方法がよく分からなかったのでお茶を濁したかんじになった。
Nuxt.js で Vuex の結合テストを書く方法を考えた
この間、友人と焼き肉を食べていて、「Nuxt.js で Vuex の結合テスト書くのどうやってますか?」という話になったので、考えてみました。
Vuex の結合テストの定義
この記事では、アクションやミューテーション単体ではなく、 アクションをディスパッチした結果のステートやゲッターが意図通りになっているか確認するテストのことを、Vuex の結合テストと呼びます。
方針
Vuex の結合テストで課題となるのは、テスト対象の Vuex Store インスタンスの生成です。
今回は、(少し強引ですが)Nuxt.js のドキュメントのエンドツーエンドテストのサンプル( 開発ツール - Nuxt.js )をベースに Store インスタンスの生成を Nuxt.js に任せることにしました。
最初、モジュールモードの場合に Nuxt.js が内部でおこなっている Store オプションの組み立て( nuxt.js/store.js at e0cc9a1cc6ffdfc87f944d0669ec1dd8d35130c9 · nuxt/nuxt.js · GitHub )をテストコードで再現しようと思いましたが、込み入った処理なのと、アクションのコードで利用している Nuxt.js のプラグインや Webpack を利用した環境変数などの仕組みの再現も考える必要があり、コストが高いと考えて諦めました。
また、Store インスタンスの生成を Nuxt.js に任せることで、(スタブ・モック化はしますが)実際に動作するコードに限りなく近い対象をテストすることが可能になります。
手順
- テストを実行するためのコンポーネントを作成する
- Nuxt コンストラクタのオプションの
extendRoutes
を用いて、上記のコンポーネントをレンダリングするルーティングの設定を追加する - テストでスタブ・モック化したい処理、例えば、
action
内でおこなっている通信やストレージを扱う処理は、事前に Nuxt.js プラグインにしておき、テストでは必要に応じてスタブ・モックに差し替える renderRoute
で上記のルートを呼び出し、テストの関数をコンポーネントのfetch
フックで実行する
サンプルコード
コードで具体的にみていきます。
ストアのコード
テスト対象の小さいストアのコードです。記事をDBから取得して、ステートにセットします。
export const state = () => ({ entry: null, }); export const mutations = { setEntry(state, doc) { state.entry = doc; } }; export const actions = { async fetchEntry({commit}, entryId) { const doc = await this.$db.get(`entry_${entryId}`).catch(() => null); commit('setEntry', doc); } };
DBのプラグインのコード
DB へのアクセスは、Nuxt.js プラグインによって注入されたオブジェクト(this.$db
)を通して行います。データベースは、なんでもよいのですが、このサンプルでは PouchDB ということにします。プラグインの内容を以下に示します。
import PouchDB from 'pouchdb'; export default (_, inject) => { const db = new PouchDB('http://localhost:5984/entries'); inject('db', db); };
テストに関係するコード
テストの関数を実行するコンポーネント
テストコードから渡された関数を、Store のインスタンスやスタブ・モックオブジェクトに差し替えるための関数(inject
)を引数として呼び出すための Page コンポーネントです。
pages
配下に設置するとテスト実行時以外にもアクセスできてしまうので、test
ディレクトリ配下に execute-test.vue
という名前で保存します。
<template> <p>Component for testing</p> </template> <script> export default { async fetch(context) { const {app, req} = context; try { await req.test(context, app.$inject); } catch (e) { throw e; } } }; </script>
injector プラグイン
Nuxt.js プラグインで注入されたオブジェクトをテストコードで、スタブ・モックのオブジェクトに差し替えるためのプラグインを用意します。plugins
の配下に injector.js
という名前で保存します。
export default (_, inject) => { inject('inject', inject); };
ava を使ったテストコード
テストコードです。Nuxt.js のドキュメントのエンドツーエンドテストをベースにしているので、ava をつかっていますが、jest など他のテストフレームワークやテストランナーを使っても問題ありません。
解説するポイントが複数あるので、下で詳しく解説します。
import test from 'ava'; import sinon from 'sinon'; import { Nuxt, Builder } from 'nuxt'; import { resolve } from 'path'; let nuxt = null; test.before('Init Nuxt.js', async t => { const rootDir = resolve(__dirname, '..'); let config = {}; try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {}; config.rootDir = rootDir; // project folder config.dev = false; // production build config.env = config.env || {}; // テスト実行時か判断するための環境変数を設定する config.env.testEnabled = true; config.router = config.router || {}; config.router = { ...config.router, extendRoutes (routes, resolve) { // テストを実行するためのルートを追加する routes.push({ name: 'execute-test', path: '/test/execute', component: resolve(__dirname, 'execute-test.vue') }); } }; config.plugins = config.plugins || []; // プラグインで注入されたオブジェクトを差し替えるためのプラグイン config.plugins.push('~/plugins/injector.js'); nuxt = new Nuxt(config); await new Builder(nuxt).build(); nuxt.listen(4000, 'localhost'); }); test('プラグインで注入されたオブジェクトの一部のメソッドをスタブにする', async t => { const test = async (context, _) => { // DBへのアクセスをスタブ化 const db = context.app.$db; const get = sinon.stub(db, 'get') get.returns(Promise.resolve({ _id: "entry_kitak", _rev: "1-c507349e68734f039c71a5050a3cac67", content: "Hello!", title: "kitak", })); // actionのdispatchとstateの確認 await context.store.dispatch('fetchEntry', 'kitak'); t.is(context.store.state.entry.content, 'Hello!'); }; await nuxt.renderRoute('/test/execute', { req: { test, }, }); }); test('プラグインで注入されたオブジェクトをスタブ・モックオブジェクトで注入しなおす', async t => { const test = async (context, inject) => { // DBへのアクセスをスタブ化 const db = { get() {}, }; sinon.stub(db, 'get').callsFake(function () { return Promise.resolve({ _id: "entry_kitak", _rev: "1-c507349e68734f039c71a5050a3cac67", content: "こんにちは!", title: "kitak", }); }); inject('db', db); // actionのdispatchとstateの確認 await context.store.dispatch('fetchEntry', 'kitak'); t.is(context.store.state.entry.content, 'こんにちは!'); }; await nuxt.renderRoute('/test/execute', { req: { test, }, }); }); // Nuxt サーバーをクローズする test.after('Closing server', t => { nuxt.close(); });
テストコードの解説と考慮すべきこと
設定オプションの変更
before フックでテスト対象のアプリケーションのビルドとサーバーの起動をおこなっています。ポイントはテストのために Nuxt コンストラクタの設定オプションを変更することです。
まず、環境変数に testEnabled
というプロパティを追加しています。
config.env = config.env || {}; // テスト実行時か判断するための環境変数を設定する config.env.testEnabled = true;
今回は利用していませんが、テスト時に実行したくないコード、例えば、Vuex のプラグインでストレージに書き込んだり、他のサーバーにイベントを送信するといった処理を回避するために利用します。
2つ目がテストを実行するページコンポーネントにアクセスするためのルートの追加です。
config.router = config.router || {}; config.router = { ...config.router, extendRoutes (routes, resolve) { // テストを実行するためのルートを追加する routes.push({ name: 'execute-test', path: '/test/execute', component: resolve(__dirname, 'execute-test.vue') }); } };
最後がプラグインで注入したオブジェクトをスタブ・モックのオブジェクトに差し替えるためのインジェクターのプラグインの追加です。
config.plugins = config.plugins || []; // プラグインで注入されたオブジェクトを注入し直すためのプラグイン config.plugins.push('~/plugins/injector.js');
最初は、テストケースごとにスタブやモックに差し替える Nuxt.js プラグインを用意して、アプリケーションをビルドしようかと考えていましたが、ビルドに10~20秒程度時間がかかるので、ビルドは before フックでの一回のみにし、汎用的なインジェクターのプラグインを用意することにしました。
Nuxt.js プラグインで注入されたオブジェクトのスタブ・モック化
今回のアプローチでは、Nuxt.js がビルドしたコードをテスト対象にしているので、proxyquire
などの import 文にフックさせてスタブ・モックさせるライブラリは利用できません。
DB や ストレージにアクセスしたり、環境に応じて処理を分岐するコードを、 積極的に Nuxt.js プラグインにして、スタブ・モック化を図ります。
スタブのためのライブラリとして、今回、Sinon.js を使いました。
サンプルコードでは、ふたつテストケースがありますが、プラグインで注入されたDBにアクセスするオブジェクトをどうスタブするかが異なります。
1つ目はメソッドレベルでスタブに変更し、2つ目はスタブオブジェクトを作成し、injector プラグインで追加された関数を利用して、注入し直して差し替えています。
どちらの方法を使うかは状況に応じて、使い分けると良いでしょう。
スタブの後始末とテストの並列化について
今回のサンプルコードでは、スタブの後始末をおこなっていませんが、これは Nuxt.js がリクエスト毎にコンテキストを生成するためです。今回のテストに関係する箇所でいうと、リクエスト毎にプラグインの関数の呼び出し(db
オブジェクトの生成)と Store インスタンスの生成が行われます。
これはテストを実行する上で非常に有用な性質です。後始末の記述を不要にする他に、テストの並列実行を可能にします。
ただし、プラグインで、関数の外側で生成したオブジェクトを変数にキャッシュして、再利用しているような場合は、リクエストのコンテキストにまたがって同じ参照を共有するので、後始末が必要になります。
例えば、今回の DB のプラグインでは以下のようなコードが関数の外側で生成したオブジェクトを変数にキャッシュして再利用しているコードです。
import PouchDB from 'pouchdb'; const db = new PouchDB('http://localhost:5984/keywords'); export default (_, inject) => { inject('db', db); };
後始末は、1つ目のテストケースの場合は db.get.restore()
を呼び出し、 2つ目のテストケースの場合はオリジナルのオブジェクトを取っておいて終了時に注入し直します。※
const originalDB = context.app.$db; // スタブ化やアクションのディスパッチなど // ... inject('db', originalDB);
また、ava はデフォルトでテストを並列で実行しますが、リクエストのコンテキストにまたがって同じ参照を共有している場合、スタブの処理が競合する可能性があります。この場合、テストは直列で実行する必要があるので注意が必要です。
ava で直列にテストを実行するには --serial
オプションを指定します。
ava --serial
※ この処理を毎回書くのが大変な場合は injector を改良することをおすすめします。注入用の関数をラップした関数を用意し、その関数でオリジナルのオブジェクトを保存する処理を追加したり、保存しておいたオブジェクトに戻す関数を提供してもよいかもしれません。
アクションのディスパッチとステートの確認
ここはコードに書かれている通りなので、特に解説はおこないません。
テストの実行
テストを実行するパスに対してrenderRoute
を呼び出します。
await nuxt.renderRoute('/test/execute', { req: { test, }, });
テストの関数は、req
オブジェクトのパラメータとして、コンポーネントではコンテキストから参照するようにします。
async fetch(context) { const {app, req} = context; try { await req.test(context, app.$inject); } catch (e) { throw e; } }
まとめ
今回は Nuxt.js のドキュメントのエンドツーエンドテストのサンプルをベースに、Vuex の結合テストを記述・実行する方法を紹介しました。 Nuxt.js のプラグインの仕組みを使って、必要な箇所をスタブ・モック化しつつ、Nuxt.js がインスタンス化した Store を対象にテストを実施することが可能です。
最近読んだ漫画
- 作者: 夢枕獏,谷口ジロー
- 出版社/メーカー: 集英社
- 発売日: 2015/06/01
- メディア: Kindle版
- この商品を含むブログを見る
とあるカメラを巡って、一匹狼の登山家と彼を追うジャーナリストが話を動かしていく。
谷口ジローの絵の描き込みがすごい。登山の場面もさることながら、渋谷や新宿の居酒屋が細部まで緻密に描かれていて、酒を飲みたくなる。
- 作者: 高橋のぼる
- 出版社/メーカー: 小学館
- 発売日: 2017/12/27
- メディア: Kindle版
- この商品を含むブログを見る
蕭何のモデルが明らかに遠藤憲一でウケる。
劉邦のだらしないキャラがギャグっぽい描き方で際立っていて面白い。でも、このペースだと天下を取るまでにどんだけかかるんだろうね...
辺境の老騎士 バルド・ローエン(1) (ヤングマガジンコミックス)
- 作者: 支援BIS,菊石森生
- 出版社/メーカー: 講談社
- 発売日: 2017/03/17
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
料理がおいしそうな、のほほん漫画と思いきや、策略・謀略の話が途中から増えてきて、なかなか読み応えのある作品。
- 作者: 倉薗紀彦
- 出版社/メーカー: KADOKAWA/エンターブレイン
- 発売日: 2016/03/25
- メディア: コミック
- この商品を含むブログ (1件) を見る
気狂いのおじさんに付き従って、地底に探検に行く物語。
19世紀のSF小説が基になっているので「いやいや!」とツッコミを入れたくなるところも多いのだけど、当時わかっている科学的事実でロジックを組み上げている。
- 作者: 渡辺ペコ
- 出版社/メーカー: 講談社
- 発売日: 2017/05/23
- メディア: コミック
- この商品を含むブログを見る
「はてなにも匿名で書くからね」というインパクトのあるコマをTwitterで見かけて、買った。
仲がいい夫婦なのだけど、婚外恋愛の上になりたっていて、どうみてもフラジャイルな関係。正直、良い終わり方がまったく想像できない...
- 作者: 田中芳樹,フクダイクミ
- 出版社/メーカー: 講談社
- 発売日: 2018/05/18
- メディア: Kindle版
- この商品を含むブログを見る
騙そうとするもの、騙されたふりをして騙そうとするもの、都市間の駆け引きが面白い。田中芳樹の物語は、人を喰ったというか、斜に構えた良い味のキャラクターが多いのが良い。
Rails + Hypernova なアプリを Heroku にデプロイする
個人メモ
- Heroku Container Registry で Rails アプリをデプロイするときにハマったこと - kitak blog
- Hypernova で Vue.js のコンポーネントを Rails でレンダリングする - kitak blog
の続き。
Rails + Hypernova で作った趣味アプリをデプロイすることに。
最近は Firebase を使うことも増えてきたのだけど、Express や Rails で作った API やウェブアプリのデプロイには、Heroku や Now などの Docker に対応している PaaS を使っている。
Docker 対応の PaaS で Rails + Hypernova のアプリをデプロイする際に、 1コンテナ 1サービス のお作法に従うと、Rails と Node でそれぞれ PaaS のアプリケーションを用意して、デプロイしないといけない。 このようにアプリケーションを分割すると、Rails と Node で同じコンポーネントのファイルを使用する事情から、正しく動かすには Rails と Node のデプロイ完了のタイミングを(可能な限り)揃える必要がある。
各ロールでさらに複数のアプリケーションを作っておいて、Blue-Green Deployment のようなことを実現するか、コンポーネントをHypernova に登録する際の名前にバージョンを含めるようにして、過去のバージョンも扱えるようにすればできそうだが、前者はデプロイタスクが煩雑になるし、後者はコードの管理が複雑になるので、趣味アプリでそこまでやる必要性は感じない。 また、Heroku や Now では、アプリケーション同士は内部ネットワークでやりとりできず、外部ネットワークを通す形になるのでレイテンシーも気になる(計測はしていない)。
上記の理由と Wantedly でも Rails のコンテナに Hypernova を同居させてうまくいっているようなので( ref: Rearchitecting Wantedly's Frontend | Wantedly Engineer Blog )、1コンテナで Rails と Hypernova 両方のサービスを動かすことにした。
Rails の対応
Hypernova に限らず Heroku にデプロイするために必要な対応。
Heroku では静的ファイルの配信をアプリケーションサーバーがおこなう必要があるので、config/environments/production.rb に以下を書く。
config.public_file_server.enabled = true
また、Heroku の制約でログを標準出力に書き出す必要があるので、RAILS_LOG_TO_STDOUT 環境変数を設定する。これは設定ファイルにデフォルトで以下のような記述がある。
if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end
昔は、rails_12factor gem を入れて上記と同じ対応していたのだけど、最近は、そういった gem を入れる必要がなくなったぽい。
Docker の対応
複数のサービスを動かすために Run multiple services in a container | Docker Documentation を参考に supervisor を使うことにした。
以下のように ruby:2.5.1 のイメージをベースに apt-get で supervisor を入れ、必要なディレクトリを掘り、設定ファイルをコピーして、supervisord を起動する。
FROM ruby2.5.1 #... RUN apt-get update && apt-get install -y supervisor # ... RUN mkdir -p /var/log/supervisor COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf # ... CMD ["/usr/bin/supervisord"]
supervisord の設定は以下のようなかんじ。Rails や Hypernova のプロセスの標準入出力を supervisord にリダイレクトするために Dockerで子プロセスからのstdoutをsupervisordにリダイレクトする方法 – 踊る犬.netブログ (旧) を参考にした。
[supervisord] nodaemon=true directory=/usr/src/app [program:rails] command=/bin/bash -c "rails server -b 0.0.0.0" autorestart=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:hypernova] command=/bin/bash -c "node hypernova.js" autorestart=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0
悪習を繰り返そうとしたときにフフッと笑う
最近、試しているやつ。何かの本か記事で読んだ気がするのだけど、思い出せない。
タイトルの通りなのだけど、自分が悪い習慣をおこなおうとしたときにフフッと笑うようにすると、その行動を抑制できる(ような気がする)。
例えば、自分の場合だと数分おきに自然とスマホに手が伸びてしまうことがあるのだけど、そういうときにフフッと笑う。そうすると、行動をおこそうとしている自分を客観的に見ているもうひとりの自分が「お前ってやつは、しょうがないやつだなぁ」と言っている気がして、伸ばしかけた手を引っ込めることができる。
脳には、身体行動を先におこなうと、それに見合った感情が形成されるというクセがある。例えば、とりあえず口角をあげると幸せな気持ちになる、といったものがよく知られている。このクセを利用して、フフッと笑うという身体行動をとりあえずおこなうことで、悪習を抑制する感情を形成させるというわけである。
とはいえ、傍から見たら、スマホに手を伸ばしかけた人間がフフッと笑っているのは気味が悪いので、さりげなくやるのがポイントだと思う。