Go 言語でつくったボットを GitHub -> CircleCI -> Bluemix と自動デプロイできるようにした
Golang でのボット作成では、1 個目は自 PC で動かすことしか考えていませんでしたが、2 個目はきちんとどこかにデプロイすることを目標に作成していましたので、ついでに自動でやれるようにしました。
Heroku では 24 時間稼働のボット用途には不向きだと思われたので、IBM Bluemix にデプロイ先に選びました。Bluemix もデフォルトで Golang をサポートしていますが、ビルトインの buildpack だと Godeps のバージョンをきちんと読んでくれなかったり (これは気のせいかもしれない) したのでgithub にあるものを利用したりして色々とあれでしたが、うまく行きました。
circle.yml の様子。
machine: environment: REPO_ROOT: "${HOME}/.go_workspace/src/YOUR_DOMAIN/YOUR_PACKAGE_DIR" dependencies: pre: - mkdir -p ${REPO_ROOT} - cp -rf ./* ${REPO_ROOT} - go get github.com/tools/godep - curl -v -L -o cf-cli_amd64.deb 'https://cli.run.pivotal.io/stable?release=debian64&source=github' - sudo dpkg -i cf-cli_amd64.deb - cf -v test: pre: - go vet ./... override: - cd ${REPO_ROOT} && godep go test ./... post: - cf api https://api.au-syd.bluemix.net - cf login -u $BLUEMIX_USER -p $BLUEMIX_PASSWORD - cf target -o $BLUEMIX_ORG -s $BLUEMIX_SPACE - cf a deployment: production: branch: master commands: - cf push APP_NAME -b https://github.com/cloudfoundry/go-buildpack.git
とにかくプッシュ、テスト、デプロイまでをやれるようにとツギハギしたので、まだ無駄があると思いますが、とりあえずこれでアップできます。
つまずきポイントとしては、
${HOME}/.go_workspace/src/
へリポジトリを展開しないと、go get -t -d -v ./...
でリモートからパッケージ fetch しようとして死んだ (YOUR_DOMAIN を github ではないところにしているため、みつからない)cf api
でエリア指定を間違えた (だいたいの例はアメリカ南部になっているが、シドニーを使っている)cf
のインターフェースが gem install cf のものとちょっとちがう
などがありました。
基本的にはテストが通ったら cf で Bluemix にデプロイするというだけなので、すでに cf でのデプロイを済ませている人は、設定の際にあらためて考えることはない感じですね。
Slack で動いてるボットの処理が長い場合、フィードバックとしてインジケーターを出すということをやった
いま golang の練習用に作成しているボットには、URL をわたすと、そのサイトのキャプチャを撮影する機能があります。諸事情からボットのいるマシンとは別の場所、Heroku に設置していますが、起動が遅かったり、キャプチャ自体が遅かったりするので、ちゃんとやっていってるのかわかりません。そこで、ローディングインジケーターを出すことにしました。
できあがり
「こはる」とは社の Slack にいるボットですが、対抗して golang でつくっているのが「ごはる」です。かわいいですね。
アイコンの元ネタには以下を使わせてもらっております。
インジケーター
まずインジケーターを作成しました。実際はベタッと書いてから切りだしましたが、とにかくまずインジケーターを用意します。できあがりを想像してワクワクするのが原動力です。
type CaptureLoadingIndicator struct { indicators []string length int step int } func (c *CaptureLoadingIndicator) initialize(indicators []string) { c.indicators = indicators c.length = len(c.indicators) c.step = c.length - 1 } func (c *CaptureLoadingIndicator) next() string { c.step = ((c.step + 1) % c.length) return c.indicators[c.step] } func createCaptureLoadingIndicator() *CaptureLoadingIndicator { c := (&CaptureLoadingIndicator{}) c.initialize([]string{"▖", "▘", "▝", "▗"}) return c }
goroutine
キャプチャ機能では、キャプチャされた画像は Heroku からダイレクトに Slack に投稿されますが、キャプチャできたかどうかは Response で受けとれます。よって、Heroku へのリクエスト開始をインジケーターの開始、Response が帰ってくれば終了するとします。
細かい処理ははぶいて、goroutine 部です。http.PostForm
は同期的に処理されますから、基本的に以降の処理をブロックします。これを goroutine に追いだして本プロセスとは並行に行いつつ、終わるまでの間はインジケーターを表示します。
Big Sky :: golang の channel を使ったテクニックあれこれを参考に、処理が終わるまで待つ処理を書きます。
// インジケーターを表示するメッセージの TS を取得する var ind slack.ChatPostMessagesResponse slack.ChatPostMessage.Strike(&ind, string(m.Channel), "starting...") // 以下で goroutine ポーリングする q := make(chan CaptureServerResult, 2) go func() { // Heroku へリクエスト開始 resp, err := http.PostForm(captureServer, values) // リクエスト終わり q <- CaptureServerResult{resp, err} }() indicator := createCaptureLoadingIndicator() for { if len(q) > 0 { break } // 上記で取得した TS をターゲットとして、chat.update をかける slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, fmt.Sprintf("loading `%v`", indicator.next())) // update 間隔は加減しましょう time.Sleep(200 * time.Millisecond) } slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, "loaded") resp := <- q // response をもとにした処理が続く
<- q
は処理をブロックしてしまうので、なんの動きもない重い処理、待ちの時に他の処理をさせておきたい場合はチェッカーとしては使えないんですね。
しかし、Channel をうまく利用することにより、簡単に実現することが出来ました (先達の知識に感謝)。こういうのは大体、開始や interval 処理ではなく、終了がめんどくさかったりするのですが、それが特に問題にならずよかったです。
あと func(){}()
ができるのが色々便利ですね。
Ruby 外から Web アクセスする何か (Capybara とか、cli とか) を RSpec でテストするときのアクセス先をモックする
mmmpa.mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmpa.net
のテストをこの方法で行いました。
本編
たとえば、Capybara
は各種ブラウザを介するアクセスのため、webmock
が効かず、別の gem が必要なのは有名です。 https://github.com/oesmith/puffing-billy
Capybara
でテストする際はこれでいいのですが、Capybara
をテストする場合には不便かもしれませんね。
また、バッククォートや Open3
を用いて呼びだすような cli、たとえば siege
などのターゲットをモックすることはできません。cli におんぶにだっこの gem を開発するときのテストに、かなり不便ですね。
RSpec
開始時にサーバーをたてる
望んだ結果を返すアクセス先があればいいので、テスト中に起動する仮想サーバーをたてます。
public
ディレクトリに適当な HTML を用意した上で、
RSpec.configure do |config| require 'webrick' config.before(:suite) do port = ENV['MOCK_PORT'] || 3000 host = ENV['MOCK_HOST'] || '127.0.0.1' started = false Thread.start do WEBrick::HTTPServer.new( DocumentRoot: File.expand_path('./public/', __dir__), BindAddress: host, Port: port, AccessLog: [], StartCallback: ->{ started = true } Logger: WEBrick::Log::new("/dev/null", 7) ).tap { |server| Signal.trap(:INT) { server.shutdown } server.start } end while !started sleep 0.5 end end end
Thread
はあたらしいものを用意しないと、sever.start
の時点で RSpec
のプロセスが停滞してしまいますので注意しましょう。
また、別 Thread
になるので server.start
が完了するしないにかかわらずテストに突入、テストが落ちるということがあるので、StartCallback
を使って、きちんとサーバーがスタートしたことを確認してからはじめましょう。
単純なアクセス以外
さらに、リダイレクトなど、単純な HTML アクセス以外の処理が必要ならば、インスタンス server
に mount
すると処理を追加できます。
# たとえばリダイレクト # Rails を模するなら、301(Moved) ではなく 302(Found) server.mount_proc('/redirect') do |req, res| res.set_redirect(WEBrick::HTTPStatus::Found, '/redirected.html') end
できあがり
これで、以下のようなテストが、自由に行えるようになりました。
expect(`siege -t 10s http://127.0.0.1:3000/foo.html`).to be_truthy
例は siege
自体のテストのようになってしまっていますが、実際は取得した結果をあれこれしたものをテストしました。
雑ではありますが、とても楽に、多くのアクセス先を用意できるようになりました。
siege をラップして多少細かい情報をまとめる gem 書いた。
Usage
たとえば Rails 内でこうやる。
re = SiegeSiege.run( time: 20, concurrent: 4, user_agent: false, urls: [ "http://localhost:3002#{students_path}", "http://localhost:3002#{students_path} POST name=abc", SiegeSiege::URL.new("http://localhost:3002#{students_path}", :post, {name: 'abc'}), ] + Student.ids.shuffle[0..2].map { |id| "http://localhost:3002#{student_path(id)}" } )
するとこういうのが得られる。
普通に使ってもコンソールに出てきてたやつ
re.total_result => {:command=>"siege -v -c 4 -t 20s -r 1 -R /tmp/20161030-21093-1lo07ca -f /tmp/20161030-21093-13f1kj6", :defaulting_to_time_based_testing=>{:value=>20.0, :unit=>"seconds"}, :transactions=>{:value=>96.0, :unit=>"hits"}, :availability=>{:value=>75.0, :unit=>"%"}, :elapsed_time=>{:value=>19.47, :unit=>"secs"}, :data_transferred=>{:value=>2.03, :unit=>"MB"}, :response_time=>{:value=>0.8, :unit=>"secs"}, :transaction_rate=>{:value=>4.93, :unit=>"trans/sec"}, :throughput=>{:value=>0.1, :unit=>"MB/sec"}, :concurrency=>{:value=>3.93, :unit=>""}, :successful_transactions=>{:value=>96.0, :unit=>""}, :failed_transactions=>{:value=>32.0, :unit=>""}, :longest_transaction=>{:value=>10.65, :unit=>""}, :shortest_transaction=>{:value=>0.01, :unit=>""}}
アクセスに使った URL 毎に平均レスポンス時間とか
re.average_log => [#<struct SiegeSiege::AverageLog id=6, url="http://localhost:3002/students/10763", count=17, secs=0.691, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>, #<struct SiegeSiege::AverageLog id=0, url="http://localhost:3002/students", count=16, secs=1.276, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::AverageLog id=5, url="http://localhost:3002/students/39284", count=16, secs=0.767, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>, #<struct SiegeSiege::AverageLog id=7, url="http://localhost:3002/students/94576", count=17, secs=0.108, siege_url=#<SiegeSiege::URL:0x007fbee6efe7a8 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/94576">>, #<struct SiegeSiege::AverageLog id=3, url="http://localhost:3002/students", count=15, secs=1.367, siege_url=#<SiegeSiege::URL:0x007fbee6cd9518 @http_method=:post, @parameter={:name=>"abc"}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::AverageLog id=1, url="http://localhost:3002/students", count=15, secs=0.651, siege_url=#<SiegeSiege::URL:0x007fbee6efea50 @http_method=:post, @parameter="name=abc", @url="http://localhost:3002/students">>]
標準出力をまとめたやつ
re.raw_log => [#<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.09, bytes=633, url="http://localhost:3002/students/10763", id=6, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe870 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/10763">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.29, bytes=45579, url="http://localhost:3002/students", id=0, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efeb18 @http_method=:get, @parameter={}, @url="http://localhost:3002/students">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=10.24, bytes=633, url="http://localhost:3002/students/39284", id=5, date=Sun, 30 Oct 2016 13:10:31 +0000, siege_url=#<SiegeSiege::URL:0x007fbee6efe938 @http_method=:get, @parameter={}, @url="http://localhost:3002/students/39284">>, #<struct SiegeSiege::LineLog protocol="HTTP/1.1", status="200", secs=0.25, bytes=633, url="http://localhost:3002/students/94576", id=7, date=Sun, 30 Oct 2016 13:10:41 +0000, siege_url=# # 略
所感
カジュアルにテストできて便利ですね、みたいなツールで、詳細な情報を取ろうとするのはやめといた方がいいと思いました。(適切なやつをさがそう)
Capybara で Chromedriver をつかってモバイルモードでテストする時の Capybara.register_driver とか
だいたいのサイトは User Agent で切り替えてると思うので、わざわざモバイルモードでテストが必要なのかしらとか思わないでもない (ダブルタップとか、スワイプをテストする?)、が一応メモ。
Capybara に Driver として登録
# chromedriver configuration # chrome の起動オプションが使える http://peter.sh/experiments/chromium-command-line-switches/ # デフォルトではサブのディスプレイに表示してしまうので、ずらしている default_args = %w( --window-position=2560,0 ) # ブラウザの外枠 (スクリーンショット撮って計測しよう) chrome_frame_offset = { w: 10, h: 86 } # プリセットの商品のセッティングを使う場合。 # DevTools で選べるモバイルから必要なものをピックアップ。(リストデータくれよ) [ {name: 'Apple iPhone 6 Plus', w: 414, h: 736}, # :apple_iphone_6_plus {name: 'Google Nexus 7', w: 600, h: 960}, # :google_nexus_7 ].map { |configure| configure[:w] += chrome_frame_offset[:w] configure[:h] += chrome_frame_offset[:h] configure }.each do |configure| Capybara.register_driver configure[:name].gsub(' ', '_').downcase.to_sym do |app| Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { args: [ "--window-size=#{configure[:w]},#{configure[:h]}" ] + default_args, mobileEmulation: {deviceName: configure[:name]} } ) ) end end # 独自のセッティングでモバイルモードを使う場合 # これはイッパツで全画面をスクリーンショットしたかった時の設定 Capybara.register_driver :chrome_dummy_mobile do |app| w = 320 h = 1600 Capybara::Selenium::Driver.new( app, browser: :chrome, desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { args: [ "--window-size=#{w + chrome_frame_offset[:w]},#{h + chrome_frame_offset[:h]}" ] + default_args, mobileEmulation: { deviceMetrics: { width: w, height: h, pixelRatio: 2.0 }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1' } } ) ) end
選んで使う
# 登録しておいた driver から選ぶ Capybara.configure do |config| config.run_server = false config.default_driver = :apple_iphone_6_plus config.app_host = 'http://googole.com/' end
RSpec とか Capybara の Example 内で使いたいメソッドをクラス単位でまとめて使いやすくする gem を書いた。
これ
先週の gem は 80 行ぐらいだったんですが、今回は 40 行です。
なぜ
入社してからこっち、ずっと Vue.js で SPA 制作という業務に従事していましたが、UI 系の宿命で (一部しか) テストがない。
そんななかで、わりと分岐の多い (難しくはない) 実装をすることになって、これは手作業では (変更の都度に) 確認しきれんなということで Capybara に出張ってもらっていました。テスト維持コスト問題というものがあるので、業務リポジトリには入っていない、ローカルでのみ存在する Capybara です。かわいいよ。
そんななかで find
や all
を直書きしていると、途中での変更抜きにしても、見栄えがとてもつらいことになります。
経路によっては他の Example にも出てくるので、メソッド化をするわけですが、しかし、ページごとにちがう submit_button
があったりするので、 registration_page_submit_button
や shopping_cart_submit_button
など、なんとかして名前をかえていく必要がありまして、これが読みづらい。
というわけで Registration.submit_button
や ShoppingCart.submit_button
というように簡単にわけられるようにしました。
例
これが
def registration_page_submit_button find('button', text: 'Register Now!') end def shopping_cart_submit_button find('button', text: 'Buy Now!') end feature 'register then buy' do scenario do visit '/resistration' # input data to some form registration_page_submit_button.click visit '/cart' # input data to some form shopping_cart_submit_button.click end end
こうなる
class RegistrationPage < RSpecMethodGrouping::Base def submit_button find('button', text: 'Register Now!') end end
class ShoppingCart < RSpecMethodGrouping::Base def submit_button find('button', text: 'Buy Now!') end end
feature 'register then buy' do scenario do visit '/resistration' # input data to some form RegistrationPage.submit_button.click visit '/cart' # input data to some form ShoppingCart.submit_button.click end end
よみやすい!
かどうかは人によるかと思いますが、メソッドの管理がしやすくなったのでヨシとします。
この量ではわかりにくいかもしれませんが、他画面遷移 + 要素めちゃ多いとかになると、結構効いてきます。
テスト内の隠蔽どうなの問題
テストに使うデータを過度に隠蔽すると、テストの意図まで隠蔽されて、まるでわからないという問題があります。
しかし、これは、テストの主題ではなく、ページ内の要素を特定しておく (変更時はイッパツで対応できる) のが主目的なので、特に問題ないと思います。
もりもり Capybara でスクリーンショットを撮っていきましょう。
最近 SQL にはまっているので、ついでに Rails というか ActiveRecord で発行された SQL query をカウントする gem を書いた。
Installation
gem 'a_r_q_logger'
せつめい
ActiveRecord::Base
サブクラスのインスタンス生成をできるだけ抑えれば、それだけ処理時間が抑えられるのは自明とされています。
そこで、今は、できるだけ少ない queries に抑えることを目的として SQL の練習をしていますので、すぐわかるようにしました。
その練習を進めていく上で、その 1 query の処理時間が気になっており、SQL のみの処理時間もとれるようにしましたというか、出発点はこっちです。
pry(main)> ARQLogger.log { 3.times { TestModel.has_children(10).map(&:children_count) } } => #<struct ARQLogger::Result count=3, msec=1508.4>
from
句で前もって as
しておくのと、CASE
の WHEN
ごとに (複数の) 集約関数を書くのではどっちが速いのか気になって、こういうことをしていたのです。 (おそらくオプティマイザがいい感じにしてくれるおかげで、CASE
に同じ式を何回も書くほうが速かった)
テストでも使える
query を抑えようと書いたかっこいい生 SQLが、善意の第三者により書きかえられるということはありがちなことですが、amount of queries を抑えようと書いた生 SQL が、その善意により too many queries になっては目もあてられません。
query 発行量はコントロールできる要素ですから、これはテストで縛っておくことができます。
たとえば以下は N + 1 の解決法でよく出てくる includes
の付与テストです。
it do expect(ARQLogger.log { TestModel.includes(:test_child_models).all.each { |m| m.test_child_models.map(&:name) } }.count).to eq(2) end
it do expect(ARQLogger.log { TestModel.all.each { |m| m.test_child_models.map(&:name) } }.count).to eq(11) end
久しぶりに gem を書くのが目的だった
どこかにありそうだなと思って軽く調べて、そんな感じの gem もありましたが軽く無視って書きました。
たのしかたです。