意識高いiOSアプリのつくり方
なんとなくRSpec使ってるやつダサい
先日、第5回若手Webエンジニア交流会でLTしてきました。
ビール飲んでピザつまみながらLTきいたりLTしたりするのカジュアルでいいですね。
というわけで、スライド公開します。
Testing Frameworkについてで、
「なんとなく、とりあえず、RSpec」みたいなのはダサいよねというお話
スライドの原稿
テストとかの話 矢口裕也 (@yayugu) 「みんなー テスト書いてる?」 みたいなの飽きたよね 今日は TDDとBDDとAgileとかの開発スタイル と テスト哲学の話を しません Testing Framework の話をします Testing Framework 大きく分けて2つ xUnit xSpec xUnit class TestInteger < UnitTest def test_add assert_equal(2, 1 + 1) end end xSpec describe Integer context 'when add 2 numbers' do it 'should return sum' do 2.should eq(1 + 1) expect(2).to eq(1 + 1) end end end ここで質問 どっちが好き? xUnit xSpec ほうほう なんで? 自分がなぜ そのテストフレームワークを 好きか 答えられる? みんなに言いたい 自分が使う ツールのことを もっと良く知ろう フレームワークは よく考えて ちゃんと選ぼう 自分の話 さいきんRubyだと minitest使ってる iOSだと SenTestingKit 何を考えて これらを選んだのか xUnit xSpec この2つを比較 ※思想とかは考えない 比較方法 機能でそれぞれ2つの パーツに分類してから比較 xUnit - test class class TestHoge, def test_hoge - assertion assert_equalとか xSpec - behavior describe, context, it - expectation should, expect (1)機能の違い (2)表記の違い (3)実装の違い (1)機能の違い assertionとexpectation →同じもの test classとbehavior →振る舞いの階層化ができるか (2)表記の違い assertionと expectationに ついてのみ比較 参考フレームワーク: Ruby - minitest - RSpec Obj-C - SenTestingKit - Kiwi Ruby minitest assert_equal(2, 1 + 1) RSpec (should) (1 + 1).should eq(2) RSpec (expect) expect(1 + 1).to eq(2) assert_equal(2, 1 + 1) (1 + 1).should eq(2) expect(1 + 1).to eq(2) Obj-C SenTestingKit STAssertEquals(2, 1 + 1); Kiwi (should) [[foo should] equal:bar]; Kiwi (expect) [[theValue(1 + 1) should] equal:theValue(2)]; STAssertEquals(2, 1 + 1) [[foo should] equal:bar]; [[theValue(1 + 1) should] equal:theValue(2)]; 表記の簡潔さ assert > should > expect (3)実装の違い assert の実装 global or test classのインスタンスに assert関数を定義 class UnitTest def assert_equal(expect, actual) if (expect == actual) puts '.' else puts('F') end end end expectation の実装 2スタイルある - should - expect should の実装 RSpec BasicObject.module_eval do def should(...) ... end end BasicObjectに shouldを追加!!! voodoo感ある Kiwi @interface NSObject (...) - (void)should; ... @end NSObjectに カテゴリとして shouldを追加!!! voodoo感ある expect の実装 RSpec ::RSpec::Matchers.module_eval do def should(...) ... end end BasicObjectオブジェクトを 汚染しないため shouldよりmagic少ない toやeqの実装は 面倒そう Kiwi #define theValue(expr) \ ({ \ ... [KWValue valueWithBytes:... }) Kiwiも 同様 実装の健全さ assert > expect > should まとめ -test class, behaviorの機能差 階層化できるか -assert, expectの機能差 なし -表記の簡潔さ assert > should > expect -実装の健全さ assert > expect > should 使うべき Testing Framework A. 階層化が不要: test class + assertion → xUnit B. 必要: 階層構造 + assertion → minitest Test::More subtest など を使うべき サンプルコード describe Integer context 'when add 2 numbers' do it 'should return sum' do assert_equal(2, 1 + 1) end end end ここに書いたのは あくまで俺の考え 「なんとなく」 ではなくちゃんと考えて Testing Framework を選んで使おう Happy testing!
技術系イベントの定員について
あっという間に埋まってしまうことが多いですが、補欠でもとりあえず申し込んでおきましょう
- 意外と参加できなくなる人は多いので順番が回ってくる
- 補欠が多いと開催者が定員を増やせるようがんばってくれる
余談1
イベントによっては盛り上がりを演出するため、最初は定員を少なめにしておいて補欠が出たあたりから枠を拡大していく手法をとることがあります。
余談2
イベント運営者は参加申請した人の50%くらいしかこないケースを覚悟しておきましょう。リマインドメールを3回くらい送ってもそれしか来ないと泣きなくなりますが世の中そんなもん
PHPすげえ
もともとRubyistだったんですが1年程仕事でPHPを書いて意外とイケてることに気づいたので。
- 運用が楽
- 速い
- 開発環境がわりと快適
- ドキュメントが高品質
運用が楽
Apacheにmod_phpを入れてindex.phpを叩けばそれだけでスタートできます。
1リクエスト1プロセスで処理され、1リクエストを返し終わったプロセスは破棄されます。
Rubyだと基本的にプロセスを複数リクエストに使いまわすため、プロセスが肥大化したり、リクエストの処理中に大きなGCが走って遅くなったり、それを防ぐために一定回数で再起動したり、プロセスを監視するためのプロセスが必要になったりと複雑になりがちですが、そういった手間はありません。
デプロイしたあとは非常に安定して動き続けてくれます。
開発環境がわりと快適
ソースコードの変更も即座に反映されます。これにより「Webrickおせー」「Shotgunおせー」「touch tmp/restart.txtだりー」「guardの更新検知がたまに若干遅れるんだけど」といったことから開放されます。
ドキュメントが高品質
php.netにほぼすべての関数、クラスについて丁寧で最新の変更に追随している英語&日本語ドキュメントが用意されています。
罠っぽい仕様やその回避策もひと通り網羅されていて、困ったらマニュアル読むだけでなんとかなります。
イケてないところは多すぎるので略
結論
つくったあと放っとけば動き続けてくれて、保守を他の人にぶん投げられるサービスがつくりたければPHPを使え。
PhoneGap/CordovaがiOSのネイティブ機能を使うしくみ
さいきんWebとiOS/Andoroid Native Appの融合がすすんでいますね!
ブラウザコンポーネント(WebViewという)のJSからiOSのネイティブ機能を呼び出すスタイルも流行っています. PhoneGapとかFacebookアプリとかね!
自分は勝手にJS-Native Bridgeと呼んでいるのですが,
あれってどうやってるのって話
Google先生で「uiwebview native」でぐぐると
UIWebViewでWebとネイティブを相互連携させる方法について
iPhoneのネイティブ機能をWebViewから呼び出す方法
こういう記事が出てきます
ようするに
Obj-C → JS | [webView stringByEvaluatingJavaScriptFromString:@"JSCode"] |
JS → Obj-C | webView:shouldStartLoadWithRequest:navigationType: を発火させる |
だが甘い
↑の記事ではあくまで原理の解説にしかなっていません.
現実のコードで使うにはもっと改良が必要なのです.
具体的には:
- iframe fire
- big size data support
- queuing
iframe fire
で,イベントを発火させる方法として↑ではaタグをクリックさせたり,location.hrefをいじっていますが,そんなことしなくてもiframeを作ってやると発火します.こっちのほうが影響範囲が少なくクリーンです.
https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js
gapBridge = document.createElement("iframe"); gapBridge.setAttribute("style", "display:none;"); gapBridge.setAttribute("height","0px"); gapBridge.setAttribute("width","0px"); gapBridge.setAttribute("frameborder","0"); document.documentElement.appendChild(gapBridge); gapBridge.src = "gap://ready";
これでイベントが発生してくれます
big size data support
URLでもある程度の情報は伝えられるのですが,あまり大きなものはダメです.
特に画像とか.そのためデータはURIに付加するのではなくJSON化してstringByEvaluatingJavaScriptFromString経由で取得するようにします.
↓の擬似コードみたいなイメージ
js
cordova.command = JSON.stringify(commandArgs); createIframe();
Obj-C
- (BOOL) webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { NSURL* url = [request URL]; if ([[url scheme] isEqualToString:@"gap"]) { NSString *json = [webView stringByEvaluatingJavaScriptFromString:@"cordova.command"]; NSDictionary *params = [NSJSONSerialization dataWithJSONObject:json options:nil error:nil] [self dosomething] // なんかする return NO; } }
queuing
webView:shouldStartLoadWithRequest:はある一定時間内に複数回発生させようとしても1回しか発生しません.おそらくJSのrun loopに戻れるまでイベントが発生させられないことなどが関係しているような.
cordova.command = JSON.stringify(commandArgs) // 無視される
createIframe();
cordova.command = JSON.stringify(commandArgs2)
createIframe();
そのためcommandのqueuingが必要となります.
cordovaではcommandをcommandQueueという配列に入れ,commandQueueFlushingというフラグで管理することで対処しています.
https://github.com/apache/incubator-cordova-js/blob/master/lib/ios/exec.js
if (cordova.commandQueue.length == 1 && !cordova.commandQueueFlushing) { if (!gapBridge) { createGapBridge(); } gapBridge.src = "gap://ready"; }
https://github.com/apache/incubator-cordova-ios/blob/master/CordovaLib/Classes/CDVViewController.m
- (void) flushCommandQueue { [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = true"]; // Keep executing the command queue until no commands get executed. // This ensures that commands that are queued while executing other // commands are executed as well. int numExecutedCommands = 0; do { numExecutedCommands = [self executeQueuedCommands]; } while (numExecutedCommands != 0); [self.webView stringByEvaluatingJavaScriptFromString:@"cordova.commandQueueFlushing = false"]; }
ということで
- iframe fire
- big size data support
- queuing
の3つを実装することでより優れたJS-Native Bridgeがつくれるでしょう!
UIWebViewのデフォルトUserAgent
WebページのUAを解析しようとしたとき、
「iOSでMobile SafariとUIWebView(それ以外のアプリ内ブラウザ)の違いって検出できないのかなー」
と思ったことないですか
UIWebViewのUAは変更可能なのであまりアテにならないのですが、
何も指定しないときは、Mobile Safariとよく似ていて、少しだけ異なるUAになります
例
Mobile Safari: Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A334 Safari/7534.48.3 UIWebView: Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A334
Mobile Safari: Mozilla/5.0 (iPad; U; CPU OS 4_3_5 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5 UIWebView: Mozilla/5.0 (iPad; U; CPU OS 4_3_5 like Mac OS X; ja-jp) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1
末尾のSafari/xxxx.xx.xがなくなるみたいです。
WebKitを使ったブラウザコンポーネントでSafariじゃねーよってことみたいですね