- 投稿日:2019-10-11T18:08:25+09:00
GitLab CI で Cloud Datastore のテストをする
TL;DR
- golang でのテストのお話
- Cloud Datastore は gRPC なので
http.Client
の変更でモック化する httpmock とかが使えない- Cloud Datastore Emulator を GitLab CI の services として起動しておけばテストできる
ことの発端
以下のようなプログラムがあったとして、 httpmock を利用して Cloud Datastore へのリクエストをモック化してテストコードを書こうとしたら datastore.Client の作成でエラーが出た.
- main.go
package main import ( "context" "log" "os" // golang.org "golang.org/x/oauth2/google" // google.golang.org "google.golang.org/api/option" // cloud.google.com "cloud.google.com/go/datastore" ) func main() { ctx := context.Background() // 1. Get google application credentials. var credOption option.ClientOption credentials, err := google.FindDefaultCredentials(ctx) if err != nil { log.Fatal(err) } if credentials.JSON != nil { credOption = option.WithCredentialsJSON(credentials.JSON) } else { credOption = option.WithTokenSource(credentials.TokenSource) } // 2. Put value. err = PutValue("xxxxxxxxxxx", "example", credOption) if err != nil { log.Fatal(err) } } type Item struct{ Key *datastore.Key `datastore:"__key__"` Value string `datastore:"value"` } func PutValue(nameKey, value string, options ...option.ClientOption) error { ctx := context.Background() // 1. Create client. client, err := datastore.NewClient(ctx, os.Getenv("DATASTORE_PROJECT_ID"), options...) if err != nil { return err } defer client.Close() // 2. Put item. _, err = client.Put( ctx, datastore.NameKey("Entity", nameKey, nil), &Item{ Value: value, }, ) if err != nil { return err } return nil }
- main_test.go
package main import ( "net/http" "testing" // github.com "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" // google.golang.org "google.golang.org/api/option" ) func TestDatastore(t *testing.T) { credOption := option.WithCredentialsJSON([]byte("hoge")) t.Run("PutValue", func(t *testing.T) { mockHTTPClient := &http.Client{} httpmock.ActivateNonDefault(mockHTTPClient) defer httpmock.Reset() err := PutValue( "xxxxxxxxxxx", "example", credOption, option.WithHTTPClient(mockHTTPClient), ) assert.Nil(t, err) }) }
- test
$ go test -v ./... --- FAIL: TestDatastore (0.00s) --- FAIL: TestDatastore/PutValue (0.00s) main_test.go:29: Error Trace: main_test.go:29 Error: Expected nil, but got: &errors.errorString{s:"dialing: WithHTTPClient is incompatible with gRPC dial options"} Test: TestDatastore/PutValue FAIL FAIL command-line-arguments 0.036sソース読んでみたところ
- ざっくり言うと
http.Client
で通信するわけじゃないと言うことCloud Datastore をテストするには
1. main_test.go で httpmock を使うのをやめる
package main import ( "testing" // github.com "github.com/stretchr/testify/assert" // google.golang.org "google.golang.org/api/option" ) func TestDatastore(t *testing.T) { credOption := option.WithCredentialsJSON([]byte("hoge")) t.Run("PutValue", func(t *testing.T) { err := PutValue( "xxxxxxxxxxx", "example", credOption, ) assert.Nil(t, err) }) }2. エミュレーターを起動する
$ docker run -d -it --rm --name cloud-datastore-emulator --entrypoint gcloud google/cloud-sdk:latest beta emulators datastore start --project=eg-example-01 --host-port=0.0.0.0:80813. go test
$ export DATASTORE_PROJECT_ID=eg-example-01 $ export DATASTORE_EMULATOR_HOST=datastore:8081 $ go test -v ./... === RUN TestDatastore === RUN TestDatastore/PutValue --- PASS: TestDatastore (0.08s) --- PASS: TestDatastore/PutValue (0.08s) PASS ok command-line-arguments 0.158sGitLab CI 化
- .gitlab-ci.yml
image: docker:stable stages: - testing variables: GIT_SUBMODULE_STRATEGY: recursive GO_VERSION: "1.12" Test golang: stage: testing image: golang:${GO_VERSION} variables: DOCKERIZE_VERSION: v0.6.1 DATASTORE_EMULATOR_HOST: datastore:8081 DATASTORE_PROJECT_ID: eg-example-01 services: - name: google/cloud-sdk:latest alias: datastore entrypoint: ["gcloud", "beta", "emulators", "datastore"] command: ["start", "--project", "eg-example-01", "--host-port", "0.0.0.0:8081"] before_script: - curl -sfL https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar -C /usr/local/bin -xzv - dockerize -wait tcp://${DATASTORE_EMULATOR_HOST} -timeout 1m script: - go test -v ./...
- services に Cloud Datastore Emulator のコンテナを指定
- ローカルでテストするときは
--host-port=localhost:8081
とか指定してもいいけど、それだとコンテナ外部から通信できないので--host-port=0.0.0.0:8081
にする- 環境変数に DATASTORE_PROJECT_ID, DATASTORE_PROJECT_ID を追加する
- DATASTORE_PROJECT_ID は services で指定したエイリアスをホスト名にする
- エミュレーターがちゃんと起動してからテストしたいので dockerize で待つ
- あとはテストするだけ
- 投稿日:2019-10-11T15:55:32+09:00
GOLANGを勉強しはじめた
おひさしぶり 皆、
先週からGOLANGを勉強しはじめました。
UDEMY で コースを 買って、毎日 1 ヴィデオ 見ていった。
今週で、3回 ホームページのデザイン はじめた。
NUXT.JS とTAILWINDCSSを けいかくしています。
少し NODE.JS わかります、
来週も、GOLANGビデオを見続けます,
コード例をとこしてみます。
またね.
- 投稿日:2019-10-11T15:06:14+09:00
東京高専プロコンゼミ総勢32名の名刺をHTMLで刷った
第30回高専プロコン、突如出現した移動部門で盛り上がっていますね。
私は今回自宅待機を選んだので酷い目に合わずほっとしているのですが、よく考えると参戦勢よりも関東自宅待機勢の方が酷い目に合うのでは?と思ってるところです1。さて、プロコンといえば「名刺クエスト」。高専生同士が名刺を交換し、交換した名刺の数で勝敗を決めるという、プロコンの中でも最もフェアな部門です。
東京高専の名刺のデザインは一昨年から私が担当していました。友人のMacに入っているCreative Cloudのイラレを使ってデザイン・印刷を行っていましたが、今年は彼が貸してくれないので、HTMLとCSSで書くことにしました。
この記事を参考にしました。
paper.cssを使ってブラウザだけで名刺を印刷する
https://qiita.com/okoppe8/items/abcafdad3a894bca7f38名刺をデザインする
名刺のデザインをどうしようかな~と考えてた時に、ちょうどポケットの中に入ってたのは定期券でした。
IC定期券風名刺を刷れば面白いんじゃないか、と思いました。東京高専ですし、緑色なのはまだしもピンク色のは東京の象徴と言えます。
また、通生が比較的多い(多分)東京高専の象徴とも言えます。そこでこんなデザインを作りました。
また、一応緑色風と真面目なデザインも作っておきました。
カードの外形
91mm×55mmなので、こんな感じに指定しています。
meishi.csscard { position: absolute; width: 91mm; height: 55mm; }背景の文字
ピンク色定期券の文字は、Google Fontsで色々と検討した結果、Nova Monoがイメージに近いという結論に至りました。Google Fontsの読み込んで使っています。
上の文字
定期券風の文字は、通学定期を参考にして描きました。基本的にFlexboxレイアウトにして、配置していきます。
定期上の文字は横幅2倍だったり半分だったりするのですが、これはcssのtransform要素で解決できます。teiki.css.double { transform: scale(2, 1); /* width方向に2倍 */ }ただし、transformには重大な欠点がありまして、Flexboxによる配置後にtransformが実行されるようなのです。
例えば、上の名刺で言う「仕事」は半幅、「名刺作り……」2倍幅のtransformを実行しているのですが、Flexboxが配置された後にtransformされるので、何もしなければ変なマージンが空いてしまいます。transform: matrixを使って平行移動も噛ませることでなんとかしましたが、どうにかならないのかな、と思う仕様でした。
また、矢印はfontawesomeで実装しています。
名刺のPDF生成
名刺は一度10枚単位の名刺用紙に刷り、そこから切り取り線に沿って切り取るという方式です。
そのため、名刺用紙に刷るPDFが必要でした。今回取ったフローとしては、以下の通りです。
- Google FormでSpreadsheet上にデータを集める
- Spreadsheet上のデータをクリップボード経由でExcelに出し、CSVで出力
それが完了すれば、次のようなコードで自動的にPDFに変換できます。
- CSV読み込み
- データに対し以下を繰り返す。
- 対象データをテンプレートエンジンに掛けて、HTMLを生成
- そのHTMLをHTTPサーバーで配信
- google-chromeのヘッドレスモードを使い、--print-to-pdfでPDF生成
- HTTPサーバーを閉じる
今思えばHTTPサーバーでまとめて配信すれば良かったのですが、プログラムの複雑性の問題や時間がなく適当に書き殴りたかったのでforループ一個で済むこの方式になりました。
まず予めHTMLのテンプレートを作成しました。上のHTMLとCSSをコピーしてきて、名前欄やID欄などをGolangのhtml/template方式で変数展開に書き換えた後、JavaScriptで10枚に増やす処理を追加しました。
するとこんな感じになりました
後はこれをHTTPサーバーで配信して、ChromeのheadlessモードでPDF生成すればPDFの出来上がりで、このPDFをサイズ100%で刷れば名刺が完成します。
何人も連続で刷る場合、PDFを連結して刷った方が良いです。Popplerに付属するpdfuniteが使いやすかったです。
困ったこと・反省点
- transformの仕様
- Go言語を選んだのはミス。ChromeとHTMLテンプレートエンジンを使うのであればNode.jsが相応しかった
- Macの印刷設定がまいどまいどサイズ95%に戻るせいで、慣れないうちは何度かミスプリントしてしまった。
- 全般的に人力が多かったので限界っぽくなった。次はもっと自動化したい。
- 緑色の定期券も作ろうとしたのだが、PDFから直接印刷しようとすると変な模様が出てしまう。画像化すれば問題はないものの、プリンタの読み込みに時間がかかるので、量産できなかった。
最後に
プロコン期間中、東京高専プロコンゼミと名刺交換して伝説の高専生の本名を手に入れよう!
本記事には登場しなかった「真面目なデザイン」「緑色の定期券風」の名刺も手に入る!2
- 投稿日:2019-10-11T09:47:45+09:00
どろどろの手動GUI作業を少しでもスマートにできないかコードで戦ってみた話
アウトプット強化週間!(第5版)
昨日はターミナル作業の話、今日はGUI作業をなんとかしたかったの話
あるある
寝食削ってその世界にハマった。ちょまどさんが語る、プログラミングへの尽きせぬ「愛」とは
エビデンス自動化の部分があるある過ぎる・・・
この世には自分にそっくりな人が3人はいるそうだが、たぶん、SIerでこのケースでは何千人かいそうな気がする。自作自動化ツール展とかやったら超絶楽しそうだけど、開発エピソード聞いたら涙が止まらなくなりそうだ。
愚痴コーナー
どうもAnsibleやらRPAやらを「楽して楽ができるツール」(よくわからない日本語のようで経験者はわかりますよね)みたいに扱っているフシがあるんだけど、そもそもコンピューターの作業を手動で行う事自体に品質の問題があって自動化=高速化=楽ではないんだ。
自動化には誤作動しないようにデバックしたりの下準備が必要で、実は手を動かすのは準備が無いのでトータルでは手動の方が短時間だったりするけどSIerの仕事だとそれが分かっていないのか自動化エライみたいになってしまうし、有名なOSSでお墨付きがあると余計にそっちに勾配降下していく。
僕はスクラッチでも良いと思ってて繰り返す作業の品質を担保するように局所的に最適化したツールをメンテナンスし続けるのも一つの手だと思う。手になじむものを大事にするの大工の道具とおんなじ気がする。チームで共有言語化しているのが大事なんだよね。
というような前口上からあんたのツールは?
少なくとも操作してからのエビデンス取得の手動繰り返しは避けたい。ツール配布を考えてgolangで作ろうとすると
https://github.com/go-vgo/robotgo
ってのがあるんだけどCrossCompilingって項目があるのに実際にLinuxからコンパイルするとエラーで通らない。色々試したけどだめ
# github.com/robotn/gohook /tmp/go-build114241532/b042/_x003.o: In function `eb_port_create': /root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:382: undefined reference to `sched_yield' /tmp/go-build114241532/b042/_x003.o: In function `eb_port_free': /root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:350: undefined reference to `sched_yield' /root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:341: undefined reference to `sched_yield' /tmp/go-build114241532/b042/_x003.o: In function `cleanup_ops': /root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:858: undefined reference to `sched_yield' /tmp/go-build114241532/b042/_x003.o: In function `port_list_signal_first': /root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:643: undefined reference to `sched_yield' /tmp/go-build114241532/b042/_x003.o:/root/pkg/mod/github.com/robotn/gohook@v0.0.0-20190905174353-5bea7a10cc3a/event/../chan/eb_chan.h:643: more undefined references to `sched_yield' follow collect2: error: ld returned 1 exit status(これ解決できた人いますかね?)
クロスコンパイルはあきらめて、WSL+msys2の構成でWindows上でのコンパイルは実行できたものの、zlib1.dllが必要なようでそのせいでgolangのワンバイナリのメリットが死んでしまう。
(htmlとかcsvみたいな静的なファイルをワンバイナリに組み込むツールはあるが、そもそも起動に影響するDLLをパッケージ化する方法は無い)よってgolangの良さを活かしつつツール化してみた。
https://github.com/yasutakatou/guicast
https://github.com/yasutakatou/guicast/releases
(バイナリも置いてみました。64bit windowsで動きます。)動かすとプロンプトが出てくる。昨日のとおんなじですねーシェルのライブラリおなじですからね。
>>>基本動作は 文字列 or キー操作のエミュレートと指定したウィンドゥ名のキャプチャ採取 の繰り返し。以下動作画像のようにあらかじめChromeをターゲットに設定してある。
>>> config wait:1 count:1 changeWindow:ctrl,\t targetWindow:Chrome autoCapture mode: false captureWait:1 captureHang:10 capturePath: ウィンドゥ名[Chrome]をターゲットに操作/文字列を投げ込んだら1秒待ってCtrl+Tabでタブを切り替える。繰り返しは1回。特徴として繰り返し回数とか柔軟に変更できるので操作端末のスペックが早ければ同時に何十窓でも開いて繰り返し可能。キャプチャが画像なので保存に時間がかかる点はSSDとか早いディスクで解消できる
操作/文字列は[ ]でくくると操作をなげます。ctrl,\tならCtrl+Tab、alt,\aならAlt+Aです。[ ]が無いと文字列が投げられます。また | で一行でまとめて投げられます。test|\nならtestを入力してリターンになります。オプションは
>>> count 3みたいに使います。以下があります。(helpみたいな説明いらなそうなのははしょりました)
Commands: ! 自動キャプチャ設定しててもキャプチャしないで投げ込みします autoCapture 投げ込み後に自動キャプチャします。タブ数+日時でpngが作られます captureHang キャプチャに時間がかかりすぎるときに処理を止めます capturePath キャプチャの保存パス。デフォはバイナリと同じところ captureWait キャプチャ一回毎に待ちを入れます。僕のPCが激遅なので用意しましたがSSDならゼロでもいいのかも change タブを切り替えるコマンドです。 count タブを繰り返す回数です list 起動させているウィンドゥ名を一覧で表示します onlyCapture キャプチャだけします。投げ込みはしません。 target 操作したウィンドゥ名を指定します wait 投げ込み後の待ち時間です。これも早いPCなら一秒も待たなくて良いかもしれないです。これで一人3多重程度だった、ぽちぽち作業を15~20多重で作業する事が出来た。キャプチャも取り漏れなし。作業端末が遅いのでたっぷりウェイトかけてるのでもち手打ちの方が早いけど、もっと早い端末ならウェイト減らしても平気なはず。昨日のみたくJupyterのkernel作ったら実行できる手順書になるので手順書を眺めつつ作業が無くなるので、さらに作業者は楽だし作業品質も高まるよね。
RPAも良いんだけど、古い人間なのでターミナルに寄ってしまいました。ぽちぽちを避けるのに別のぽちぽちを産むのが嫌だったもので。
まあ案の定、黒画面見た事が無いような人が使えるわけも無く、闇に葬られたわけなんですけど。。(オチ)
同じ作業を何百回と繰り返すとなると・・おわかりですよね。腱鞘炎ですね。
ぽちぽち100万回/年にも耐えられるようにエンジニアには筋肉が必要だ!というのもわからんでもないな。
- 投稿日:2019-10-11T09:00:21+09:00
cweill/gotestsでTable Driven Testsをらくらく実行
はじめに
Table Driven Tests(テーブルドリブンテスト。以下、TDT)とは、入力値と期待値などをもったテストケースを用意し、順次実行していくテスト手法です。
ここでいうTableとはエクセルのようなスプレッドシート、すなわち表のことを指し、テストケースを網羅した表を用意するようにテストコードを用意していきます。
公式でも使われています。
https://github.com/golang/go/wiki/TableDrivenTestsTDTの例
例として、挨拶メッセージを返すメソッドをテストしてみましょう。
引数の時刻によって「おはよう」「こんにちは」「こんばんは」の3種類のメッセージのうちどれか1種類が返されます。
条件と期待結果は以下のとおりです。
Input Output 05:00-11:59 おはよう 12:00-17:59 こんにちは 18:00-04:59 こんばんは メソッドは以下のとおり。
// greeter.go package main import "time" func Greet(time time.Time) string { hour := time.Hour() switch { case 5 <= hour && hour <= 11: return "おはよう" case 12 <= hour && hour <= 17: return "こんにちは" case (18 <= hour && hour <= 23) || (0 <= hour && hour <= 4): return "こんばんは" } panic("Unknown Hour") }ボイラープレートの生成をcweill/gotestsに任せる
公式例のようにテストを書いていこう!としても、なかなか面倒です。
そこでcweill/gotestsを活用していきます。
gotestsはメソッドシグネチャを読み取り、テスト用のボイラープレートを自動生成してくれます(大感謝)。https://github.com/cweill/gotests
// 2019/10/08 最新版 go get -u github.com/cweill/gotests@v1.5.3 // greeter.goのテストコードを生成 gotests -w -all greeter.goテストケースを追加していく
以下のような
greeter_test.go
が生成されたはずです。
nameがテスト名、argsがGreetの引数、wantが期待結果を指します。// greeter_test.go package main import ( "testing" "time" ) func TestGreet(t *testing.T) { type args struct { time time.Time } tests := []struct { name string args args want string }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Greet(tt.args.time); got != tt.want { t.Errorf("Greet() = %v, want %v", got, tt.want) } }) } }実際に
// TODO: Add test cases.
の箇所にケースを追加していきましょう。
境界値を意識すると、テストはこんな感じになるかと思います(便宜的に3ケースだけ掲載)。
Name Input Output 05:00 expects おはよう 05:00 おはよう 11:59 expects おはよう 11:59 おはよう 12:00 expects こんにちは 12:00 こんにちは コードで網羅すると、テストメソッドは以下のようになります。
package main import ( "testing" "time" ) func TestGreet(t *testing.T) { type args struct { time time.Time } tests := []struct { name string args args want string }{ // おはよう { name: "05:00 expects おはよう", args: args{ time: time.Date(2019, 1, 1, 5, 0, 0, 0, time.Local), }, want: "おはよう", }, { name: "11:59 expects おはよう", args: args{ time: time.Date(2019, 1, 1, 11, 59, 59, 59, time.Local), }, want: "おはよう", }, // こんにちは { name: "12:00 expects こんにちは", args: args{ time: time.Date(2019, 1, 1, 12, 0, 0, 0, time.Local), }, want: "こんにちは", }, { name: "17:59 expects こんにちは", args: args{ time: time.Date(2019, 1, 1, 17, 59, 59, 59, time.Local), }, want: "こんにちは", }, // こんばんは { name: "18:00 expects こんばんは", args: args{ time: time.Date(2019, 1, 1, 18, 0, 0, 0, time.Local), }, want: "こんばんは", }, { name: "23:59 expects こんばんは", args: args{ time: time.Date(2019, 1, 1, 23, 59, 59, 59, time.Local), }, want: "こんばんは", }, { name: "00:00 expects こんばんは", args: args{ time: time.Date(2019, 1, 1, 0, 0, 0, 0, time.Local), }, want: "こんばんは", }, { name: "04:59 expects こんばんは", args: args{ time: time.Date(2019, 1, 1, 4, 59, 59, 59, time.Local), }, want: "こんばんは", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Greet(tt.args.time); got != tt.want { t.Errorf("Greet() = %v, want %v", got, tt.want) } }) } }Run
書き終わったら、あとはテストを走らせるだけ。
input/outputが網羅されて、わかりやすいテストになりました。Running tool: /usr/local/go/bin/go test -timeout 30s github.com/mshrwtnb/sandbox -run ^(TestGreet)$ -v === RUN TestGreet === RUN TestGreet/05:00_expects_おはよう === RUN TestGreet/11:59_expects_おはよう === RUN TestGreet/12:00_expects_こんにちは === RUN TestGreet/17:59_expects_こんにちは === RUN TestGreet/18:00_expects_こんばんは === RUN TestGreet/23:59_expects_こんばんは === RUN TestGreet/00:00_expects_こんばんは === RUN TestGreet/04:59_expects_こんばんは --- PASS: TestGreet (0.00s) --- PASS: TestGreet/05:00_expects_おはよう (0.00s) --- PASS: TestGreet/11:59_expects_おはよう (0.00s) --- PASS: TestGreet/12:00_expects_こんにちは (0.00s) --- PASS: TestGreet/17:59_expects_こんにちは (0.00s) --- PASS: TestGreet/18:00_expects_こんばんは (0.00s) --- PASS: TestGreet/23:59_expects_こんばんは (0.00s) --- PASS: TestGreet/00:00_expects_こんばんは (0.00s) --- PASS: TestGreet/04:59_expects_こんばんは (0.00s) PASS ok github.com/mshrwtnb/sandbox 1.309s Success: Tests passed.以上