トップ 最新 追記

2018-07-01 [長年日記]

_ JavaScriptのPromiseではまずcatchから書け

以下は Promise 全般について詳しい者が書いているわけではなく、単に JavaScript の実装に基づいて分かったことを書いている程度の話です。

Promiseについて説明されないこと「catchが何をcatchしているのか」

少なくとも JavaScript の実装では

  • Promise が reject したもの
  • catch 以前に Promise call 後に throw された例外

の両方を catch してしまう。

何が困るのか

特にサンプルとして世の中に出てくるコードの大半は残念ながら

Promise
.then(func)
.catch(func)

の形をしている。ここに罠がある。具体的には

then の中の例外がすべて catch に吸い込まれてしまう。

この Promise はこういう挙動をするはずだ、例えば「Fetch API の catch はネットワークエラーに対応しているはずだ」という理解でコードを書き始めるとしょっちゅう意図しない catch が起き、then を途中ですっ飛ばしてしまうという挙動に悩まされる。しかも console には何も出ない。

これは以下のような簡単なコードで確認できる。

fetch('/success', {method: 'POST'}).
  then((response) => {
    throw new Error();
    console.log(response)
  }).
  catch((err) => {
    console.log(err)
    console.log('network error')
  })

ネットワークエラーが起きていないにも関わらず network error と console には表示される。(この場合はその前に throw new した Error も表示される。)

まずcatchしろ

上に対する解決策は単純で、

「まず reject を catch で処理しろ、そのあとに then を書け」

になる。こうすれば then の中のミスを問題なく普通に runtime に伝えることができる。

※ はじめ「さもなくば捨てろ」と書いたが、Promise の引数は reject, resolve の順で必須だし、Node.js 8 では catch がないと UnhandledPromiseRejectionWarning が出まくるので勧めない。

Promiseは本当に面倒くさいと思った、

Promise は callback 地獄に対する回答のように見えるが、

  • 値の Proxy(取得タイミングはお任せ)
  • Promise をもとに「処理を記述する」

の複数の役割を持っており、単なる Proxy なら単に await すれば値が取得できるだけのように見えるが、Promise ベースで catch/then で「状態を待ち、処理を記述していく」際には、「状態こそ不変かもしれないが Promise の reject もその他の例外も『記述順によっては丸ごと catch される』」という繊細な挙動をする」。

この辺が実際にものを作っていく際に本当に難しいなぁというか、異常に面倒くさい感じがしましたとさ。

どうしても then の中が複雑になる場合はそこだけ切り出して別な名前空間で書いた方がいいんでしょうね。渡ってくる response が何かは分かってるわけで、テストに十分なデータのパターンを作って Promise の外で普通に TDD で作ります、自分なら。

おしまい。

参考

Tags: JavaScript

2018-07-04 [長年日記]

_ Vue componentのevent handlerのテストはwrapper#triggerから

単純なメモ。

<template>
  <container>
    <button @click="handler">押して</button>
  </container>
</template>

<script>
export default {
  methods: {
    handler() {
      // これをテストから直接呼ぶとrender済んでない可能性大
    }
  }
}
</script>

こんな Vue component があるとする。

端折りまくったテストコードはこんな感じに書けるんだけど、ここで handler() を直接呼んではいけない。

import Komponent from 'komponent.vue'
..

describe('Komponent', () => {
  var wrapper

  function mountComponent(component) {
    return mount(component, {localVue, router, ..})
  }

  describe('condition', () => {
    beforeEach(() => {
      wrapper = mountComponent(Komponent)
    })

    it('wrong example', () => {
      wrapper.vm.handler()
    })

    it('good example', () => {
      wrapper.find('button').trigger('click')
    })
  })
})

どうも直接 handler() を呼ぶと component が render される前に handler() が呼ばれてしまうらしく、少なくとも DOM 上のデータは取得できない。

単純なロジックだけのメソッドならこれでもテスト可能だが、event handler としてのテストをする場合はちゃんと event から呼んであげるのがよいらしい。

で、event を起こすには、まず event を起こしたい要素を特定してあげる。wrapper.find() で特定してあげると DOM wrapper のようなオブジェクトが取得できるので、そいつで event を trigger する*1。すると実際に Vue component が render されているような状態で handler() を動作させることができる。

※ ちなみに render という function も export されているのだが、これは @vue/test-utils ではなく @vue/server-test-utils に含まれるものであり、component のテストではなく、render された HTML に対するテストしかできない。mount や shallow の代わりには使えない。

*1 @vue/test-utils を使ったテストはとにかく wrapper を相手にする


2018-07-10 [長年日記]

_ lodash v4以降のpairsがArrayとObjectの変換に便利

JavaScript は ES2015 以降、言語標準の機能が強化され、Array の操作だけなら別に Underscore / lodash は必要ないかなと思い、最近はできるだけ粘って入れないようにしています。

ですが、どうしても Object と Array の変換がコンパクトに書けません。

Ruby ならこういうので一発なわけですよ。

Hash[*[['a', 'b'], ['b', 'c'], ['c', 'd']].flatten]
# => {"a"=>"b", "b"=>"c", "c"=>"d"}

で、軽く変換方法を見るとまーだいたい副作用ベースで forEach で書くわけですけど、いやそれやりたくないなぁと思って、稀によく利用する lodash を調べたらありました。

_.fromPairs - Lodash Documentation

_.fromPairs([['a', 1], ['b', 2]]);
// => { 'a': 1, 'b': 2 }

なんてこった Ruby そのままというか、flatten がない分むしろスマートです。

逆もあります。

toPairs と toPairsIn の違いは本家ドキュメントを参照してください。

lodash v4 がリリースされたのが 2016年1月なのですが、コツコツ更新されてるんですねー。*1

さらに、「これだけのために lodash 入れるのやだなぁ」と思う方のために朗報。なんと、個別に npm があります。

こいつらを使えば必要な function だけを導入できるわけです。いやー至れり尽くせり。

Tags: JavaScript

*1 これを書いている時点で最新の 4.17.10 が 2018-04-24 リリース。Underscore.js の方も最新リリースが 2018-06 で継続中。


2018-07-12 [長年日記]

_ ChromeDriverを使ったE2Eテストは再現性に注意が必要

他の環境ではちょっとどうなっているのか分からないのでとりあえず Ruby 限定の話。

登場人物は以上。以下、メモ。

  • ChromeDriver は standalone server である
  • Ruby では ChromeDriver を利用しやすくしてくれる chromedriver-helper gem がある
  • これを使うと chromedriver を起動するコマンドとこれのアップデートを支援する chromedriver-update コマンドがインストールされる
  • chromedriver-helper gem のバージョンと chromedriver server のバージョンは一致しない
  • chromedriver-update を叩いた時点での最新バージョンが常に安定バージョンとは限らない
  • 明示的にバージョンダウンするには chromedriver-update <version> とする(helpは特にない)

これを CI で再現するには chromedriver-update をとりあえず叩くようにしておきつつ、コケたらバージョン指定してダウンするといった対応を即座にしていく必要がある。のかな? いや違うな、CI で動く Chrome のバージョンの変更を検知するところからか?

とにかく PhantomJS 時代と違って Headless Chrome でいこうと思ったら Chrome, ChromeDriver, chromedriver-helper gem のバージョンの情報を追う必要があるので、ちょっとこれまでより気を使うことが増えるなぁという感想でした。

Tags: Chrome Ruby

2018-07-13 [長年日記]

_ Laravelアプリはお手軽にscale outしないので注意が必要

Laravel で Form を利用して、エラーだった場合にそれをどう返すかのサンプルコードは 4.2 でも 5.6 でも以下のように

redirect で返すようになっている。

そして Form を組み立てる View 側では Input::old() を使うとよいということになっている。さっき入力していたデータを復元してあげないと不親切だからだ。

しかしこの組み合わせはクセモノ。

何が起きるかというと Session データに Form の Input すべてを載せることになるので、実質 Session Driver に Cookie は利用できない*1。つまり、アプリケーションサーバだけでカジュアルに scale out することはできないということになる。

Session Driver はすべてバックエンドのストレージ前提になり、DBMS や memcached, Redis などが必要になる*2。これらの準備が簡単なよくできた PaaS を使っているならよいが、そうでないならインフラの知識も必要になってしまうので、注意しておいた方がよさそうだ。

しかし、この Form の error を redirect で返す方法って Laravel のこのコードで初めて見たんだけど、こういう設計って推奨されるんですかねえ…?

Tags: PHP

*1 Input がどんな大きさになるのかはよほど注意深く作らないとコントロールできないだろう

*2 当たり前だが file は scale out しない。てゆーか DBMS に Session データを置くのも scale out しないような?