2019年4月20日土曜日

net/http を httptest を使ってテストする方法

概要

net/http には net/http/httptest という標準パッケージがありこれを使えばテストを書くことができます
今回は net/http/httptest パッケージを使ってテストを書いてみました

環境

  • macOS 10.14.4
  • go 1.11.5

サンプルアプリ

  • vim $GOPATH/src/github.com/hawksnowlog/a/main.go
package main

import (
    "fmt"
    "net/http"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("hello")
    w.Write([]byte("hello"))
}

func main() {
    http.HandleFunc("/", HelloHandler)
    http.ListenAndServe(":8080", nil)
}

/ にアクセスすると「hello」と返す簡単なアプリです
実際にアプリのハンドラに通っていることを確認するために fmt.Println を記載しています

基本的なテスト

ではテストを記載します
xxx_test.go というテストファイルを同一ディレクトリに置きましょう

  • vim $GOPATH/src/github.com/hawksnowlog/a/main_test.go
package main

import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestRoot(t *testing.T) {
    testserver := httptest.NewServer(http.HandlerFunc(HelloHandler))
    defer testserver.Close()
    res, err := http.Get(testserver.URL)
    if err != nil {
        t.Error(err)
    }
    hello, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    if err != nil {
        t.Error(err)
    }
    if res.StatusCode != 200 {
        t.Error("a response code is not 200")
    }
    if string(hello) != "hello" {
        t.Error("a response is not hello")
    }
}
  • go fmt github.com/hawksnowlog/a
  • go test github.com/hawksnowlog/a

でテスト出来ます

解説

まずはテストサーバを作成します
アプリ側で作成したハンドラを使って作成します

testserver := httptest.NewServer(http.HandlerFunc(HelloHandler))
defer testserver.Close()

ちゃんと Close するのを忘れないようにしましょう
作成したサーバに GET リクエストを送信するには http.Get を使います

res, err := http.Get(testserver.URL)

あとは受け取ったレスポンスをチェックすれば OK です

実際にアプリを動かしてテストしても OK

実は net/http/httptest で NewServer すると指定したハンドラを持つアプリがテスト用に立ち上がり LISTEN しています
上記の場合ただそこに http.Get しているだけです
なので httptest を使わずに以下のように書くこともできます

  • go build github.com/hawksnowlog/a
  • ./a

で普通にアプリを立ち上げたあとに、そのアプリ目掛けて http.Get すれば OK です

  • vim $GOPATH/src/github.com/hawksnowlog/a/main_test.go
package main

import (
    "io/ioutil"
    "net/http"
    "testing"
)

func TestRoot(t *testing.T) {
    res, err := http.Get("http://localhost:8080")
    if err != nil {
        t.Error(err)
    }
    hello, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    if err != nil {
        t.Error(err)
    }
    if res.StatusCode != 200 {
        t.Error("a response code is not 200")
    }
    if string(hello) != "hello" {
        t.Error("a response is not hello")
    }
}
  • go test github.com/hawksnowlog/a

ハンドラを mock するには

ハンドラを mock したいのであれば単純にアプリのハンドラを使わずにテスト側でハンドラを定義すれば OK です

  • vim $GOPATH/src/github.com/hawksnowlog/a/main_test.go
package main

import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
)

func mockHelloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("mock hello"))
}

func TestRoot(t *testing.T) {
    testserver := httptest.NewServer(http.HandlerFunc(mockHelloHandler))
    defer testserver.Close()
    res, err := http.Get(testserver.URL)
    if err != nil {
        t.Error(err)
    }
    hello, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    if err != nil {
        t.Error(err)
    }
    if res.StatusCode != 200 {
        t.Error("a response code is not 200")
    }
    if string(hello) != "mock hello" {
        t.Error("a response is not mock hello")
    }
}
  • go test github.com/hawksnowlog/a

ハンドラ内の特定の処理だけ mock したい場合には面倒ですがその部分だけ mock したハンドラを定義すれば OK です
もしくは gomock というパッケージがあるのでこれを使って特定のメソッドを mock するという方法もあります

最後に

net/http/httpclient を使って net/http をテストしてみました
基本的にはテストしたいハンドラ指定したり mock してテストします
これだけでも基本的なテストは十分にできるかなと思います

2019年4月19日金曜日

golang で環境変数を使う方法

概要

デフォルト値がある場合は LookupEnv を使いましょう

環境

  • macOS 10.14.4
  • go 1.11.5

os.Getenv

package main

import (
    "fmt"
    "os"
)

func main() {
    val := os.Getenv("HOGE")
    fmt.Println(val)
}

HOGE が設定されていない場合は空文字が val には入ります

os.LookupEnv

package main

import (
    "fmt"
    "os"
)

func main() {
    val, ret := os.LookupEnv("HOGE")
    if ret == false {
        val = "default"
    }
    fmt.Println(val)
}

ret に環境変数がセットされていたかのフラグが返ってきます
false の場合はデフォルト値をセットしましょう

2019年4月18日木曜日

docker で git credential を使ったプライベートリポジトリをクローンする方法を考える

概要

プライベートリポジトリを clone する場合には大抵の場合認証情報が必要になります
普通はインタラクティブに入力しますが docker の場合はそうもいきません
今回は git credential を使ってプライベートリポジトリを clone する方法を複数考えてみました

環境

  • macOS 10.14.4
  • docker 18.09.2

build-arg を使う方法

まずは build-arg を使う方法を紹介します
ビルドする際に認証情報を与えるので run する際には常に同じ認証情報を使うことになります
イメージは適当にローカルにあったものを使っているので環境に合わせて変更してください
要件としては git コマンドが使えれば OK です

  • vim Dockerfile
FROM golang:latest

RUN git config --global credential.helper store
ARG credential
RUN echo $credential > /root/.git-credentials

RUN git clone https://repo.url.com/hawksnowlog/private-repo.git

CMD ["cat", "/root/.git-credentials"]
  • docker build -t private_repo_clone --build-arg credential='https://hawksnowlog:secret@repo.url.com' .

docker run はしてもしなくても OK です
build すると clone もされるのですでにイメージはリポジトリがある状態になります

メリット・デメリット

build-arg に認証情報を設定するだけなので無駄なファイルが必要ないです
今回たまたま golang のイメージを使っていますが、このまま go get することも可能です

デメリットとしては run するたびに認証情報を変更できない点です
別のユーザがイメージを使う場合にビルドした人が設定した認証情報を使うことになるのであまりセキュリティ的によろしくありません

認証情報が固定になるのでプライベートな場合に使える方法かなと思います

環境変数を使う方法

コンテナに環境変数渡すオプション「-e」を使って実現する方法を考えます
シェルスクリプトを追加で用意しそこで認証情報の設定と clone を行います

  • vim Dockerfile
FROM golang:latest

ADD . /root
RUN chmod +x /root/credential.sh
RUN git config --global credential.helper store

CMD ["/bin/sh", "-c", "/root/credential.sh"]
  • vim credential.sh
#!/bin/sh

echo $CREDENTIAL > /root/.git-credentials
git clone https://repo.url.com/hawksnowlog/private-repo.git
cat /root/.git-credentials
ls go-vddk
  • docker build -t private_repo_clone .
  • docker run --rm -e CREDENTIAL='https://hawksnowlog:secret@repo.url.com' private_repo_clone

メリット・デメリット

この方法だと毎回認証情報が渡せるのでセキュアかなと思います
内容はただただシェルスクリプトを実行しているだけなので他にもやりようはいくらでもあると思います
が、ポイントは環境変数が参照できるようにわざわざ別のスクリプトを作成しているという点になります
Dockerfile 内だと -e の内容は参照できないためそうしています

ただデメリットが少し多いという気がしていて、まず 1 つファイルが増えてしまう点があります
また毎回 clone が走ってしまうのもデメリットかなと思います
本来であればイメージ作成時に必要なライブラリやファイルの配置し build を完了させておいて run でプロセスだけ起動するという方法が無難かなと思います

あとは微妙な点としては CMD が複数指定できないので clone のあとに何かしたいのであればシェルスクリプトに追記していくしかない点です
本来であればプロセスを起動したいと思うのでそれらの内容をシェルスクリプトに記載する必要があります

その他

git credential を使わなくても単純に clone コマンドに認証情報を入れてもいいと思います
もしくは鍵認証にすれば鍵をイメージ or コンテナに配置する方法でも良いと思います

複数のプライベートリポジトリから clone しなければいけない場合は .git-credentials に複数の認証情報を書き込むように書き換えれば OK です
今回の場合であれば複数の ARG を指定可能にしたり複数の環境変数を参照するように変更すれば OK です

最後に

今回紹介した方法は

  • --build-arg を使ってイメージ自体に認証情報を埋め込む
  • run 時に -e を使って認証情報を渡しそれをスクリプトで読み込む

になります
個人的には前者の方がシンプルで良いかなとは思います
がイメージにパスワードが埋め込まれているのでパブリックに公開するのは難しいかなと思います

他にもやり方はたくさんあるので一例として参考にしてみてください

2019年4月17日水曜日

irbrc 入門

概要

irb はデフォルトで ~/.irbrc を読み込んでくれます
プロンプトの表示や補完、履歴の保存などいろいろ irb をカスタマイズする設定を書くことができます
今回はオススメする設定の一部を紹介したいと思います

環境

  • macOS 10.14.4
  • Ruby 2.6.2p47
  • irb 1.0.0

履歴を保存する

IRB.conf[:SAVE_HISTORY] = 100
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-save-history"

100 件分の履歴を HISTORY_FILE で指定したファイルに保存してくれます
ここで保存された履歴は Ctrl+p や Ctrl+r などで再度使用することができます
ファイルはただのテキストファイルなのでディスクの容量に余裕があれば 10000 件くらい保存してもいいと思います

コマンドを補完する

require 'irb/completion'

これだけで TAB で Ruby の標準のメソッドや定数を補完してくれます
試しに「pu」まで打って TAB で補完すると puts などの候補が表示されると思います

プロンプトをカスタマイズする

IRB.conf[:PROMPT][:DEFAULT] = {
  :PROMPT_I=>"%N(%m):%03n:%i> ",
  :PROMPT_N=>"%N(%m):%03n:%i> ",
  :PROMPT_S=>"%N(%m):%03n:%i%l ",
  :PROMPT_C=>"%N(%m):%03n:%i* ",
  :RETURN=>"=> %s\n"
}

上記はデフォルトの設定です
%N%m などのフォーマット文字列が使えます

  • %N・・・irb
  • %m・・・main
  • %03n・・・3 桁の行番号
  • %i・・・インデントの数
  • %l・・・文字列中のタイプ

%l がわかりにくいですが例えば each を使っている場合は * になります

irb(main):007:0> a.each { |aa|
irb(main):008:1* puts aa
irb(main):009:1> }

wirb

  • gem install wirb
require 'wirb'
Wirb.start

これで出力結果に色が付きます
余談ですが irb -d で起動すると以下のようなエラーが発生しました
が require は正常に動作しているので問題ないです
おそらく $LOAD_PATH の順番が影響しているっぽく始めにロードしたパスではライブラリを見つけられなかったのですが後から検索したパスでうまく require できたんだと思います

Exception `LoadError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54 - cannot load such file -- wirb
Exception `LoadError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54 - cannot load such file -- 2.6/psych.so
Exception `LoadError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:132 - cannot load such file -- 2.6/psych.so
Exception `NameError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/psych/class_loader.rb:68 - uninitialized constant BigDecimal
Exception `NameError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/psych/class_loader.rb:68 - uninitialized constant Date
Did you mean?  Data
Exception `NameError' at /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/psych/class_loader.rb:68 - uninitialized constant DateTime

設定の一覧を確認

  • irb

で irb を起動して

irb(main):003:0> IRB.conf

で確認できます
参照する場合は

puts IRB.conf[:VERSION]
irb 1.0.0 (2018-12-18)

こんな感じで参照できます

最後に

irb を拡張する方法を紹介しました
.irbrc には Ruby そのものが書けるのでモジュールを拡張したりすれば更にいろいろできます

デフォルトの設定でもだいぶ強力なのでまずはデフォルトに慣れておくのが良いと思います

参考サイト

2019年4月16日火曜日

macOS の irb で前のコマンドがカーソルを先頭にするショートカットがうまく動作しないときの対処方法

概要

macOS で irb を使うと Ctrl+a でカーソルを先頭にしたり Ctrl+p で前のコマンドにしたい場合は ^A^P になってしまい動作しません
readline が必要なのですが普通に readline をインストールしただけではダメだったので対象方法を紹介します

環境

  • macOS 10.14.4
  • Ruby 2.6.2p47

readline インストール

  • brew install readline

ruby 再インストール

homebrew ですでに ruby をインストールしていました
おそらくその時にはまだ readline がインストールされていなく irb の挙動もおかしくなっていたんだと思います
homebrew で ruby をインストールした場合もし readline があればそれを使ってビルドしてくれるので irb も正しい挙動になります

なので readline をインストールしたあとで ruby も再インストールすれば irb も直ります

homebrew の ruby はいろいろと依存関係がある可能性が高く面倒なので upgrade しました

  • brew upgrade ruby
  • echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.bash_profile

homebrew の Ruby 2.6 はインストールパスが代わり手動で PATH を通す必要があります

irb のインストール

Ruby 2.6 の場合 irb も一緒に homebrew でインストールされます
が以下のエラーになり起動できません

Traceback (most recent call last):                                                                                             
        2: from /usr/local/opt/ruby/bin/irb:23:in `<main>'                                                                     
        1: from /usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path'                             
/usr/local/Cellar/ruby/2.6.2/lib/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': can't find gem irb (>= 0.a) with executable 
irb (Gem::GemNotFoundException)

どうやら gem 側でもインストールする必要があるのでインストールしましょう

  • gem install irb

ちなみに bundler でも同じエラーになるのでインストールしました

  • gem install bundler

あとは rake や rdoc コマンドも一緒にインストールされていたので、それらも使うのであれば gem install が必要だと思います

.irbrc の場所

普通に ~/.irbrc に配置すれば OK です
うまく読み込まれいるか確認する場合は irb -d で起動すればデバッグモードになるのでわかりやすいと思います

最後に

readline をインストールすれば OK みたいな記事が多かったのですが、それだけではダメでインストールした readline を使って再ビルドする必要がありました

2019年4月15日月曜日

golang で UUID を生成する

概要

google/uuid を使います

環境

  • macOS 10.14.4
  • go 1.11.5

インストール

  • go get github.com/google/uuid

サンプルコード

package main

import (
        "fmt"
        "github.com/google/uuid"
)

func main() {
        u, err := uuid.NewRandom()
        if err != nil {
                fmt.Println(err)
                return
        }
        uu := u.String()
        fmt.Println(uu)
}

たぶんこれが一番簡単で確実な方法かなと思います

2019年4月14日日曜日

go-workers で Struct をエンキューしたらどうなるか

概要

タイトルの通りです
go-workers でエンキュー/デキューするときに struct の情報がどうなるか試してみました

環境

  • macOS 10.14.4
  • go 1.11.5

struct を管理するパッケージ

エンキューする側、ワーカー側から参照するので外出ししておきます

  • vim utils/utils.go
package utils

type MyStruct struct {
    Name string
    Age  int
}

エンキュー

まずはエンキューします
エンキューする際に struct をそのまま投げてみます

  • vim main.go
package main

import (
    "github.com/jrallison/go-workers"
    "github.com/hawksnowlog/a/utils"
)

func main() {
    workers.Configure(map[string]string{
        "server":  "localhost:6379",
        "process": "1",
    })
    st := utils.MyStruct{
        Name: "hawksnowlog",
        Age:  20,
    }
    workers.Enqueue("default", "SampleWorker", st)
}
  • go run main.go

これでエンキューできます
事前に redis-server は上げておきましょう

Redis 確認

Redis に入った情報を確認してみたところ json に変換されて入っているようです
今度はこれをデキューして struct に変換してみます 

127.0.0.1:6379> LRANGE queue:default 0 -1
1) "{\"queue\":\"default\",\"class\":\"SampleWorker\",\"args\":{\"Name\":\"hawksnowlog\",\"Age\":20},\"jid\":\"180127fab357439ddd229174\",\"enqueued_at\":1554960010.4390862,\"at\":1554960010.4390862}"

worker 側でデキューする

最終的に struct に落とし込みますが直接はできませんでした
一旦 json 文字列を取得してそれを json.Unmarshal() する感じ です

  • vim worker/worker.go
package main

import (
    "encoding/json"
    "fmt"
    "github.com/jrallison/go-workers"
    "github.com/hawksnowlog/a/utils"
)

func perform(msg *workers.Msg) {
    j := msg.Args().ToJson()
    st := utils.MyStruct{}
    err := json.Unmarshal([]byte(j), &st)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(st.Name)
    fmt.Println(st.Age)
}

func main() {
    workers.Configure(map[string]string{
        "server":  "localhost:6379",
        "process": "1",
    })
    workers.Process("default", perform, 1)
    workers.Run()
}
  • go run worker/worker.go

最後に

go-workers でエンキュー/デキューするのに struct を使ってみました
エンキューする際はそのまま struct を投げられます
デキューするときだけ json 文字列から struct に変換する必要がありました
デキューするときだけ少し面倒な感じはしますが簡単にできます

調査不足かもしれないで何ともですがもしかしたら直接指定した struct に変換してくれる機能が go-workers にあるかもしれません