電池冷蔵庫

除雪のない世界に行きたい

Vue.js E2E test (GitLab CI + Webpack + Jest + Puppeteer)

TL;DR

 snippet & zip file for YOU!

gitlab.com

 vue-cliで自動生成されたE2Eテストの設定が意味不明だったので、自前で置き換えたものです。一応動きました。やったぜフラン!

ご注意

 PuppeteerとChromiumのリリース頻度を考えると、この記事に書いてあることは3時間くらいで陳腐化します。動かなかったら自分でなんとかしてね。

 [追記] 参考にした公式ドキュメントがあることを忘れてました。 Using with puppeteer by jest

GitLab CIとE2Eで幸せになろう

 GitLabとはgitリポジトリとdockerレジストリとcron内蔵型docker-based CIが一体化した無料で使える世界最高のサービスですが、そのことは常識なので説明しません。まさかGitLabを単なるGitHubのパチモンだなんて思っている人はいませんよね?

 E2Eテストというのは要するに「実際ちゃんと動くか」というテストですが、これまた常識なので説明は差し控えさせていただきます。決して私がよく分かってないわけではありません。

 最近では、vue-cilのwebpackテンプレートを利用するとE2Eテストが追加されるなど、E2Eは「あって当たり前」のテストになって来ています。まだ製造コストが高すぎるため「全ての画面に書いて当たり前」とまでは行きません(そうなる時代は当面来ないと思います)が、壊れたときに致命的な部分のE2Eテストを書いておくと、即死ダメージを瀕死レベルまで軽減できる可能性があります。

 クレームを入れるページとか、不具合を報告するページなど、「そこが壊れてたらもう終わり」という部分ですね。こういったセーフティネットがぶっ壊れていると救いがありませんので、是非テストしましょう。

 ただ、既存のE2Eテストはツールの使い方そのものが難しく、設定ファイルはおよそ理解不能であり、導入したが最後、誰もメンテナンスできる気がしないようなコードで構成されていました。これではやはり、プロジェクトの基盤としてはなかなか受け入れられません。

puppeteer

 ここで注目したいのが、最近にわかに流行りつつあるヘッドレスChromium操作ライブラリー puppeteer です。

 puppeteer はあくまで Chromium を操作するだけなので、複数のブラウザー (ぶっちゃけIE) を操作して、厳密に動作チェックするようなテストには使えません。しかし、「極めてクリティカルな機能の、本当に最低限の動作」を確かめるくらいなら、たとえブラウザーごとにデザインの崩れなどがあったとしても、Chromiumで動けばだいたい問題ないはずです*1

 また puppeteer は機能がシンプルである分、使い方もシンプルです。これと別にアクセス先のサーバーさえ用意すれば、E2Eテストに着手できるわけです。それならメンテできそう!というエンジニアも多いと思います。

具体的

 コード類は冒頭のスニペットからzipを取得してください。以下は一部ファイルとポイントの解説。

pacage.json

{
  "scripts": {
    "e2e": "jest --config test/e2e/jest.conf.js --forceExit"
  }, ...
}

 asyncを複数利用すると、現在のjestは壊れます。仕様なのかバグなのか知りませんが、永久にプロセスが終了しません。そこで --forceExit をつけます。

runner.js

let server
let host
let port

function path (str = '/') {
  const h = host || 'localhost'
  const p = port || process.env.PORT
  return `http://${h}:${p}/#${str}`
}

const run = async () => {
  const webpack = require('webpack')
  const DevServer = require('webpack-dev-server')
  const webpackConfig = require('../../../build/webpack.prod.conf')
  const devConfigPromise = require('../../../build/webpack.dev.conf')
  const fetch = require('node-fetch')

  const devConfig = await devConfigPromise
  const devServerOptions = devConfig.devServer
  const compiler = webpack(webpackConfig)

  server = new DevServer(compiler, devServerOptions)
  host = devServerOptions.host
  port = devServerOptions.port

  await server.listen(port, host)

  const url = path()
  console.log('server url', url)

  await fetch(url)
  console.log('dev server running!')
}

module.exports = {run, path}

 ここで行われていることを平易に言い換えると「力ずくで Webpack dev サーバーを起動し、そこにアクセスしてE2Eテストを実施する。そのとき初回ロードだけめちゃくちゃ長いので、初期設定において1回ロードできるまで待つ。こうすると次からは普通に動く」となります。クッソ力技ですね!

 ここの処理は完全に間違っているような気がしますが、とにかくこれで動く(ほかの方法も見つからない)ので、気にしない。後でちゃんと調べて直すかもしれない(願望)。

setup.js

const puppeteer = require('puppeteer')
const mkdirp = require('mkdirp')
const fs = require('fs')
const {dir, endpoint} = require('./endpoint')

module.exports = async function () {
  await require('./runner').run()

  const browser = await puppeteer.launch({args: ['--no-sandbox']})

  // store the browser instance so we can teardown it later
  global.__BROWSER__ = browser

  // file the wsEndpoint for TestEnvironments
  mkdirp.sync(dir)
  fs.writeFileSync(endpoint, browser.wsEndpoint())
}

 argsが既存の例と異なりますが、Linux SUID Sandboxのページを見ると

The Linux SUID sandbox is almost but not completely removed. See https://bugs.chromium.org/p/chromium/issues/detail?id=598454 This page is mostly out-of-date.

 とあり、なんとなく「別になくても動きそうだな?」と思ったので取りました。今のところ動いています。ただ、いまだに問題の所在が謎なので、ダメなら戻すかも…

 なお --no-sandbox も削除してみましたが、これはissueを踏み抜いてエラーになりました。したがって、これは完全にバグのワークアラウンドです。珍妙な動作なので修正してほしいですね。  

404.spec.js

const {path, browser} = require('../puppeteer').get()

describe('/404', () => {
  let page

  beforeAll(async done => {
    page = await browser.newPage()
    await page.goto(path('/404'))
    done()
  })

  afterAll(async done => {
    await page.close()
    done()
  })

  it('loads without error', async done => {
    const text = await page.evaluate(() => document.body.textContent)
    expect(text).toContain('404')
    done()
  })

  it('contains correct email link', async done => {
    const button = await page.$('[data-prj-role="email-button"]')
    expect(await (await button.getProperty('href')).jsonValue()).toEqual('mailto:tottokotkd@me.com')
    done()
  })
})

 これが具体的なE2Eテストのサンプルです。今回はクリックなどをせず、単純にHTMLの内容を見ています。

 謎の呪文もありませんし、意味不明なコードもないと思います。await page.$('[data-prj-role="email-button"]') という部分は、プロジェクトのルールとして導入された独自のアトリビュートに基づく処理です。このような属性があるとテストが非常に楽になるので導入されていますが、もちろん必須ではありませんから、適宜IDなどで引いても問題ありません。

 どうですか? これならE2Eテストを作れそうじゃありませんか?

.gitlab-ci.yml

variables:
  PUPPETEER_DEPS: gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
stages:
- test
build:
  stage: test
  image: node:latest
  script:
  - apt-get update
  - apt-get install -y $PUPPETEER_DEPS
  - npm install
  - npm run test

 GitLab CIでは node:latest を使ってテストを実行していきます。ただ、素の状態ではChromiumがインストールできません。

 もちろんPuppeteer実行用にイメージを作ってもいいと思うのですが、なんかそこのメンテをするのも面倒だよなということで、とりあえず毎回インストールします。node:latestDebianベースなので、必要なパッケージを apt-get しましょう。

 依存については公式ドキュメントに言及があります。

プッシュするたびE2E!

 ここまでの設定で、GitLabにプッシュするたびE2Eテストが実行され、マージリクエストを出すたびにテストが成功するか表示され、「テストが成功したら自動マージ」機能が使えるようになりました。やったぜフラン!

*1:デザインが崩れてボタンが隠れたりすると悲惨ですね。ただそもそも、そんなクリティカルなページに、ピクセル単位で正確に描写しないと操作不能になるデザインを導入するのはやめておきましょう

プログラミング言語と定番書: Scalaコップ本

 ちょっと必要に迫られて書評。

 一言で言うと、まあこんなもんでしょ、という本です。翻訳の品質を問題視するレビューもありますし、実際のところ、英単語を日本語単語に置き換えただけのような部分があったりするかもしれませんが、「英語の文章を英単語で読むより、英語の文章を日本語単語で読むほうが多分はやい」と考えれば特に問題ないですね ワハハ

 初学者が必要とする情報は揃っていたと思いますし、実装に突っ込んでいく解説スタイルは(たとえ分からなくても、分からない事柄があるという発見として)読む甲斐があります。

 ただひとつ致命的問題があるとすれば、この本を何回読んでもPlay Framework (あるいはSlick) のコードは全然読める気がしないというところでしょうかね。 implicit というキーワードが分かっても、実際のPlayにおける使用方法が宇宙的すぎて、自動生成コードを読んでもまったく意味が分からんと思います。それはもちろんコップ本のせいじゃないんですが、現実としてはやっぱり困りますね。今あえてKotlinじゃなくScalaやる理由なんてPlayとAkkaしか思いつかないし…

 いい本です。ただ高いし厚いし、カロリーがすごい。

プログラミング初心者がこの先生きのこるためには…

 タイトルはどうでもいいです。

わしのおしごと

 現在、以下のテクノロジーでご飯を食べてます。

  • Ruby (Rails: 好きじゃない)
  • ObjC / Swift (iOSアプリ: Swiftだけにして)
  • CoffeeScript (Backbone.js: 限界)
  • Python (データ処理: むり)
  • SQL (DB: 言わずもがな)
  • BashとかNginxとかDockerとかChef (所謂インフラ: たーのしー!)

 これ全部、1つのプロジェクトです。とはいえもちろん、常に全部を並行してやっているわけでも、全コードを一人で書いたわけでもありません。都度都度、チーム内でアサインされた作業をやっていくって感じです。クライアントはバグ調査でちょいちょい覗くくらいで、製造作業はやっていません。

 あからさまに「薄く広く」というやつなので、よりプロい人材が確保できるタイミングならば、その人に仕事が飛んでいきます。そういう人的体制が完備された暁には、もっと責任範囲が限定的になり、少なくともインフラ周りは手放せるはずです(そんな日がいつ来るかは知りません)。

わしの学習歴

 まず、プログラミング自体はわりとダラダラ触っていて、C# Java あたりは書いたことある(しかし仕組みは全く謎だし成果物もゼロ)という状態がスタートです。そこから1年半くらい、ガッツリ勉強した期間があります。

 やった言語を思い出せる順番 (勉強した順番ではない) で書いてみると、まあこんな感じ。

 HTMLはプログラミング言語じゃねーぞみたいな注意書きはまあいいですよね。みんな分かってるはずなんで。

 これら言語ごとに、まずは解説書1冊を読んで、モノによってはフレームワーク1つ弄ったくらい。JavaScript以外はサラーっと構文だけ知った程度。

 例えばScalaだと、通称コップ本を読みつつ Play と Slickをやりました。多分もっとも深入りした部類だと思います。他は、定評のある本を1冊読んで「ふーん全部Scalaのパクリだな」で終わりです。クソ初心者!

 JavaScriptは、今考えるとメチャクチャ変化の激しい時期だったので辛かったわけですが、Reactあたりを主として弄っていました。あとReduxとか、lodashを触って「ふーん全部Scalaのパクリだな」という具合です。ほんまクソ!

いいこと悪いこと

 乱読のメリットは明らかにあります。直ちにやれる仕事が増えるのもそうなんですが、それはどちらかというとオマケです。より重要なこととして「新しいもの」への抵抗が非常に薄くなりましたし、新しいものに対して「あれと似ている」という勘が働くようになりました。IT技術はお互いに影響を与えあっているので、一通りあっちこっち触っておくと「ああ、アレだな」という場面が非常によくあります。予習みたいなもので、将来への投資というやつです。詐欺師の決めゼリフですね!

 悪いことは、まあ明らかだと思いますが、1年以上かかっているわけです。その間ずっと引きこもりニートやって、書籍を買いまくってるんですから、頭がおかしくなりそうですね。私は諸般の事情があって精神的に楽でしたが、普通はやめといた方がいい気がします。アルバイトしながら… とかなら、まだマシかな?

笑っちゃうくらい腹が立った同僚のプログラミング

 ここに書いてあることは全部フィクションですぞ!

なんかコードが減ってる

 ある日の業務中、ふとした瞬間に「あったはずの処理がない」と気づきました。

before

# わしの記憶の片隅にあるコード
result = sugoi_results.first

 元々はこういう感じで、複数の値が帰ってくる処理 sugoi_results があり、その1つ目(あるいはnil)を取り出すという処理でした。ごく平凡なRubyのコードですね。

after

# 最新のコード
result = sugoi_results

 おかしいですね。 result = results という時点で3回くらい発狂しそうです。とにかくおかしいです。

 というのは、このプロジェクトはRSpecで管理しているので、コードが欠損すれば自動的に検出できるはずなんです。それが壊れていないということは、何かおかしなことが起こっています。そこで暗い気持ちでGit履歴を見ます。すると…

def sugoi_results

  results = []

  # ...
  # すごい処理
  # ...

  # ↓ここから増えてたコード

  if results.size == 2
    results.first
  else
    results
  end

  # ↑ここまで増えてたコード
end

 わかりますか? これ。この衝撃。

 実は業務上、冒頭の利用箇所では results が2個の配列になるだろうなーという暗黙の了解があったんです。だから、どうせfirstつけるし、2個のときはあらかじめsugoi_results内部でfirstつけたろ!ということらしいです。

# sugoi_resultsの出力結果は… だいたい配列!

[]
[1]
1  # <- だけどここだけ要素1つ!
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4, 5]

何がダメなのか

 すごくつらいですが、一応書きます。

名前から想像できない動作がダメ

 results と複数形なのに、2個の時だけ1個になる(?)なんて絶対わからないです。皆さんのプログラミング言語にそんな哲学的な関数ありますか? 同僚をだまし討ちにするのはよくありませんね。

汎用性ないのがダメ

「オレ、2個の要素が2個とも欲しいなあ」という人はどうすればいいんでしょうか。わざわざ results という関数として独立して公開されているからには、他にも利用者がいるはずです。同僚の顔色を伺いましょう。

意味わかんないのがダメ

 致命的だなあと思うのは「意味わかんない」ところですね。なんでこんな改変したんだ?という思考過程が全く見えない。仮に見えても全力却下ですが、こういうことをやると全般的な信用の問題になってきます。

つまり

 ちゃんとプログラムを英語として読んでから書きましょってこと!

追記

 原題「笑っちゃうくらい腹が立った初心者のプログラミング」ですが、この想像上の同僚が初心者かどうかは定かでないので「笑っちゃうくらい腹が立った同僚のプログラミング」に改題しました。

プログラミング初心者へのお手紙

 私は如何にして投稿サイト*1を止めて公式ドキュメントを愛するようになったか


 目次もあるよ!


初心者べからず集

 「初学者」が「投稿サイトの記事を読む」のはダメです。誰がなんと言おうが絶対にダメです。念の為もう一度書いておきますが、ダメです。

 投稿型のサイトには様々なコンテンツがありますね。数が多いですから、ネットの検索にも良く引っかかります。読んでみると「良さそう」なものもあるし、明らかに「ダメそう」なものもあるわけです。そうするとつい「良さそう」な記事を読みたくなりますが、ダメです。

信用できないものは読むな

 何よりも初学者が知るべきは「初学者にはいい記事に見えるが、プロが見れば噴飯モノのクソ記事」が存在するというという悲しき事実です。

 いいですか。あなたが読もうとしている「良さそう」な記事は、実のところカレー味のうんこかもしれないのです。うんこ味のカレーかもしれませんが。

 人狼というゲームを知っていますか? プレイヤーはお互いの正体を知らず、人狼の影に怯えては手当たり次第に参加者を処刑していき、人狼が全滅する頃には村人もだいたい死んでいます。プログラミング初心者が投稿サイトの記事を読めば、これと全く同じ危険を冒すことになります。記事のクオリティを判断するすべを持たないあなたは、良質な記事を処刑し、クソ記事を熟読することになるのです。あまりにも馬鹿馬鹿しいですね。

 そのような無意味な作業をせず、確実に信用できる公式ドキュメントを読みましょう。

読む価値のないものは読むな

 大抵の「解説記事」なるものは、公式ドキュメントの焼き直しか、単なる「つくれぽ」です。となれば、あなたはどうして直接、公式のドキュメントを読まないのですか?

 プログラミングをしていれば、分からないことだらけです。そして我々が「分からねえなあ」と思うことは大抵、全世界のみんなが分からないのです。それゆえ公式ドキュメントに答えが書いてあることがほとんどです。説明書があるのですから、説明書を読みましょう。

 プログラミングをしていけば、頼れる解説がないトラブルに出くわすことはザラです。自分が世界で初めてバグを踏むこともあるでしょう。そういう時に「解説記事」を探すようでは困りますね。公式ドキュメントを読み、ソースコードを読み、GitHubで報告する。公式ドキュメントから、そのための力を得ましょう。

古い情報を読むな

 どんなに良い記事も刻一刻と古くなっていきます。しかし初学者にはそれを見抜く力がありません。

 例えば経験者なら「このソースコードフレームワークのバージョン4だな、古いな」などと一目で理解できる場面でも、初心者は「これが最新の書き方か」となってしまいます。そうしてそのまま手元の最新バージョンにコピペしてプロジェクトをぶっ壊すのです。

 書かれた時期は問題ではありません。たとえ昨日書かれた記事でも、その記事が最新情報を反映している保証はないのです。中身をよくみると数年前の記事のコピペかもしれません。そんな危ない記事を読むべきではないのです。メンテナンスされた公式ドキュメントを読んでください。

吟味されていない議論を読むな

「動きました!」

「これで起動できました!」

 大変結構なことですね。しかし、それはアンチパターン (やめとくべきパターン) かもしれません。その記事に書いてある処理を実装した結果、半年後にとんでもないことになっているかもしれませんが、そんなことを記事から読み取ることは(初心者には)できません。

 もしあなたが「河豚を焼いたら激ウマ!焼き河豚サイコー!」という記事を信用して真似すると、これは確実に死にますね。そういうことはやめておきましょう。正しい対処法は公式ドキュメントに書かれています。正しい知識で、あなた自身の身を守りましょう。

でも実際、英語難しいよね

 初心者なのに公式ドキュメント(英語)を読めって言っても無理だよジョージ!助けて!というそんな場合の心得

自分で投稿する

 これはもうハッキングみたいな話なんですが、初心者が投稿サイトに間違った記事を投稿すると、その道のプロを激怒させ、辛辣なツッコミが入ることがあります(真剣に書いたことが伝わる記事だと尚いいです)。これは初心者にとって非常に勉強になります。もっともプロは初心者の記事であることを一眼で見抜きますから、多少は手加減してくれるかもしれませんが。

 普段なら単価いくらで請求書をぶん投げてくる本物のプロが、なんと無料で、たった一人あなたのために時間を費やし、愛の鞭をくれるわけです。非常にありがたいですね。「分かる人が無料で教える」ということ、これはITコミュニティの美徳であり名誉であると、個人的には思います。なんか海賊みたいでカッコ良いです。海賊よく知らんけどさ。

 ただし世の中には、偉そうにツッコミを入れてくるクセに全部間違っているというふざけた輩もいます。無批判に信じてはダメですね。

公式ドキュメントに言及する投稿を読む

 本当に信頼できる投稿者は、情報の出所に注意しています。

 最新の公式ドキュメントやソースコードに言及した上で「ここが不正確である」とか「間違っている」とか「説明不十分である」という場合、その記事はたぶん読む価値があります*2

 そうでなくても、一度公式ドキュメントを読んだあなたは「ああこいつ公式ドキュメントを読んでないな」と鋭く見抜く力を得ますから、たぶん大丈夫です。たぶん。

結局、人に聞くのが一番

 twitterとかで人に聞くのが一番いいよ。当然わしに聞いてもいいけど。人間関係は大事ね!

*1:某iitaとか、tera某ilとか、日本語版Stack Over某とか

*2:そう、ぶっちゃけ公式ドキュメントは間違いだらけなのである。南無三!

飲みたくて飲みたくて…

 いや震えてないです。

 久しぶりにお酒をガッツリ飲みたくなったので、バランタインのファイネストくんを買ってきました。諸般の事情により24時間営業スーパーマーケットのお酒コーナーに通暁するエンジニア諸氏におかれましては、これはもう常識のことかと存じますが、こいつは日本全国で手に入るスコッチとして指折りのコスパと風格を備えておりますね。マッサン以来の値上がりに胡座かいてやがるとしか思えない本邦のウィスキーとは比べ物にならぬ充実ぶりです。

 まあ、ウィスキー飲み始めの人にはちょっとパンチが強めかもしれないです。そういう時は、お行儀が悪いかもしれないですけど、匂いを嗅いでちょっと舐めるくらいでいいかも。

 安くて美味くて面白い酒っていうとズブロッカなんですが、これはスーパーでは売ってないんですよね。まあバランタインが強すぎるのであります。

 飲みながら考えてみると、自分のアルコール知識って学生時代から更新されてないんですよねえ。お金がないって怖い…

身辺整理中

 あっちのアカウントを消して、そっちのアカウントを潰して、こっちのアカウントに移して、みたいなことをやってます。インタネット断捨離。

 その流れでブログも抹殺したので、ちょうどいい機会だし、nuxtあたりで自力再構築しようと思ったんです。ところがあれ、SSRがアピールポイントなのに、誰でもブログ構築を最初に思いつくはずなのに、どこをどう探してもブログ用テンプレートがないのでびっくりしました。ブログ用モジュールなるものはありましたが、説明書がない… でまあ諦めました。

 わし別に、はてブロでいいし。はてな好きだし。そうだし。

 そういうわけなんで、ブログに熱意を注ぐエンジニア諸氏にはGatsbyをオススメします。VueじゃなくてReactだけど、静的サイト作成に特化していて、WebpackローダーとGraphQLを駆使してコンポーネント側から必要なデータを要求できますよ。例えば「マークダウンの著者欄だけもってこい」とか「2018年1月の記事だけよこせ」とかコンポーネントの方から定義できるわけです。これはもう他のジェネレーターとは水準が違います。Reactじゃなければ完璧なんですけどねえ。