読者です 読者をやめる 読者になる 読者になる

野次馬エンジニア道

野次馬な気持ちでプログラミングをあれこれと綴ります

ElasticSearch入門 (2) キャッシュ・検索

ElasticSearch

前回は基本概念をみたので続き。参考にした書籍が古すぎるので思い切って

Mastering Elasticsearch - Second Edition

Mastering Elasticsearch - Second Edition

で興味があったキャッシュと検索の動作を見てみた。Chapter 6. Low-level Index Controlが特に参考になる。

キャッシュ

フィルタキャッシュ

全文書(match_all)の中から"programming"のカテゴリにある文書を検索する例。この場合検索結果はキャッシュされる。 index.cache.filter.typenodeindexがある。デフォルトはnode。一番オーソドックスなキャッシュ。

GET books/_search
{
  "query": {"match_all": {}},
   "filter" : {
    "term" : {
     "category" : "programming"
    }
   }  
}

フィールドのキャッシュ

ElasticSearchはフィールドのデータを使うファセット、アグリゲーション、スクリプト実行やソートを行う際に データをメモリ上に一旦読み込む。I/OやCPU的にコストが高い処理なのでバージョンアップ毎にLuceneレベルでの改善が行われている。

その中の一つがインデキシングの際にインデックス毎にフィールドを"ディスクに"保存したDocValue*1

    "category" : {
     "type" : "string",
     "index" : "not_analyzed",
     "doc_values" : true
     }

で明示的にコントロール可能。

以前デフォルトだったfielddataはオンメモリなので、fielddataを使って出現頻度や正規表現を使ったフィルタも可能。例えば出現頻度で、50%以上のものだけに絞りこむ場合のクエリは

{
 "book" : {
  "properties" : {
   "tag" : {
    "type" : "string",
    "index" : "not_analyzed",
    "doc_values" : false,
    "fielddata" : {
     "filter" : {
      "frequency" : {
       "min" : 0.5,
       "max" : 0.99,
      }
     }
    }
   }
  }
 }
}

シャードレベルのクエリキャッシュ

settingsのところにある設定に注目。ElasticSearchはデフォルトでクラスタとして構築される。検索が行われると関連のあるシャードにクエリを投げる。その際のノードのローカルにキャッシュする設定。 1.4から導入*2されていてデフォルトは無効。

  "settings": {
    "index.cache.query.enable": true
  }

検索

ここまでの設定例で具体的に検索してみる。

インデックス作成

マッピングを作って

PUT books
{
 "settings" : {
  "number_of_shards" : 1,
  "number_of_replicas" : 0,
  "index.cache.query.enable": true
 },
 "mappings" : {
  "book" : {
   "properties" : {
    "category" : {
     "type" : "string",
     "index" : "not_analyzed",
     "doc_values" : false,
     "fielddata" : {
      "filter" : {
        "frequency" : {
        "min" : 0.5,
        "max" : 0.99
        }
      }
     }
    }
  }
 }
}

でバルクで投入

POST _bulk
{ "index": {"_index": "books", "_type": "book", "_id": "1"}}
{"category":["programming","algorithm"]}
{ "index": {"_index": "books", "_type": "book", "_id": "2"}}
{"category":["design pattern","architecture"]}
{ "index": {"_index": "books", "_type": "book", "_id": "3"}}
{"category":["programming","javascript"]}
{ "index": {"_index": "books", "_type": "book", "_id": "4"}}
{"category":["algorithm","statstics","programming"]}

"programming"を含む文書の例は、

GET books/_search
{
  "query": {"match_all": {}},
   "filter" : {
    "term" : {
     "category" : "programming"
    }
   }  
}

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 1,
    "hits": [
      {
        "_index": "books",
        "_type": "book",
        "_id": "1",
        "_score": 1,
        "_source": {
          "category": [
            "programming",
            "algorithm"
          ]
        }
      },
      {
        "_index": "books",
        "_type": "book",
        "_id": "3",
        "_score": 1,
        "_source": {
          "category": [
            "programming",
            "javascript"
          ]
        }
      },
      {
        "_index": "books",
        "_type": "book",
        "_id": "4",
        "_score": 1,
        "_source": {
          "category": [
            "algorithm",
            "statstics",
            "programming"
          ]
        }
      }
    ]
  }
}

"programming"を含む3文書がマッチ。続いてアグリゲーション。

GET books/_search
{
  "query" : {"match_all": {}},
  "aggregations" : {
    "filter" : {
      "terms" : {
        "field" : "category"
      }
    }
  }
}

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1,
    "hits": [
      {
        "_index": "books",
        "_type": "book",
        "_id": "1",
        "_score": 1,
        "_source": {
          "category": [
            "programming",
            "algorithm"
          ]
        }
      },
      {
        "_index": "books",
        "_type": "book",
        "_id": "2",
        "_score": 1,
        "_source": {
          "category": [
            "design pattern",
            "architecture"
          ]
        }
      },
      {
        "_index": "books",
        "_type": "book",
        "_id": "3",
        "_score": 1,
        "_source": {
          "category": [
            "programming",
            "javascript"
          ]
        }
      },
      {
        "_index": "books",
        "_type": "book",
        "_id": "4",
        "_score": 1,
        "_source": {
          "category": [
            "algorithm",
            "statstics",
            "programming"
          ]
        }
      }
    ]
  },
  "aggregations": {
    "filter": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "programming",
          "doc_count": 3
        },
        {
          "key": "algorithm",
          "doc_count": 2
        }
      ]
    }
  }
}

50%以上なので集計の方は"programming"と”algorithm”だけになっていることがわかる。

React入門 - コンポーネント・サーバサイドレンダリング・Flux

React JavaScript

年末年始は本屋でオライリーのカレンダーをもらうのが毎年恒例。カレンダー欲しさに買った

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

最近周囲のプロジェクトでも採用されることが徐々に増えているのでサッと目を通した。

コンポーネントの合成

Reactとはどのようなものか。ReactのコンポーネントはHTMLの要素にJavaScriptの表現力が追加されたようなイメージ。6章のコンポーネントの合成のP47の文章を借りると、「コンポーネントはpropsとstateを入力として受け取り、HTMLを出力として返す一種の関数のようなもの」となる。また考え方として

P47より

Reactは「継承(inheritance)」よりも「合成(composition)」を好みます。小さくて単純なコンポーネントを組み合わせて、大きくて複雑なアプリケーションを構築するのです。他のMVCフレームワークオブジェクト指向のツールに慣れた方は、React.extendClassのような関数を期待するかもしれませんが、そのようなものは存在しません。

propsとstate

propsはプロパティのことでコンポーネントに渡されるデータ。作成時に属性値として渡されてthis.propsで参照可能。あくまで参照用。一方のstateはコンポーネントが持つ内部状態のこと。コンポーネントの内部でのみ利用される。必ずsetStateで更新をかける。するとコンポーネントrenderが呼ばれる仕組み。

JSX

コンポーネントの記述にはJSXが使える。JSXは「JavaScript XML」の略。React内のコンポーネントでHTMLのようにXMLで宣言的に記述できることで構造化が容易になる。ビルドツールもしくはライブラリを使ってオンデマンドにJavaScriptに変換可能。

React.render(
    <h1>Hello, world!</h1>, document.body
    // React.createElement("h1", null, "Hello, world!"), document.body に変換
);

ref属性

JSXの中の特別な属性の一つ。コンポーネントのthisコンテキストから別のコンポーネントを参照するためのReact独自の属性。親のコンポーネントから子のコンポーネントを参照可能。次の例だと、this.refs.myInputのようにアクセスする。

render : function() {
    return <div> 
        <input ref="myInput" />
    </div>;
}

P19より

ref経由でアクセスできるオブジェクトは「バッキングインスタンス」と呼ばれて実際のDOMではなく、コンポーネントの情報が記述されたもので、Reactは必要であればこの記述から実際のDOMを作成します。実際のDOMにはReact.findDOMNode(this.refs.myInput)のようにアクセスします。

React.findDOMNodeを使えば実際のDOMにアクセスできるが、renderのメソッドの中ではDOMノードにアクセスできない。初回の描画が成功して実際のDOMが表示されたタイミングで呼ばれるcomponentDidMountの中でReact.findDOMNode経由でDOMにアクセス可能となる。

key属性

これもJSXの中の特別な属性の一つ。Reactの中身がどのように動作しているのか垣間見える。11章のパフォーマンスチューニングの記述を引用する。

P105より

key属性は不要な処理をさけるために使用されます。例えば、<div key="foo">のように定義されたコンポーネントがあったとして、key属性の値が後ほど"bar"に変更された場合、Reactは差分計算を行わずに、即座に子コンポーネントも含めた全ての仮想DOMオブジェクトを破棄し、一から描画し直します。

リストなどを使っていて全体を再描画する場合は無駄な計算をスキップすることができる。一方で順番が変わるだけ場合すでにその要素はDOMツリーの中に存在するので、

P106より

return <img src={item.src} key={item.id}/>; これにより、Reactはkey属性をもとにリストの要素の順番だけが変更されたことを知ることができるので、img要素のsrc属性を設定する代わりに insertBeforeを実行して、単純にDOMノードを移動します。

のようにチューニング可能。

サーバーサイドレンダリング

シングルページアプリケーションを構築している人が次に気になるのは12章のサーバーサイドレンダリング

SEOに関しては先日 AJAX crawlingが非推奨になったこともあり*1それが動機となることはないかもしれないが、冒頭にある

P109より

シングルページアプリケーションはクライアントサイドでHTMLページを作成するので、検索エンジンのインデックス対象になりません。 また、JavaScriptのロードが完了してからページを作成するので、初回のページ表示が遅いという問題があります。 サーバーサイドレンダリングはこれらの問題に対する有効な解決策となりえます。

というのは非常に大きい利点。それも仮想DOMのなせる技といえる。

Reactのコンポーネントは、実際のDOMに反映する前にメモリー上のデータとして仮想DOMを出力する。それを単純にHTML文字列として出力しサーバクライアントで共有することができる。サーバーサイドではターゲットのDOMを引数に設定しないReact.renderToStringを実行する。

data-react-checksum

renderToStringをするとReactは二つの属性を追加する。

<div data-reactid=".fgvrzhg2yo" data-react-checksum="-1663559667">Hello, world!</div>

data-reactidはDOMノードを特定するために利用されてstateやpropsに応じて変化する。data-react-checksumはサーバサイドのレンダリング時にのみ追加される。サーバサイドで作成されたDOMが再利用可能かチェックするための値。

ライフサイクル

renderよりも後のコンポーネントのライフサイクルのメソッド(componentDidMount, componentWillUnmount)はサーバ側では呼ばれない。なのでイベントリスナーをcomponentWillMountで登録せずにcomponentDidMount, componentWillUnmountで登録削除を行うのがベストプラクティス。

Flux

アーキテクチャパターン。単一のデータフローを強いることによってアプリケーションの動作が推測しやすい利点がある。

P200より

Fluxは3つの主要なパーツ - Store, Dispatcher、View (Reactのコンポーネントツリー) - で構成されています。さらにActionを4つの目の主要なパーツとみなすことができます。Actionに補助的なメソッドを提供することで、Dispatcherへのインターフェースとして機能します。

Fluxにおいて、最上位のReactコンポーネントはController-Viewとしての役割を担います。

書籍の図は日本語訳にへ変更されているがイマイチピンとこない。のでオリジナルの図*2を見てみる。

https://raw.githubusercontent.com/facebook/flux/master/docs/img/flux-diagram-white-background.png

Action・Dispatcher

ユーザがUIに対して行った操作はAction経由でDispatcherへ。

全てのデータは必ずDispatcherを通ることに注意。Dispatcherシングルトンオブジェクトになっている。Storeへ通知と依存の管理はDispatcherが行う。実際のアプリでは複数のStoreを扱うユースケースもありえる。その際のアイデアとしては、

P206より

  • DispatcherはActionをキューに格納するように変更する
  • DispatcherはActionの処理が完了するまで処理を停止できるように変更する
  • Dispatcherにコールバックを登録する際に、どのActionに対して待ち受けるか指定できるようにする

Actionは言わばDSLの役割を果たしていることになる。ユーザのUIなどの操作をStoreが意味として理解できるように変換する。「補助的に」という説明はここからきている。

Store

データのやり取りは全てここに集約しここにビジネスロジックもここに入れる。その際

P204より

  • Storeはアプリケーションのすべてのデータを保有する
  • アプリケーションの他の部分はデータの操作方法を知らないしたがって、Storeはアプリケーションで唯一データの変更が行われる場所である
  • Storeはgetterメソッド(値を取得するためのメソッド)のみサポートし、setterメソッド(値を変更するためのメソッド)を持たない。データの変更はすべてDispatcherのコールバック経由で行われる

に注意する。データとそれに関連する操作を一緒に持つのがポイント。さらに図の通りchangeイベントが発行されてそれがController-View経由で親から子の一方向で処理されていく。

まとめ

単純な入門書というよりは、基本となるReactの思想から実践的な設計やテスト手法まで網羅されている充実の内容だった。実務で使うにはもう少し利用例や組み合わせて使うライブラリなどの良し悪しを検討する必要がありそう。

ElasticSearch入門 (1) 基本概念・インデックス作成・アナライザ

ElasticSearch

近所の本屋で売っていたので興味を持って購入。現在のElasticSearchのバージョンは2.1。書籍は0.90.11とかなり古い。色々変わっていたのでメモ。

高速スケーラブル検索エンジン ElasticSearch Server

高速スケーラブル検索エンジン ElasticSearch Server

本書はcURLを使って読み進める前提だが、リクエストとDSLを手で書くのは骨が折れる。 今ならSenseという専用のエディターが使える*1。 補完も聞きJSONのバリデートもしてくれるのでそちらで絶対に試すべき。

デーモンでの起動・停止

書籍では-fとなっていた。

./bin/elasticsearch -d

で、ついでに停止のAPIは無くなった*2

スクリプトによるドキュメントの更新

curl -XPOST http://localhost:9200/blog/article/1/_update?pretty -d '{"script": "ctx._source.content=\"new content\""}'

な感じで更新する例がでているが、登録したドキュメントの更新にはconfigの変更が必要*3

script.inline: on
script.indexed: on

基本概念

インデックス(Index)

データを保存する場所。RDBでいうところのテーブルに該当する。ElasticSearchは検索を目的としているので元の文書の格納は必ずしも必要ない。 実体は転置インデックスであることをイメージすると理解が進む。

ドキュメントタイプ(Document type)

一つのインデックス内に複数の用途に応じたオブジェクトを格納する場合。インデックスを分けるかタイプを分けるか? 下記の記事が参考になる。

ノード(Node)とクラスタ(Cluster)

ElasticSearchは可用性と耐障害性を実現するために複数のサーバ上で実行される。このサーバ群をクラスタ、個々のサーバをノードと呼ぶ。

シャード(Shard)

大量のデータを扱うときはそれぞれのノードでデータを分割して保持する。その際にシャードと呼ばれる部分インデックスで分割する(シャードのそれぞれの実体はApache Luceneのインデックス)。アプリケーションはクラスタに対してRESTのAPIで検索をかけるが、その際それぞれのシャードの配置や構成に関しては意識する必要がない。

レプリカ(Replica)

それぞれのノードにはシャードのレプリカが存在する。更新は勿論プライマリのシャードに対して行われるが、万が一プライマリのシャードが失われると新しく、レプリカのシャードがプライマリになる。

インデックス作成

スキーママッピング(Schema mapping)

ElasticSearchはスキーマレスで動的にデータを解釈することもできる。が、多くの場合あらかじめ構造を定義して用いる。

{
  "mappings": {
    "post" : {
      "properties": {
        "content" : {
          "type": "string",
          "index": "analyzed"
        }
      }
    }
  }
}

タイプ(Document Type)とフィールド

上の例では一つのタイプとしてpostが含まれている。フィールドの定義でのポイントは"analyzed"。これはElasticSearchがアナライザをかけて転置インデックスに格納するかどうかの設定。"not_analyzed"を指定した場合はそのままインデックスに入る。つまり完全一致のみとなる。さらに "not_analyzed"の場合はDoc Values*4というファイルを使ったキャッシュに入る。

マルチフィールド(Multi fields)

場合によっては2つのフィールドに同じフィールドの値を持ちたいユースケースもある。検索用とブラウズ用のような。そのためのデータの持ち方*5

{
  "mappings": {
    "post": {
      "properties": {
        "name": {
          "type": "string",
          "fields": {
            "raw": { 
              "type":  "string",
              "index": "not_analyzed"
            }
          }
        }
      }
    }
  }
}

アナライザ

ElasticSearchのアナライザとは、インデックスを作成したりクエリを解析するときにデータをどのように処理するかを指定する機能のこと。 インデックスの時であれば、どのように転置インデックスを作成するか。検索のときであればどのようにクエリを分解してインデックスにマッチさせるか。

アナライザはトークナイザと複数のフィルタから構成される。インデックス時と検索時で別々のアナライザを指定可能。 図で表すと下記の感じ。

f:id:notta55:20151221000950p:plain

独自のアナライザを定義すると下記のような記述となる。フィルタは順番が重要なことに注意。

{
 "settings": {
   "analysis": {
     "analyzer": {
       "ourAnalyzer" : {
         "tokenizer" : "standard",
         "filter" : [
           "asciifolding",
           "lowercase",
           "ourEnglishFilter"
          ],
         "char_filter" : ["html_strip"]
       },
       "filter" : {
         "ourEnglishFilter" : {
           "type" : "kstem"
         }
       }
     }
   }
 }
}

Android 2.3 の WebView のクロスドメインリクエスト(続編)

Android JavaScript

ここ数ヶ月かなり忙しく更新をサボっていたので疑問に思っていたことを。

モバイルのサイトをやっているとどうしても避けられないのが古いデバイスへの対応。 悩ましいのがAndroid2.3のようなデバイス達*1。例え中古で二束三文で売られていたしてもは出来の悪いかわいい子などとは到底思えない。

CSSや操作に関する問題は別のサイト*2に譲るとして、血の気が引きそうな

uupaa.hatenablog.com

の記事に関して自分でもう一度調べて(勝手に)続編を書いてみた。

結果: 再現 (但し条件あり)

「最初の1回だけ」というのがポイントのよう。つまりキャッシュに起因した問題。 前述のブログの検証の通りjQueryでテストコードを書いて検証した。 ここでいうキャッシュとは、$.ajax({ cache: true のこと。

2回目のクロスドメインリクエスト処理結果

NGの場合はstatus=0 が返ってくる。

OS Version Browser Cache JSONP GET POST DELETE
Android2.3.4 標準ブラウザ 有効 OK NG OK NG
無効 OK OK OK NG
WebView 有効 OK NG OK NG
無効 OK OK OK NG
Android2.3.5 標準ブラウザ 有効 OK NG OK NG
無効 OK OK OK NG
WebView 有効 OK NG OK NG
無効 OK OK OK NG

Android4.0.4 と Android4.4.2では全てOK。iOSでも問題なし。特にWebViewと標準ブラウザでの挙動の違いは見られない。 JSONPとPOSTを使っていれば問題無しという結果に。

DELETEがキャッシュを無効にしても失敗?

次の疑問はDELETEの列。ヒントになったのはキャッシュ無しをどう実現しているかだった。

XMLHttpRequestのキャッシュ

W3Cの記述を取り上げている こちらの記事を見ても

var req = new XMLHttpRequest();
req.open ('GET', 'test.txt', true);
req.setRequestHeader('Pragma', 'no-cache');  // HTTP/1.0 における汎用のヘッダフィールド
req.setRequestHeader('Cache-Control', 'no-cache');  // HTTP/1.1 におけるキャッシュ制御のヘッダフィールド
req.setRequestHeader('If-Modified-Since', 'Thu, 01 Jun 1970 00:00:00 GMT'); // 指定日時以降に更新があれば内容を返し、更新がなければ304ステータスを返すヘッダフィールド。古い日時を指定すれば、必ず内容を返す

のように設定すればよいと思われる。実際クロスドメインでこれらのヘッダを設定するにはおそらく Access-Control-Allow-Headers などサーバ側の対応も必要(今回は試せていない)。

jQueryajaxのキャッシュ

jQueryのキャッシュはこのXMLHttpRequestのものとは別でソース(http://code.jquery.com/jquery.js)を見てみると

// Add anti-cache in url if needed
if ( s.cache === false ) {
        // 省略 
    // Otherwise add one to the end
    cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
}

こんな感じでURLに意味のない文字列_=1445733432387を付与していることで一意にすることでキャッシュ無しを実現している。 が、POSTやDELETEにcacheオプションを設定してもエラーにならないが、POSTやDELETEのときに飛んでいるリクエストを見ると付与されないことがわかる。

POSTは元々平気なのでDELETEのときだけキャッシュの問題を踏んでしまい、先ほどのように動作しない結果となる。

実際にDELETEのリクエストURLに'&_='+Math.floor($.now()/1000))を付与したところ正しく動作した。

まとめ

ひとまず自分が行った検証結果では

  • 標準ブラウザとWebViewの違いは見られない
  • JSONPやPOSTは問題無し
  • GETやDELETEはキャッシュのコントロールに気をつければ2回目の通信は可能

となった*3

*1:Android2.3のシェアはDashboards | Android Developersによると3.8%

*2:to-R AndroidiPhoneのHTML,CSS,JavaScriptのバグまとめ http://blog.webcreativepark.net/2012/03/13-093853.htmlは特に役立つ

*3:よく考えるとサーバ側はPOST/DELETEは更新なのでレスポンスをno-cacheで返すのが正しいやり方なのだろうか

モバイルWebアプリのテスト自動化 (2) - Appium

Android Testing JavaScript

前回は、iOS向けの記事だったので、 Android向けもメモ。

環境のセットアップ(Android)

Android SDK環境変数のチェック

appium-doctor -–dev

でOKか確認する。2015/05/24時点の最新はAPI Level: 22だが、Appiumの一部モジュールのビルドのため18,19,21もインストールしておく。前回でAppiumのソースをGitでクローンしているのでそのまま

./reset.sh --android --dev

でビルドしてしまう。

サンプルコードの実行(実機)

appium/mobile-web.md at master · appium/appium · GitHub

Pre-requisites:

  • Make sure Chrome (an app with the package com.android.chrome) is installed on your device or emulator. Getting Chrome for the x86 version of the emulator is not currently possible without building Chromium, so you may want to run an ARM emulator and then copy a Chrome APK from a real device to get Chrome on an emulator.

という記述が。なので実機にChromeのAPKをインストールしておく。

appium/submodules/sample-code/sample-code/examplesにあるios-safari.jsを編集する *1

    var desired = _.clone(require("./helpers/caps").android19);
    desired.browserName = 'chrome';
    if (process.env.SAUCE) {
      desired.name = 'android - chrome';
      desired.tags = ['sample'];
    }
    return driver.init(desired);
  });

わかりやすくandroid-chrome.jsにリネームして実行。

mocha android-chrome.js

Chromeをアプリとして動作させるかブラウザとして動作させるか

の記事も参考になる。

*1:標準ブラウザの場合は、browserName='Browser’と指定する

モバイルWebアプリのテスト自動化 (1) - Appium

iOS Testing JavaScript

AndroidiOSに対応したオープンソースのテスト自動化フレームワーク

Appium: Mobile App Automation Made Awesome.

を試してみた。Appiumの特徴は

  • アプリのリビルドが不要
  • Webアプリとネイティブとを混ぜたハイブリッドアプリに対応
  • 様々なプログラム言語のバインディングもあり

インストール自体は、

npm install -g appium
npm install wd  # get appium client

だけだが、試行錯誤した箇所も多数あったので早速メモ。 今回はiOSSafari上でのテストを試してみる。

環境のセットアップ

シミュレーターの利用の許可

sudo authorize_ios #Appliumをインストールすると実行可能

ideviceinstallerのインストール

brew install ideviceinstaller

端末側の設定

ios-webkit-debug-proxyのビルド

brewでインストールされるバージョンでは、

Unknown app_id PID:721
Invalid message _rpc_applicationSentListing: 

というエラーとなり正しくテストが実行されない。途方に暮れていたところ

appium/ios-webkit-debug-proxy.md at master · appium/appium · GitHub

下記の記述があるのを発見。

NOTE: As of April 2, 2015, the primary ios-webkit-debug-proxy repository has not been updated for some time. We currently recommend using James Chuong's fork. To do so, you'll need to build from source. If you're unfamiliar with building with GMake, try following the steps below.

なので、このフォークリポジトリから取得してビルド。

git clone git@github.com:jchuong/ios-webkit-debug-proxy.git
cd ios-webkit-debug-proxy.git
./autogen.sh
./configure           # for debug symbols, append 'CFLAGS=-g -O0'
make
sudo make install

SafariLauncher Appのビルド

Gitから直接取得してビルドする

$ git clone https://github.com/appium/appium.git
$ cd appium
./reset.sh --ios --real-safari --code-sign <code signing idendity> --profile <retrieved profile identity code>

SafariLauncherのビルドに関しては下記の記事が参考になる

自分の環境では/appium/submodules/SafariLauncher/SafariLauncher.xcodeprojが見つからず、代わりに./appium/node_modules/safari-launcher/の下にXcodeのプロジェクトファイルが見つかったので開いて設定してビルドした。

info: [debug] Responding to client with error: {"status":33,"value":{"message":"A new session could not be created. (Original error: ENOENT, stat '/usr/local/lib/node_modules/appium/build/SafariLauncher/SafariLauncher.zip')","errno":34,"code":"ENOENT","path":"/usr/local/lib/node_modules/appium/build/SafariLauncher/SafariLauncher.zip","origValue":"ENOENT, stat '/usr/local/lib/node_modules/appium/build/SafariLauncher/SafariLauncher.zip'"},"sessionId":null}

のようなエラーになる場合は、以下のどちらか

  • 自分でビルドしたSafariLauncher.zipをコピーする
  • node bin/appium -U (UDID)で起動

環境のチェック

$ appium-doctor --ios

サンプルコードの実行 (実機)

コードの入手

先ほどcloneしたAppiumのappium/submodules/sample-code/sample-code/examplesにサンプルがあるのでclone。今回はJavaScriptで実行したいのでnodeのディレクトリに行ってインストール。

npm install -g -f mocha
npm install

サーバーの起動*2

UDIDの指定が必要 *3

ios_webkit_debug_proxy -c (UDID):27753 
appium -U (UDID)

実行

mocha ios-safari.js

シミュレーターの場合は、ios_webkit_debug_proxyとUDIDの指定は不要 *4

*1:https://discuss.appium.io/t/ios-8-1-real-devices-you-need-to-enable-ui-automation-setting/1271

*2:停止 - killall -9 node

*3:Xcode6からは xcrun simctl list devices が使える

*4:手元の環境がiOS8.2だったので examples/node/helpers/caps.js の少し修正が必要だった

コードの複雑さ - テスタブルJavaScript

Testing JavaScript Design Pattern

テスト熱が高まっているので自宅で下記の本を発掘。放置していた間に本書でメインに取り上げられているYUIは残念ながらメンテナンスされなくなっているようだ*1

テスタブルJavaScript

テスタブルJavaScript

後半のツールに関する記述も内容が古いかもしれないが、前半の1,2章は著者の見識が伺える内容だった。

1章は、一般的なソフトウェア開発のアプローチを紹介した上でテストの方法論やツールを紹介していく流れ。 前回アジャイル開発の流れでBDDについて書いてなかったのでここにメモ。

P6.より

ビヘイビア駆動開発(BDD: Behavior-Driven Development)はTDDを元にしており、開発者もそうでない人も共通の言語を使ってアプリケーションやモジュールのふるまいを表現できるようにしようというものです。(中略) 例えば、testEmptyCartというテストを作成するのではなく、テスト対象のふるまいを「ショッピングカートが空の場合は、ユーザは購入手続きに進めません」というように記述します。(中略) BDDはアジャイル開発で使われているユーザーストーリーがテストへ直接変換されます。一般的には、ユーザーストーリーは「私は[誰か]として[何かを行っ]て、[何かの処理結果を得る]ようにしたい」というテンプレートに基づいていなければなりません。

ユーザーストーリを直接テストコードに落とせることで「これって前々回のイテレーションで実装したはずなんだけどデグレしてる」といった事態をプロセス的に防ぐ。

複雑さ

2章のテーマは複雑さ。保守が容易なJavaScriptは、疎結合であり短くて分離が可能。この辺りは特にJavaScriptに限った話ではない。有名な話だが、与えられた時間のうちに半分はテストやデバッグに費やされる(http://www.rand.org/content/dam/rand/pubs/papers/2008/P4947.pdf)。

JSLintとforループ

複雑さを減らす一つの方法としては、JSLintなどを導入してコードを常に健全にしておくというもの。本書ではforループの例が紹介されていた。例えば、JSLintでは、`var i = 0i++と書かずに

for( i = 0; i < a.length; i = i + 1) {
    a[i] = i * i;
}

のように書く必要がある。もっともJavaScriptの慣習としては、

a.forEach(function (val, index) {
    a[index] = index * index;
});

の方が意図も伝わりクリーン。JavaScriptは変数を巻き上げるのでこのコールバックのみのスコープを用意できる方が確かに優れている。

循環的複雑度(cyclomatic complexity)

コードの中での実行の経路の数でそのままユニットテストの個数になる。

P25より

Aivosto(http://www.aivosto.com/)が循環的複雑度と誤った変更が行われる確率との関係を調査したところ(http://www.aivosto.com/project/help/pm-complexity.html)、表2-1のような結果が得られました。

循環的複雑度(CC) 誤った変更の可能性(Bad fix probability)
1-10 5%
20-30 20%
>50 30%
approaching 100 60%

(中略)先ほどの論文の中でMcCabeは、循環的複雑度が16以上のコードは信頼性が乏しいとも述べています。

感覚的にこれらの数字を頭にいれながらコードやテストを眺めるだけでメリットはあるのではないかと思う。

再利用

「コードをなるべく書かないように書く」という個人的な経験にも通じる有名な話、

P28より

コードのうち85%はアプリケーション特有のものではなく、プログラムの独自性が発揮されるのはコードの15%にすぎないという試算を発表しました。

後ろの章でdupfindツール(https://github.com/sfrancisx/dupfind)をJenkinsに導入する例が紹介されている。

ファンアウト

関数が直接または間接的に依存しているモジュールやオブジェクトの数を表す指標。で、具体的にどう数値化されるかというと、

P30より

手続きAについてのファンアウトとは、Aから発するローカルなフローの数とAが更新するデータ構造の数を加えた値です。

で、ローカルなフローというのがさらに下記のように分かれている。

  1. AがBを呼び出す場合
  2. AがBからの呼び出しに対して値を返し、Bがこの値を使用する場合
  3. CがAを呼び出し、返された値をBに渡す場合

ファンイン

イメージとしては、コードが再利用されているか否か。

P39より

手続きAについてのファンインとは、Aに対するローカルなフローの数とAが情報を取得する対象のデータ構造の数を加えた値です。

これらの値を使って情報理論的に複雑さは、(ファンイン x ファンアウト)2で表されるとしている。この値とバグには明確な相関関係が見られる。つまり、設計段階でこれらの値を把握することで、複雑さを避けて保守のコストを下げることにつながる。

結合とインジェクション

サブモジュールを定義すればファンアウトの値は小さくなるが依然元のモジュールは必要となるため結合度は下がらない。 密に結合してしまっているコードをインジェクションを用いて書き換える例を。

//P34より
function makeChikenDinner(ingredients) {
    var chicken = new ChickenBreast();
    var oven = new ConventionalOven();
    var mixer = new Mixer();
    var dish = mixer.mix(chiken, ingredients);
    return oven.bake(dish, new FDegrees(350), new Timer('50 min'));
}
var dinner = makeChikenDinner(ingredients);

関数と5個のオブジェクトが密結合してしまっている。ここでインジェクションを用いて結合度を下げる。 まずは、関連付けられそうなものをFacadeパターンを利用して抽象化してしまって

//P35-36より
function Cooker(oven){
    this.oven = oven;
}
Cooker.prototype.bake = function(dish, deg, timer) {
    return this.oven.bake(dish, deg, timer);
}
Cooker.prototype.degree_f = function(deg) {
    return new FDegrees(deg);
}
Cooker.prototype.timer = function(time) {
    return new Timer(time);
}

とする。これでOvenとTimer, FDegreesがスッキリした。

他もインジェクションの形にすると

//P37より
function makeChikenDinner(ingredients, cooker, chicken, mixer) {
    var = dish.mixer.mix(chicken, ingredients);
    return cooker.bake(dish, 
          cooker.degrees_f(350), 
          cooker.timer('50min'));
}
var cooker = new Cooker(new ConventionalOven());
var dinner = makeChikenDinner(ingredients, cooker);

となる。

テストコード

インジェクションを使うことで複雑度が下がるだけでなくモックでテストが可能となる。

//P38より
describe("test making dinner(after injection is applied)", function(){
    it("making dinner", function(){
        this.addMatchers(
            toBeYammy: function(expected) {
                return this.actual.attr.isCooked && this.actual.attr.isMixed;
            }
        );

        var ingredients = ["Parsley","Salt"];
        var chicken = {};
        var mixer = {
            mix : function(chick, ings) {
                expect(ingredients).toBe(ings);
                expect(chicken).toBe(chick);
                return { attr: { isMixed: true } }; 
            }
        };

        var MockCooker = function(){ }
        MockCooker.prototype = {
            bake: function(food, deg, timer) {
                expect(food.attr.isMixed).toBeTruthy();
                food.attr.isCooked = true;
                return food;
            },
            degree_f: function(temp) { expect(temp).toEqual(350); },
            timer: function(tiime) {
                expect(time).toEqual('50 min');
            }
        };

        var cooker = new MockCooker();
        var dinner = makeChickenDinner(ingredients, cooker, chicken, mixer);

        expect(dinner).toBeYammy();
    });
});

JavaScriptのテストに精通する著者のコードだけあって非常に綺麗なテストコード。