- 投稿日:2021-01-17T19:38:58+09:00
Goでテストを書くために、まずinterfaceを設計するということ
Goでテストコードを書くことになった場合に
あなたが、勤務先で上長から「テストコードを書いてください」と指示されたとき、
テストが書ける設計になっているでしょうか。この記事では、S3からファイルを取得して読み取るというユースケースに対して
テストを書くことについて考えていきます。テストが書きづらいケース
main.govar awssess = newSession() var s3Client = newS3Client() const defaultRegion = "ap-northeast-1" func newSession() *session.Session { return session.Must(session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, })) } func newS3Client() *s3.S3 { return s3.New(awssess, &aws.Config{ Region: aws.String(defaultRegion), }) } func readObject(bucket, key string) ([]byte, error) { obj, err := s3Client.GetObject(&s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), }) if err != nil { return nil, err } defer obj.Body.Close() res, err := ioutil.ReadAll(obj.Body) if err != nil { return nil, err } return res, nil } func main() { res, err := readObject("{Your Bucket}", "{Your Key}") if err != nil { log.Println(err) } log.Println(string(res)) return }S3バケットに存在するファイルを
log.Println()
により表示しています。
readObject()
に対してテストコードを書きます。
しかしreadObject()
はS3に直接アクセスする処理が含まれており、外部処理に完全に依存しています。
このままではテストを実行するごとにS3へのアクセスを実行しなくてはならず、現実的ではありません。ここでまず考えるべきことは、モックを作るということです。
関数を分ける
まず、現状の
readObject()
の問題点は以下の2点が同じ関数に存在することにあります。
- S3からファイルの取得
- ファイルの読み取り
そこで、この2つの処理を別関数で分離します。
func getObject(bucket, key string) (*s3.GetObjectOutput, error) { obj, err := s3Client.GetObject(&s3.GetObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), }) if err != nil { return nil, err } return obj, nil } func readObject(bucket, key string) ([]byte, error) { obj, err := getObject(bucket, key) if err != nil { return nil, err } defer obj.Body.Close() res, err := ioutil.ReadAll(obj.Body) if err != nil { return nil, err } return res, nil }これで分離ができました。
しかし、このままでは前述のS3アクセスへの依存は解決できていません。ここで、interfaceの出番です。
interfaceの実装
結論から述べると、モック化したい関数は
interface
の関数として実装するとよいです。
当記事では、getObject()
をinterface
の関数とします。そうすることで、
getObject()
の中の処理を外から渡すことができます。
正常時はS3へのアクセスを渡し、テスト時はモックを渡すことができるわけです。以下が
interface
を実装した全体コードです。main.gopackage main import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "io/ioutil" "log" ) var awssess = newSession() var s3Client = newS3Client() const defaultRegion = "ap-northeast-1" func newSession() *session.Session { return session.Must(session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, })) } func newS3Client() *s3.S3 { return s3.New(awssess, &aws.Config{ Region: aws.String(defaultRegion), }) } type objectGetterInterface interface { getObject() (*s3.GetObjectOutput, error) } type objectGetter struct { Bucket string Key string } func newObjectGetter(bucket, key string) *objectGetter { return &objectGetter{ Bucket: bucket, Key: key, } } func (getter *objectGetter) getObject() (*s3.GetObjectOutput, error) { obj, err := s3Client.GetObject(&s3.GetObjectInput{ Bucket: aws.String(getter.Bucket), Key: aws.String(getter.Key), }) if err != nil { return nil, err } return obj, nil } func readObject(t objectGetterInterface) ([]byte, error) { obj, err := t.getObject() if err != nil { return nil, err } defer obj.Body.Close() res, err := ioutil.ReadAll(obj.Body) if err != nil { return nil, err } return res, nil } func main() { t := newObjectGetter("{Your Bucket}", "{Your Key}") res, err := readObject(t) if err != nil { log.Println(err) } log.Println(string(res)) return }
readObject()
の引数に注目してください。
objectGetterInterface
を引数に渡しています。また、
getObject()
はobjectGetter
構造体をレシーバとし、メソッド化しています。
objectGetterInterface
は、以下のメソッドを満たしていることを条件としています。getObject() (*s3.GetObjectOutput, error)つまり、
objectGetter
構造体はobjectGetterInterface
の条件を満たしているということになります。では、モックのメソッドについても考えていきます。
同じように、getObject() (*s3.GetObjectOutput, error)
を満たすように実装してみましょう。type objectGetterMock struct{} func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) { b := ioutil.NopCloser(strings.NewReader("hoge")) return &s3.GetObjectOutput{ Body: b, }, nil }
hoge
という文字列が記録されているファイルが格納されているという想定です。これで、
objectGetterMock
構造体も同様にobjectGetterInterface
の条件を満たすことができました。では実際のテストコードをみてみましょう。
テストコード
main_test.gopackage main import ( "github.com/aws/aws-sdk-go/service/s3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "io/ioutil" "strings" "testing" ) type testSuite struct { suite.Suite service *objectGetter } func (s *testSuite) SetUpTest() { s.service.Bucket = "dummy" s.service.Key = "dummy" } func TestExecution(t *testing.T) { suite.Run(t, new(testSuite)) } type objectGetterMock struct{} func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) { b := ioutil.NopCloser(strings.NewReader("hoge")) return &s3.GetObjectOutput{ Body: b, }, nil } func (s *testSuite) Test() { mock := objectGetterMock{} res, _ := readObject(mock) assert.Equal(s.T(), "hoge", string(res)) }以下に注目してください。
func (s *testSuite) Test() { mock := objectGetterMock{} res, _ := readObject(mock) assert.Equal(s.T(), "hoge", string(res)) }
objectGetterMock
をreadObject()
に渡しています。
そのため、テストではS3からファイルの取得について気にする必要はなく、ファイルの読み取りにフォーカスしたテストを書くことができました。終わりに
interfaceを設計することでテストコードが非常に書きやすくなります。
ぜひ、テストコードを書くことで品質の高いプロダクトを目指してください。※こちらにサンプルコードを公開しています。
- 投稿日:2021-01-17T16:36:36+09:00
【Go】go-oidcを使ってOpen ID Connectを利用する
Open ID Connect
- outh2の進化版でトークン発行の処理がある
- アプリケーション自体にユーザーパスワードなどの認証情報を持たせずに、別のプロバイダー(ここではGoogle)に委任する
- プロバイダーはいろいろある
- AzureAD
- AWS Cognito
- outh0
state
という概念があり、クロスサイトスクリプティングなどのなりすましを防止公式パッケージ
https://github.com/coreos/go-oidc
インストール
git cloen https://github.com/coreos/go-oidcexampleのREADMEの通りにセットアップする
cd go-oidc/exampleyuta:~/go-oidc/example (v3=) $ cat README.md # Examples These are example uses of the oidc package. Each requires a Google account and the client ID and secret of a registered OAuth2 application. To create one: 1. Visit your [Google Developer Console][google-developer-console]. 2. Click "Credentials" on the left column. 3. Click the "Create credentials" button followed by "OAuth client ID". 4. Select "Web application" and add "http://127.0.0.1:5556/auth/google/callback" as an authorized redirect URI. 5. Click create and add the printed client ID and secret to your environment using the following variables:Google Developer Consoleへアクセス
https://console.developers.google.com/
認証情報(アプリケーション)を作る
設定する
環境変数の設定
作成
すると、クライアントIDとクライアントシークレットが作られるexport GOOGLE_OAUTH2_CLIENT_ID=739786550065-g36863uform2efr5nrvvmjpj30pu9nuf.apps.googleusercontent.com export GOOGLE_OAUTH2_CLIENT_SECRET=4sLRLGBdYu_un8C58c8Yg2yrosパッケージで実行サーバのOS環境変数が読み込まれる
var ( clientID = os.Getenv("GOOGLE_OAUTH2_CLIENT_ID") clientSecret = os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET") )動作確認
起動
yuta:~/go-oidc/example (v3=) $ go run idtoken/app.go 2021/01/17 16:13:04 listening on http://127.0.0.1:5556/
http://127.0.0.1:5556/
へアクセス認証成功時
- 投稿日:2021-01-17T16:11:08+09:00
【Golang】サポートする GOOS と GOARCH を一覧で確認するコマンド(ビルドの対応プラットフォーム確認)
go build
する際に指定可能な$GOOS
と$GOARCH
の一覧をコマンドで確認したい。なんか、Go 言語のバージョンだけでなく環境によってもビルドできる(サポートされた)OS やアーキテクチャが異なるようだったので、シェル・スクリプトで事前にチェックしたいのです。
「golang GOOS 一覧 確認 コマンド」でググってもピンポイントな記事が出てこなかったので、自分のググラビリティとして。
TL; DR
go tool dist list
TS; DR
$ go tool dist list aix/ppc64 android/386 android/amd64 android/arm ...(以下省略) $ go version go version go1.15.6 linux/amd64
- とりま、どんなものがあるか知りたい人
- Go言語のクロスコンパイル設定値 $GOOS, $GOARCH 一覧リスト @ Qiita
- Environment | Install | Doc @ Golang.org (本家、英語)
$GOARM
(Arm 系 CPU の一覧)
- https://github.com/golang/go/wiki/GoArm | wiki @ GitHub
参考文献
- Gist コメント | Go (Golang) GOOS and GOARCH @ Gist by asukakenji
- Go言語のクロスコンパイル設定値 $GOOS, $GOARCH 一覧リスト @ Qiita
- Environment | Install | Doc @ Golang.org (本家、英語)
- 投稿日:2021-01-17T16:11:08+09:00
【Golang】サポートする GOOS と GOARCH を一覧で確認するコマンド(GOARM のリンクもあるでよ)
go build
する際に指定可能な$GOOS
と$GOARCH
の一覧をコマンドで確認したい。なんか、Go 言語のバージョンだけでなく環境によってもビルドできる(サポートされた)OS やアーキテクチャが異なるようだったので、シェル・スクリプトで事前にチェックしたいのです。
「golang GOOS 一覧 確認 コマンド」でググってもピンポイントな記事が出てこなかったので、自分のググラビリティとして。
TL; DR
go tool dist list
TS; DR
$ go tool dist list aix/ppc64 android/386 android/amd64 android/arm ...(以下省略) $ go version go version go1.15.6 linux/amd64
- とりま、どんなものがあるか知りたい人
- Go言語のクロスコンパイル設定値 $GOOS, $GOARCH 一覧リスト @ Qiita
- Environment | Install | Doc @ Golang.org (本家、英語)
$GOARM
(Arm 系 CPU の一覧)
- https://github.com/golang/go/wiki/GoArm | wiki @ GitHub
参考文献
- Gist コメント | Go (Golang) GOOS and GOARCH @ Gist by asukakenji
- Go言語のクロスコンパイル設定値 $GOOS, $GOARCH 一覧リスト @ Qiita
- Environment | Install | Doc @ Golang.org (本家、英語)
- 投稿日:2021-01-17T15:02:19+09:00
Macでgqlgenのserver.goを起動するとファイアウォール警告が出る
GoのGraphQLサーバライブラリであるgqlgenの開発サーバ(GraphQL Playground)をMac上で起動すると、ファイアウォールの警告が出てしまってめんどくさい。
通常はMacの環境設定のセキュリティとプライバシーのファイアウォール設定で例外を設定するのですが、うまくいかなかったので別の方法で解決します。
server.goの最後で次のようになっています。
log.Fatal(http.ListenAndServe(":"+port, nil))これだと「0.0.0.0」でlistenするようです。0.0.0.0はすべてのネットワークインタフェースを指し、過剰です。
以下のように「localhost」まはは「127.0.0.1」を明示するように変更します。
log.Fatal(http.ListenAndServe("localhost:"+port, nil))これで警告は出なくなりました。
- 投稿日:2021-01-17T14:30:52+09:00
LINE Bot からDropboxに写真をアップロードする
タイトルの通り、LINE Bot からDropboxに写真をアップロードするシステムを作ってみました。現状は課題だらけですが、主要機能が実装できたので投稿します。
ソースコード
main.gopackage main import ( "io" "log" "net/http" "os" "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/dropbox/files" "github.com/line/line-bot-sdk-go/linebot" "gopkg.in/ini.v1" ) type Config struct { channelSecrt string channelToken string dropboxToken string } var conf Config func init() { c, _ := ini.Load("config.ini") conf = Config{ channelSecrt: c.Section("lineBotApi").Key("secret").String(), channelToken: c.Section("lineBotApi").Key("token").String(), dropboxToken: c.Section("DropboxApi").Key("token").String(), } } func main() { http.HandleFunc("/callback", lineHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } func lineHandler(w http.ResponseWriter, r *http.Request) { bot, err := linebot.New( conf.channelSecrt, conf.channelToken, ) if err != nil { log.Fatal(err) } events, err := bot.ParseRequest(r) if err != nil { if err == linebot.ErrInvalidSignature { w.WriteHeader(400) } else { w.WriteHeader(500) } return } for _, event := range events { if event.Type == linebot.EventTypeMessage { switch message := event.Message.(type) { case *linebot.ImageMessage: content, err := bot.GetMessageContent(message.ID).Do() if err != nil { log.Println(err) } filename := message.ID + ".png" file, err := os.Create(filename) if err != nil { log.Println(err) } if _, err = io.Copy(file, content.Content); err != nil { log.Println(err) } config := dropbox.Config{ Token: conf.dropboxToken, } cli := files.New(config) req := files.NewCommitInfo("/" + filename) f, _ := os.Open(filename) _, err = cli.Upload(req, f) if err != nil { log.Print(err) return } if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("アップロードが完了しました")).Do(); err != nil { log.Print(err) } default: if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage("画像を送信してください")).Do(); err != nil { log.Print(err) } } } } if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil { log.Fatal(err) } }要点まとめ
Botに送信した画像を受け取ってローカルサーバに保存する
保存するファイルが一意な名前になるようにmessageIDをファイル名にしています。
content, err := bot.GetMessageContent(message.ID).Do() if err != nil { log.Println(err) } filename := message.ID + ".png" file, err := os.Create(filename) if err != nil { log.Println(err) } if _, err = io.Copy(file, content.Content); err != nil { log.Println(err) }Dropboxにファイルをアップロードする
config := dropbox.Config{ Token: conf.dropboxToken, } cli := files.New(config) req := files.NewCommitInfo("/" + filename) f, _ := os.Open(filename) _, err = cli.Upload(req, f) if err != nil { log.Print(err) return }改善点
・ローカルに保管したファイルを削除する
・複数の画像を受け取れるようにする(現状は1枚ずつしか送信できない)感想
思い描いてた機能を実装できるって楽しいですね。
- 投稿日:2021-01-17T13:02:17+09:00
go moduleを使う
- 投稿日:2021-01-17T12:07:42+09:00
Go.modの使い方
GOPATHを使っているとimport出来ないライブラリとか出てきて変なエラーが出てくる事があると思います。
GoModulesのgo.modを使えば解決するかもしれないので記載しておきます。【環境】
windows10
VScode
ターミナルでgo envと打って設定を確認します
VScodeからターミナル→新しいターミナルで開いてgo envと打ち込みます。
ずらーっと色々設定が並んでくると思いますがその中の
GO111MODULE=
これがデフォルトだとonになっていないので設定をonにします。GO111MODULE=をonにする
ターミナルで
go env -w GO111MODULE=on
と打ち込みましょう。
もう一度go envと打って確認すると
set GO111MODULE=onとなっているはずです。
※筆者はこの時、なぜかonにならず原因が分からなかったので
一度Go言語を再インストールしたところうまくいきました。go.modを配置する
プロジェクトのルートディレクトリにcdで移動して、
go mod init example.com/m
と打ちます。
すると、ルートディレクトリにgo.modが作成されます。
go.modが作成されてから、go build と打ち込んで
ビルドするとimportに書かれているライブラリが自動でダウンロードされます。
ダウンロードされたライブラリがgo.modに書き込まれていきます。
こういう感じで↓[go.mod]module example.com go 1.15 require github.com/labstack/echo/v4 v4.1.17go.modはプロジェクト毎に作成して使います。
go.sumというファイルも自動生成されますが気にしなくて大丈夫です!moduleの部分についての説明はこちら
- 投稿日:2021-01-17T02:00:27+09:00
「世界一美しい数式」をプログラムで表現する
はじめに
$e^{iπ} = -1$
は、「世界一美しい数式」といわれているオイラーの等式です。
確かに美しいというか無駄がないという感じですが、これをプログラムで出力してみたいと思いました。
ただ、それだけです。とはいえ、一見簡単そうな数式ですが、プログラムで表現するのは少しだけ手間です。
- $e$や$π$といった無理数を扱う必要がある。
- 複素数を扱う必要がある。
- さらに、複素数のべき乗を扱う必要がある。
実行環境
オンラインでコンパイル可能な以下のいずれかで実行確認しました。
Paiza.io
TIOD言語の場合
- $e$は
E
、$π$は、PI
という定数定義がstd.math
で用意されている。- 複素数は、
complex
テンプレートが用意されている。- べき乗は
^^
演算子を利用できる。import std; void main() { auto x = complex(cast(real)0.0, PI); auto e = complex(E, cast(real)0.0); auto ans = e ^^ x; writeln(ans); writefln("%.10f %.10f", ans.re, ans.im); }出力結果-1-5.01656e-20i -1.0000000000 -0.0000000000
writeln(ans)
ように、演算結果をそのまま出力すると、-1-5.01656e-20i
と誤差が発生します。
他のプログラミング言語でもそうですが、無理数を浮動小数で扱う以上、このようになります。
ちなみに、D言語の場合、倍精度(double
)よりも精度が高いreal
型を利用できるので、虚数部の誤差が5.01656e-20
と、より少なくなっています。
double
型を利用すると、他のプログラミング言語と同様-1+1.22465e-16i
となります。誤差を丸めて表示するために
writefln("%.10f %.10f", ans.re, ans.im)
と小数点以下10桁表示にしました。
その場合、実数部が-1.0000000000
、虚数部が-0.0000000000
と表示されました。以下、他のプログラミング言語の実装例も紹介します。
C言語の場合
$e$や$π$は、C言語の標準仕様では定数定義されていない。
コンパイラによりM_E
やM_PI
として定義されている場合がある。複素数は、
double complex
が用意されている。複素数のべき乗は
cpow
関数を利用できる。#include <stdio.h> #include <math.h> #include <complex.h> #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #ifndef M_E #define M_E 2.71828182845904523536 #endif int main(void){ double complex x1, z; x1 = M_PI * I; z = cpow(M_E, x1); printf("%.20f %.20f\n", creal(z), cimag(z)); printf("%.10f %.10f\n", creal(z), cimag(z)); }出力結果-1.00000000000000000000 0.00000000000000012246 -1.0000000000 0.0000000000
double complex
型を複素数形式でそのまま表示できないようです。C++の場合
- $e$や$π$は、
<cmath>
でM_E
やM_PI
として定義。- 複素数は、
complex
テンプレートが用意されている。- べき乗は
pow
関数を利用できる。#include <iostream> #include <cmath> #include <complex> using std::complex; int main(void){ auto x1 = complex<double>(0.0, M_PI); auto e = complex<double>(M_E, 0.0); auto z = pow(M_E, x1); std::cout << z << std::endl; printf("%.10f %.10f\n", real(z), imag(z)); }出力結果(-1,1.22465e-16) -1.0000000000 0.0000000000C#の場合
- $e$や$π$は、
Math.E
やMath.PI
として定義。- 複素数は、
Complex
クラスが用意されている。- 複素数のべき乗は
Complex.Pow
関数を利用できる。using System; using System.Numerics; public class Example { public static void Main() { Complex x = new Complex(0.0, Math.PI); Complex ans = Complex.Pow(Math.E, x); Console.WriteLine(ans); Console.WriteLine("{0:f10} {1:f10}", ans.Real, ans.Imaginary); } }出力結果(-1, 1.22464679914735E-16) -1.0000000000 0.0000000000Goの場合
- $e$や$π$は、
math.E
やmath.Pi
として定義。- 複素数は、
complex
型が用意されている。- 複素数のべき乗は
cmplx.Pow
関数を利用できる。package main import ( "fmt" "math" "math/cmplx" ) func main() { x1 := complex(0, math.Pi) ans := cmplx.Pow(math.E, x1) fmt.Println(ans) fmt.Printf("%.10f %.10f", real(ans), imag(ans)) }出力結果(-1+1.2246467991473515e-16i) -1.0000000000 0.0000000000Perlの場合
- $e$や$π$は、定数定義されていない。
- 複素数は、
1.0 + 1.0 * i
といった形で利用できる。- べき乗は
**
演算子を利用できる。use Math::Complex; my $pi = 3.14159265358979323846; my $e = exp(1.0); my $x = 0.0 + $pi * i; my $ans = $e ** $x; print $ans, "\n"; printf "%.10f %.10f\n", Re($ans), Im($ans);出力結果-1+1.22464679914735e-16i -1.0000000000 0.0000000000Pythonの場合
- $e$や$π$は、
math.e
やmath.pi
として定義。- 複素数は、
complex
型が用意されている。- べき乗は
pow
関数を利用できる。import math x = complex(0, math.pi) ans = pow(math.e, x) print(ans) print("%.10f %.10f" % (ans.real, ans.imag))出力結果(-1+1.2246467991473532e-16j) -1.0000000000 0.0000000000Rubyの場合
- $e$や$π$は、
Math::E
やMath::PI
として定義。- 複素数は、
Complex
型が用意されている。- べき乗は
**
演算子を利用できる。x = Complex(0.0, Math::PI) ans = Math::E ** x puts(ans) printf("%.10f %.10f\n", ans.real, ans.imag)出力結果-1.0+0.0i -1.0000000000 0.0000000000Rubyでは、演算結果をそのまま出力しても誤差が表示されませんでした。
Haskellの場合
- $e$は、定数定義されていない。$π$は、
pi
として定義。- 複素数は、
1.0 :+ 1.0
といった形で利用できる。- べき乗は
**
演算子を利用できる。import Data.Complex import Text.Printf main = do let x = 0.0 :+ pi let e = exp 1.0 let ans = e ** x let re = realPart(ans) let im = imagPart(ans) print ans printf "%.10f %.10f\n" (re::Double) (im::Double)出力結果(-1.0) :+ 1.2246467991473532e-16 -1.0000000000 0.0000000000Juliaの場合
- $e$や$π$は、
ℯ
やπ
として定義。- 複素数は、
Complex
型が用意されている。
- 虚数単位
im
も定数として用意されている。- べき乗は
^
演算子を利用できる。using Printf x = π * im # == complex(0.0, π) ans = ℯ ^ x println(ans) @printf("%.10f %.10f\n", real(ans), imag(ans))出力結果-1.0 + 1.2246467991473532e-16im -1.0000000000 0.0000000000