Firebase Remote Config for iOS
この記事は Firebase Advent Calendar 2016 - Qiita の4日目の記事です。
最近は Golang を用いたサーバサイド開発を専らの仕事としていますが、約2年前はガッツリと iOS アプリ開発を行っていました。そのときから「アプリの振る舞いや見た目、A/Bテストをサーバサイドでどうにかコントロールしたい」と思っていたので、今回はそれを満たすことのできる Firebase Remote Config について紹介しようと思います。
Firebase Remote Config とは
Firebase をご存知の方は多いと思いますが、Remote Config は Database, Storage と同じ立ち位置となる Firebase の一つのサービスです。この Remote Config が提供する機能は Firebase 上である値を定義し、その値を iOS、Android や Web のアプリ(主にクライアント)で取得し、使用することができます。ここでいう使用方法は実装者次第になりますが、設定した値に対してセグメントさせたユーザにのみ配信してA/Bテストに役立てたりします。 これによって、アプリ開発者が「開発〜検証〜リリース」という作業を行わずとも、アプリをアップデートすることができるようになります。 また、Firebase Analytics とも連携することができます。
開発
今回は iOS アプリでの開発で説明します。
0. Firebase on iOS
「Add Firebase to your iOS Project」を読んでプロジェクトで Firebase を使用できる状態にします。
1. Xcode Project 作成
Xcode プロジェクトに Podfile ファイルがなければ pod init
で作成し、下記のように Podfile に pod 'Firebase/RemoteConfig'
を追加します。
target 'YourTargetName' do use_frameworks! pod 'Firebase/RemoteConfig' end
追加したら pod install
で RemoteConfig をインストールし、ワークスペース(.xcworkspaceファイル)を開いて開発を行います。
2. 実装
Firebase シングルトンの初期化
application:didFinishLaunchingWithOptions
で Firebase を初期化します。
import UIKit import Firebase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { FIRApp.configure() return true } }
Remote Config から値の取得
FIRRemtoeConfig.remoteConfig()
でシングルトンインスタンスを呼び出し使用します。例えば、ボタンのタイトルを変更するためには下記のように実装します。
let appDefaults: [String: NSObject] = [ "reaction_button_text" : "Like", ] FIRRemoteConfig.remoteConfig().setDefaults(appDefaults) let fetchDuration: TimeInterval = 0 FIRRemoteConfig.remoteConfig().fetch(withExpirationDuration: fetchDuration) { [weak self] (status, error) in guard error == nil else { return } FIRRemoteConfig.remoteConfig().activateFetched() let title = FIRRemoteConfig.remoteConfig()["reaction_button_text"].stringValue ?? "Like" self.button.setTitle(title, for: UIControlState.normal) }
こうすることによって、 "reaction_button_text" を Remote Config の方に値を設定することによって、Firebase 上でボタンのタイトルを変更することが可能になります。仮に、Remote Config に値を設定しないか、通信状況が悪い場合などはデフォルトとして "Like" が表示されるようになります。
3. Remote Config に値を追加
Firebase のダッシュボードからプロジェクトを指定して、Remote Config に値を設定します。プロジェクトを作成していない場合は新規で作成するか、GCP のプロジェクトをインポートしましょう。
Remote Config は Firebase ダッシュボードの左パネルから選択ができます。
"ADD PARAMETER" で値を追加します。
値が追加完了したら "PUBLISH CHANGES" を押して、値を配信します。
これで "reaction_button_text" の値に "Favorite" が設定され、配信されるようになります。
おわりに
軽めに Firebase Remote Config の解説をしましたが、次はもう少し振る舞いを変更させる記事を書こうかなと思います。
golang で始める Slack bot 開発
この記事は Slack Advent Calendar 2016 - Qiita の3日目の記事です。
昨日は Kinoppyd さんの「今そこにあるSlack」でした。
さて、今回、この記事では golang で Slack bot を実装する方法を紹介しようと思います。
世に蔓延る Slack bot
これから bot を世に放とうとしている人は、是非、1日目と2日目の記事を読み、事前知識を頭に叩き込んでおくと良いと思います。
基本的に、自身で作成した bot はもちろん好きになると思いますが、人によっては理解不能な bot や、意味不明な場面で反応したりと、「邪魔だな」と思われてしまうことがあります。そのため、bot を開発する人は「謙虚・尊敬・信頼」(Team Geekより)を持った上で、世に放ちましょう。
golang で bot を開発
Golang の開発環境は整っているとします。
bot の新規作成
Slack の bot の仕様については下記のページに色々と書かれています。
アプリケーションに統合された bot の開発も可能ですが、今回は簡単に実装するために Slack Bots からトークンを取得します。
Slack Bots の新規作成はこちらから作成ができ、下記のようなページで新規作成となります。
作成が完了すると、下記の画像の設定ページに遷移するので、ここにあるトークンを使用して bot を稼働させます。
実装
Go で開発をすすめます。 今回、簡単に Slack API 連携するために github.com/nlopes/slack を先に go get しておきます。
$ go get -u github.com/nlopes/slack
コード全体
package main import ( "log" "os" "github.com/nlopes/slack" ) func run(api *slack.Client) int { rtm := api.NewRTM() go rtm.ManageConnection() for { select { case msg := <-rtm.IncomingEvents: switch ev := msg.Data.(type) { case *slack.HelloEvent: log.Print("Hello Event") case *slack.MessageEvent: log.Printf("Message: %v\n", ev) rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel)) case *slack.InvalidAuthEvent: log.Print("Invalid credentials") return 1 } } } } func main() { api := slack.New("[YOUR-API-TOKEN]") os.Exit(run(api)) }
順を追って解説します。
slack.Client の生成
func main() { api := slack.New("[YOUR-API-TOKEN]") os.Exit(run(api)) }
slack.New
関数に先ほど取得したAPIトークンを渡して、 slack.Client
を生成します。これは Slack API のクライアントになるので、bot 以外でももちろん API を叩くことが可能になります。
api := slack.New("[YOUR-API-TOKEN]")
Real Time Messaging API
func run(api *slack.Client) int { rtm := api.NewRTM() go rtm.ManageConnection() for { select { case msg := <-rtm.IncomingEvents: switch ev := msg.Data.(type) { case *slack.HelloEvent: log.Print("Hello Event") case *slack.MessageEvent: log.Printf("Message: %v\n", ev) rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel)) case *slack.InvalidAuthEvent: log.Print("Invalid credentials") return 1 } } } }
Real Time Messaging を使用して、 Websocket から情報を取得します。
rtm := api.NewRTM()
go rtm.ManageConnection()
コネクションを張り次第、ひたすらループして情報を取得します。情報はチャネルの IncomingEvents
に enqueue されるので、それを dequeue して型によって振る舞いを変えます。
for { select { case msg := <-rtm.IncomingEvents: switch ev := msg.Data.(type) { case *slack.HelloEvent: log.Print("Hello Event") case *slack.MessageEvent: log.Printf("Message: %v\n", ev) rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel)) } } }
今回の例では誰かがこの bot が招待されているチャネルに投稿すると、slack.MessageEvent
が発火して、 "Hello world" をそのチャネルに投稿しています。
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel))
実行
上に記述したコードを main.go という名前で実装していると仮定すると。
$ cd /path/to/slack-bot $ go run main.go 2016/12/03 06:32:53 Hello Event 2016/12/03 06:32:53 Message: &{{message XX XX Hello world 1480746760.000010 false [] [] <nil> false <nil> [] <nil> false <nil> 1 []} <nil>}
のように出力されれば動いています。
デーモン化
bot が死んでいたら悲しいので、デーモン化をちゃちゃっとしちゃいます。
まず、作成した bot をコンパイルしてバイナリにしておきます。
$ go build -o /path/to/bot main.go
次に、デーモン化ですが、今回は supervisor での例を載せておきます。
[program:bot] command=/path/to/bot user=someone autostart=true autorestart=true stderr_logfile=/var/log/bot.err.log stdout_logfile=/var/log/bot.out.log
上記を conf に用意して、 supervisord を再起動させましょう。これでデーモン化された bot の完成です。
おわりに
慣れている言語でちゃちゃっと bot は実装したいと思うので、各言語での bot 実装法はもっと出回っていいのではと思っています。最近、 Crystal にハマっているので、今度機会があれば Crystal バージョンを書こうと思います。
golang の httptest パッケージを使う
この記事は Go Advent Calendar 2016 - Qiita の2日目の記事です。
Golang については書きたいことがたくさんあるので、Go Advent Calendar 2016 その4が出てきても良いのではと思っている次第です。(空いていればいつでも書きます)
さて、今回、この記事では Golang で書かれた Web アプリケーションのリクエストのユニットテストについて解説しようと思います。
1. Testing HTTP Handler
検証のために、ただ単に "pong" を返却する pingHandler と、URLクエリから値を取得してそのまま返却する echoHandler の2つを定義します。
ー pingHandler
// pingHandler returns just "pong" string. func pingHandler() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } }
ー echoHandler
// echoHandler returns a 'msg' query parameter string. func echoHandler() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(r.URL.Query().Get("msg"))) } }
※ ヘッダーやステータスコードの設定は省略しています。
Tests with httptest.Server
本テストコードは こちら です。
実装したHTTPハンドラのテストを実行するために、httptest (net/http/httptest) パッケージを使用してリクエストをサービングします。
ー pingHandler テスト
t.Run("pingHandler", func(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(pingHandler())) defer s.Close() res, err := http.Get(s.URL) assert.NoError(t, err) defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, "pong", string(body)) })
ー echoHandler テスト
t.Run("echoHandler", func(t *testing.T) { candidates := []struct { query string expected string }{ {"", ""}, {"foo=bar", ""}, {"msg=foo", "foo"}, } for _, c := range candidates { c := c t.Run(c.query, func(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(echoHandler())) defer s.Close() res, err := http.Get(fmt.Sprintf("%v?%v", s.URL, c.query)) assert.NoError(t, err) defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, c.expected, string(body)) }) } })
テストのアサートには github.com/stretchr/testify/assert を使用しており、テストの記述については go1.7 以上の仕様となりますので、 Golang におけるサブテストの並行処理実装について | eureka tech blog を是非一読してみてください。
httptest.NewServer とは
上の例のように、既にHTTPハンドラが定義されている場合、 httptest.Server を利用してリクエストをサービングします。
httptest.NewServer(http.Handler) *httptest.Server
httptest.NewServer を呼んで httptest.Server を初期化します。httptest.Server は httptest.NewServer で初期化を行うと、リクエストをサーブする状態の httptest.Server が返却されるため、使用後に Close を行う必要があります。
また、そもそも初期化時にサービングしている httptest.Server が欲しくない場合は
httptest.NewUnstartedServer(http.Handler) *httptest.Server
にて初期化を行います。
httptest.NewServer の使い方
// http.HandleFuncを満たすハンドラの用意 handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) { // ... }) // 定義したハンドラをサーブする Server s := httptest.NewServer(handler) // s.URL にリクエストURLが設定されている res, err := http.Get(s.URL) // .... // 適切に Close させて終了 s.Close()
httptest.NewServer には http.Handler を満たした変数を渡せば十分です。 http.Handler を渡すこすためには func(w http.ResponseWriter, r *http.Request) のハンドラ関数を http.HandleFunc 型にするだけで満たされます。
handler := http.HandleFunc(func(w http.ResponseWriter, r *http.Request) { ... })
2. Recording Response while Testing
httptest.Server を使用するのは上記で十分ですが、わざわざ検証を行うために httptest.Server を使って検証を行いたくない場合や、何らかの理由で httptest.Server が使用できないが、リクエストをシミュレートしてのユニットテストを行いたい場合は httptest.ResponseRecorder を使用します。
httptest.ResponseRecorder
Responseを記録するための構造体で、httptest (net/http/httptest) にこちらも存在しています。
type ResponseRecorder struct { Code int // the HTTP response code from WriteHeader HeaderMap http.Header // the HTTP response headers Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to Flushed bool // contains filtered or unexported fields }
http.ResponseWriter を満たしているため、HTTPハンドラに渡すことによって結果を書き込むことができます。
Tests with httptest.ResponseRecorder
本テストコードは こちら です。
HTTPハンドラに ResponseRecorder を渡して、その値を検証することになります。
// ResponseRecorder の生成 res := httptest.NewRecorder() // リクエストの生成 req, err := http.NewRequest(http.MethodGet, "http://example.com/?msg=foo", nil)
用意したリクエストとレスポンスに対してHTTPハンドルします。
// ハンドラに ResponseRecorder と Request を渡して実行する
handler := echoHandler()
handler(res, req)
※ echoHandler は Server で使用したものと同一
// ResponseRecorder に記録された値の検証 assert.Equal(t, "text/plain", res.HeaderMap.Get("Content-Type")) assert.Equal(t, 200, res.Code) // Body の検証 body, err := ioutil.ReadAll(res.Body) assert.NoError(t, err) assert.Equal(t, c.expected, string(body))
検証には testify/assert を使用しています。httptest.ResponseRecorder には Response 検証に必要な情報が全て格納されているので、それを利用して検証を行います。
おわりに
net/http/httptest パッケージは軽量ですが、テスト時に強力なパッケージになりますので、是非活用してもらえればと思います。
リソース
Yogibo の Traybo という最高のトレー
膝の上で PC を操作する人に絶対オススメしたい商品。
感想
膝の上に置くことになるので、使い始めは「Traybo が重くてすぐ使わなくなるかも…」と思っていましたが使い心地最高です。 最高な点として、PC が熱を持ってもそれが膝に伝わらないのとトレーの横に iPhone などの小物を置くことができることです。
どこで買うか
日本の Amazon だと倍以上の値段なので、公式のオフラインストアかオンラインストアで購入したほうが良いです。
golang で regexp パッケージを使うときに気をつけること
golang の Regexp は初期化の Compile コストがそこそこありますが、実は正規表現のパターンと対象となる文字列によって、初期化コストが無視できる(パターンと文字列に支配される)くらい遅くなります。よく言われる regexp の初期化コスト問題が無視できると言われても、正直、全く嬉しくないですね。
ただ、コーディング中に regexp パッケージを使わなければいけない場面は出てくるため、なるべくコストの掛からない実装を心がけています。
Compile/MustCompile
初期化コストはなくすため、グローバルに保持するようにします。
定義
var re = regexp.MustCompile("[a-z]{3}") func main() { fmt.Println(re.FindAllString("foobarbazqux", -1)) // => [foo bar baz qux] }
グローバルで定義しておけば起動時に実行されるので、Compile でエラーチェックではなく、MustCompile を使用してそもそも起動できなくしています。
var re = regexp.MustCompile("([a-z]{3}") // => panic: regexp: Compile(`([a-z]{3}`): error parsing regexp: missing closing ): `([a-z]{3}` func main() { fmt.Println(re.FindAllString("foobarbazqux", -1)) }
関数実行で隠蔽してしまうのもアリ。
var finder = func() func(string) []string { re := regexp.MustCompile("[a-z]{3}") return func(str string) []string { return re.FindAllString(str, -1) } }() func main() { fmt.Println(finder("foobarbazqux")) // => [foo bar baz qux] }
Benchmark
regexp を初期化する/しないのベンチマークです。ベンチデータはそこそこ大きいデータを使用しています。
// BenchmarkRegexpMustCompile1-16 // 5000 385760 ns/op 100850 B/op 830 allocs/op func BenchmarkRegexpMustCompile1(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { re := regexp.MustCompile("[a-z]{3}") _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`) } } // BenchmarkRegexpMustCompile2-16 // 5000 367423 ns/op 59961 B/op 805 allocs/op func BenchmarkRegexpMustCompile2(b *testing.B) { re := regexp.MustCompile("[a-z]{3}") b.ResetTimer() for i := 0; i < b.N; i++ { _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`) } }
func (*Regexp) Copy *Regexp
go1.6 から用意された Copy 関数です。
func (re *Regexp) Copy() *Regexp
コピーの利点は下記の通りです。
Copy returns a new Regexp object copied from re. When using a Regexp in multiple goroutines, giving each goroutine its own copy helps to avoid lock contention.
regexp - The Go Programming Language
要は、 Regexp が複数の goroutine で使われても大丈夫なように goroutine safe に作られているのですが、競合されないようにロックをしているので複数の goroutine で使いまわされることのある Regexp は使用前に Copy せよ、ということになります。
上にあったコードから参考に修正すると、Regexp の関数を実行する前に re := re.Copy()
でコピーを取得しています。
var finder = func() func(string) []string { re := regexp.MustCompile("[a-z]{3}") return func(str string) []string { // Copy regexp. re := re.Copy() return re.FindAllString(str, -1) } }() func main() { list := []string{ "foo bar baz", "kaneshin", "shintaro kaneko", } var wg sync.WaitGroup for _, str := range list { wg.Add(1) str := str go func() { defer wg.Done() fmt.Println(finder(str)) }() } wg.Wait() }
Benchmark
// !! No Copy // BenchmarkParallelRegexpMustCompileNoCopy-16 // 5000 375899 ns/op 99689 B/op 817 allocs/op // BenchmarkParallelRegexpMustCompileCopy-16 // 30000 56602 ns/op 59976 B/op 805 allocs/op func BenchmarkParallelRegexpMustCompileCopy(b *testing.B) { re := regexp.MustCompile("[a-z]{3}") b.ResetTimer() b.RunParallel(func(pb *testing.PB) { // Copy Regexp // https://golang.org/src/regexp/all_test.go?s=20028:20077#L718 re := re.Copy() // ❤️ for pb.Next() { _ = re.FindAllString(`Lorem ipsum dolor sit amet, consectetur..`) } } }