20201127のGoに関する記事は6件です。

Testify を使った Go 言語で書かれたプログラムのテスト

ここ数か月ほど仕事で Go 言語を使ってプログラムを書いています。その中で、テストの記述が楽になる testify というライブラリを見つけました。この記事では、その使い方をチュートリアル形式でご紹介します。

テスト対象の HTTP サーバを作る

以下の仕様で HTTP サーバを実装する、というストーリーに沿って testify の機能を紹介します。

  • /probe にアクセスすると http://example.com にアクセスして、200 が返ってくるかどうかをチェックする
  • 上記に成功した場合、/probe のレスポンスは probe_success 1 となり、失敗した場合は probe_success 0 が返ってくる (Prometheus の exporter と同じ形式)

テストコード

まず testify なしで、標準ライブラリだけでテストを書きます。handleProbe がテスト対象の関数です。http.HandleFunc に渡せるようにシグネチャを合わせています。Go 言語には net/http パッケージのテストを書くためのヘルパとして net/http/httptest というパッケージが用意されているので、これを使いました。

main_test.go
package main

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

func Test_handleProbe(t *testing.T) {
    rec := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/probe", nil)

    handleProbe(rec, req)

    expectedCode := http.StatusOK
    if rec.Code != expectedCode {
        t.Errorf("unexpected status code, expected %d but got %d", expectedCode, rec.Code)
    }

    expectedBody := "probe_success 1\n"
    if rec.Body.String() != expectedBody {
        t.Errorf("unexpected body, expected %s but got %s", expectedBody, rec.Body.String())
    }
}

プロダクションコード

テストが通るようにプロダクションコードを実装していきます。handleProbe は以下のように実装しました。

main.go
func handleProbe(w http.ResponseWriter, req *http.Request) {
    var ret int
    if new(probe).Probe("http://example.com") {
        ret = 1
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(fmt.Sprintf("probe_success %d\n", ret)))
}

Probe メソッドで与えた URL にアクセスできるかどうかチェックします。

Probe は以下のように実装しました。

main.go
type Prober interface {
    Probe(string) bool
}

type probe struct{}

func (p *probe) Probe(url string) bool {
    res, err := http.Get(url)
    if err != nil {
        return false
    }

    return res.StatusCode == http.StatusOK
}

単に GET でアクセスして http.StatusOK (200) かどうかチェックしているだけです。 err が返ってきた場合はプロトコル的なエラー (URL が間違ってる、サーバの HTTP レスポンスが不正等) が起きていたり、接続がタイムアウトしたりしている訳ですが、今回は簡単のためこれらも接続できなかったという扱いで false にしています。

ここまで書くと go test がパスするようになりました。

assert を使ったテスト

assert.Equal

標準ライブラリだけでテストを書くと、山ほど if hoge { t.Errorf("fuga") } のようなコードを書くことになるのですが、assert パッケージを使うとすっきり書けます。

main_test.go
func Test_handleProbe(t *testing.T) {
    rec := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/probe", nil)

    handleProbe(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code) // *testing.T, 期待する値、実際の値 を順に渡す
    assert.Equal(t, "probe_success 1\n", rec.Body.String())
}

すっきりして読みやすくなったと思います。

テスト失敗時の出力

assert パッケージの良いところは、記述が簡潔になることだけではなく、失敗したアサーションが分かりやすく出力されるところにあります。試しに handleProbe を以下のように書き換えてテストしてみます。

main.go
func handleProbe(w http.ResponseWriter, req *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("probe_success 0\n")) // 常に 0 を返す
}
$ go test ./...
--- FAIL: Test_handleProbe (0.00s)
    main_test.go:18:
        Error Trace:    server_test.go:18
        Error:    Not equal:
                expected: "probe_success 1\n"
                actual  : "probe_success 0\n"

                Diff:
                --- Expected
                +++ Actual
                @@ -1,2 +1,2 @@
                -probe_success 1
                +probe_success 0

        Test:      Test_handleProbe
FAIL
FAIL    qiita-testify-example 0.004s
FAIL

このように、期待する結果と実際の結果、そして値の diff が表示されます。アサーションライブラリを使わないと自分でエラーメッセージを丁寧に書く必要がありますが、その手間が省けて便利です。

HTTP ハンドラ用のアサーション

testify には HTTP ハンドラに特化したアサーションメソッドもあるので、さらに短く書くこともできます。

main_test.go
func Test_handleProbe(t *testing.T) {
    assert.HTTPStatusCode(t, handleProbe, "GET", "/probe", nil, http.StatusOK)
    assert.HTTPBodyContains(t,  handleProbe, "GET", "/probe", nil, "probe_success 1\n")
}

mock を使ったテスト

ここまでに書いたテストは実際に http://example.com へとアクセスしています。そのため、インターネットに繋がらない環境でテストすると失敗します。また、 http://example.com が落ちるといった、開発者がコントロールできない要因でテストが失敗することもあります。こういった環境や外部システムへの依存をなくす手段がモックです。このプログラムにおいては、 probe 構造体がテスト時にモックで差し替えたいオブジェクトです。

プロダクションコードの修正

始めに、テストから probe を差し替えられるよう、パッケージ変数を定義し、handleProbe からパッケージ変数を参照するように変更します。

main.go
var defaultProbe Prober = new(probe)

func handleProbe(w http.ResponseWriter, req *http.Request) {
    var ret int
    if defaultProbe.Probe("http://example.com") {
        ret = 1
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(fmt.Sprintf("probe_success %d\n", ret)))
}

モックを使ったテストの記述

モックに使う mockProbe 構造体を新たに定義し、 mock.Mock を埋め込みます。mock.Mock にはモックオブジェクトに必要な機能がいろいろとあるのですが、今回はモックオブジェクトのメソッドが期待された形式で呼び出されていることのチェックと、そのメソッドの戻り値を指定する機能を使います。

main_test.go
import (
    // 省略
    "github.com/stretchr/testify/mock"
)

type mockProbe struct {
    mock.Mock
}

func (m *mockProbe) Probe(url string) bool {
    args := m.Called(url)
    return args.Bool(0)
}

func Test_handleProbe(t *testing.T) {
    m := &mockProbe{}
    m.On("Probe", "http://example.com").Return(true)
    defaultProbe = m

    assert.HTTPStatusCode(t, handleProbe, "GET", "/probe", nil, http.StatusOK)
    assert.HTTPBodyContains(t, handleProbe, "GET", "/probe", nil, "probe_success 1\n")
}

On メソッドに、テスト中に呼び出されるメソッド名とそのメソッドに渡される引数を与えます。今回の場合は Probe メソッドに "http://example.com" という文字列が渡されるはずなので、これらを指定しています。さらに、Return メソッドで Probe が返す戻り値を指定します。今回は戻り値が true 1つですが、複数ある場合は Return(false, fmt.Errorf("foobar")) のように複数の戻り値を指定できます。

モックを導入したことで Probe に失敗したケースもテストできるようになりました。テストケースを追加してみます。

main_test.go
func Test_handleProbe_success(t *testing.T) {
    m := new(mockProbe)
    m.On("Probe", "http://example.com").Return(true)
    defaultProbe = m

    assert.HTTPStatusCode(t, handleProbe, "GET", "/probe", nil, http.StatusOK)
    assert.HTTPBodyContains(t, handleProbe, "GET", "/probe", nil, "probe_success 1\n")
}

func Test_handleProbe_fail(t *testing.T) {
    m := &mockProbe{}
    m.On("Probe", "http://example.com").Return(false)
    defaultProbe = m

    assert.HTTPStatusCode(t, handleProbe, "GET", "/probe", nil, http.StatusOK)
    assert.HTTPBodyContains(t, handleProbe, "GET", "/probe", nil, "probe_success 0\n")
}

suite を使ったテスト

suite パッケージを使うとテストをグルーピングすることができます。これによって、パッケージよりも細かい単位 (テストスイート) でテストをまとめることができ、テストスイートごとに Setup, TearDown といったテストの前処理、後処理や、ヘルパ関数の定義ができるようになります。

テストスイートの定義

まず suite.Suite を組み込んだ構造体を定義します。この構造体にはテストスイート内で共有したいフィールドを定義できます。今回はモックオブジェクトを m として定義しました。

main_test.go
type ProbeSuite struct {
    suite.Suite
    m *mockProbe
}

SetupTest

ProbeSuite 構造体に対して SetupTest を定義して前処理を記述します。SetupTest はテスト1つ1つの実行直前に実行されます (RSpec をご存知の方は before(:each) をイメージしてください)。今回はここでモックオブジェクトを初期化します。

main_test.go
func (s *ProbeSuite) SetupTest() {
    s.m = new(mockProbe)
    defaultProbe = s.m
}

テストスイートに対するテスト定義

個々のテストで初期化したモックオブジェクトを使ってテスト対象を実行します。テストも先述の SetupTest と同じように ProbeSuite のメソッドとして定義します。また、引数の t *testing.T は不要となり、代わりにレシーバの s *ProbeSuite を使います。

main_test.go
func (s *ProbeSuite) TestSuccess() {
    s.m.On("Probe", "http://example.com").Return(true)

    s.HTTPStatusCode(handleProbe, "GET", "/probe", nil, http.StatusOK)
    s.HTTPBodyContains(handleProbe, "GET", "/probe", nil, "probe_success 1\n")
}

func (s *ProbeSuite) TestFail() {
    s.m.On("Probe", "http://example.com").Return(false)

    s.HTTPStatusCode(handleProbe, "GET", "/probe", nil, http.StatusOK)
    s.HTTPBodyContains(handleProbe, "GET", "/probe", nil, "probe_success 0\n")
}

テストスイートの実行

最後に、テストスイートを実行するために 通常の テストを書きます。これを忘れるとテストスイート全体が実行されなくなるので注意が必要です。

main_test.go
func TestProbeSuite(t *testing.T) {
    suite.Run(t, new(ProbeSuite))
}

まとめ

最終的に全ての機能を使うと以下のようなコードになりました。

プロダクションコード:

main.go
package main

import (
    "fmt"
    "log"
    "net/http"
)

var defaultProbe Prober = new(probe)

type Prober interface {
    Probe(string) bool
}

type probe struct{}

func (p *probe) Probe(url string) bool {
    res, err := http.Get(url)
    if err != nil {
        return false
    }

    return res.StatusCode == http.StatusOK
}

func handleProbe(w http.ResponseWriter, req *http.Request) {
    var ret int
    if defaultProbe.Probe("http://example.com") {
        ret = 1
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(fmt.Sprintf("probe_success %d\n", ret)))
}

func main() {
    http.HandleFunc("/probe", handleProbe)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

テストコード:

main_test.go
package main

import (
    "net/http"
    "testing"

    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/suite"
)

type mockProbe struct {
    mock.Mock
}

func (m *mockProbe) Probe(url string) bool {
    args := m.Called(url)
    return args.Bool(0)
}

type ProbeSuite struct {
    suite.Suite
    m *mockProbe
}

func (s *ProbeSuite) SetupTest() {
    s.m = new(mockProbe)
    defaultProbe = s.m
}

func (s *ProbeSuite) TestSuccess() {
    s.m.On("Probe", "http://example.com").Return(true)

    s.HTTPStatusCode(handleProbe, "GET", "/probe", nil, http.StatusOK)
    s.HTTPBodyContains(handleProbe, "GET", "/probe", nil, "probe_success 1\n")
}

func (s *ProbeSuite) TestFail() {
    s.m.On("Probe", "http://example.com").Return(false)

    s.HTTPStatusCode(handleProbe, "GET", "/probe", nil, http.StatusOK)
    s.HTTPBodyContains(handleProbe, "GET", "/probe", nil, "probe_success 0\n")
}

func TestProbeSuite(t *testing.T) {
    suite.Run(t, new(ProbeSuite))
}

今回は基本的な機能を一通り紹介するために全パッケージを使いましたが、assert を導入するだけでも十分便利だと思います。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangのテストで関数名に日本語を使ってわかりやすくする方法

標準テストの場合

package hoge_test

import (
    "reflect"
    "runtime"
    "strings"
    "testing"
)

func Test(t *testing.T) {
    t.Run("足し算テスト", func(t *testing.T) {
        Run(t, 卍1プラス1イコール2)
    })
}

// Run [標準のテスト] 関数名を説明文として設定する処理
func Run(t *testing.T, f func(*testing.T)) {
    fv := reflect.ValueOf(f)
    funcFullName := runtime.FuncForPC(fv.Pointer()).Name()
    list := strings.Split(funcFullName, ".")
    funcName := list[len(list)-1]
    t.Run(funcName, f)
}

// テスト本体 先頭の卍は、数字全角が先頭にくるとコンパイルエラーとなるための苦し紛れ
func 卍1プラス1イコール2(t *testing.T) {
    actual := 1 + 2
    expected := 2
    if actual != expected {
        t.Errorf("actual=%d, expected=%d", actual, expected)
    }
}

実行結果
--- FAIL: Test (0.00s)
    --- FAIL: Test/足し算テスト (0.00s)
        --- FAIL: Test/足し算テスト/卍1プラス1イコール2 (0.00s)
            hoge_test.go:33: actual=3, expected=2
FAIL

テストライブラリ:goblinを使った場合

package hoge_test

import (
    "reflect"
    "runtime"
    "strings"
    "testing"

    "github.com/franela/goblin"
    . "github.com/franela/goblin"
)

func Test(t *testing.T) {
    g := Goblin(t)
    g.Describe("足し算テスト", func() {
        RunG(g, 卍1プラス1イコール2)
    })
}

// RunG [goblinテスト] 関数名を説明文として設定する処理
func RunG(g *goblin.G, f func(g *goblin.G)) {
    fv := reflect.ValueOf(f)
    funcFullName := runtime.FuncForPC(fv.Pointer()).Name()
    list := strings.Split(funcFullName, ".")
    funcName := list[len(list)-1]
    g.It(funcName, func() {
        f(g)
    })
}

// テスト本体 先頭の卍は、数字全角が先頭にくるとコンパイルエラーとなるための苦し紛れ
func 卍1プラス1イコール2(g *goblin.G) {
    actual := 1 + 2
    expected := 2
    g.Assert(actual).Equal(expected)
}

実行結果
=== RUN   Test

  足し算テスト
    1) 卍1プラス1イコール2


 0 tests complete (0 ms)
 1 tests failed: 

  1) 足し算テスト 卍1プラス1イコール2:

    3 does not equal 2
        /home/daijin/go/pkg/mod/github.com/franela/goblin@v0.0.0-20200825194134-80c0062ed6cd/assertions.go:67 +0x197
        /home/daijin/src/testp/hoge_test.go:61 +0x9e
        /home/daijin/src/testp/hoge_test.go:53 +0x2e
        /home/daijin/go/pkg/mod/github.com/franela/goblin@v0.0.0-20200825194134-80c0062ed6cd/goblin.go:228 +0x24
        /home/daijin/go/pkg/mod/github.com/franela/goblin@v0.0.0-20200825194134-80c0062ed6cd/goblin.go:351 +0x7f
        /home/daijin/go/pkg/mod/github.com/franela/goblin@v0.0.0-20200825194134-80c0062ed6cd/goblin.go:228 +0x7b
        /home/daijin/go/pkg/mod/github.com/franela/goblin@v0.0.0-20200825194134-80c0062ed6cd/goblin.go:225 +0x371
--- FAIL: Test (0.00s)

上記のようにはせず、普通に書く場合、タイトルと関数名の同じものを並んで2個書くことになり冗長。

日本語関数名だけセットすれば、それがタイトルになると良いなぁという感じで書きました。

githubにでも上げるか?とかおもったのですが、コード量少ないので、使うのであれば、テストのユーティリティにでも入れてもらえばと思います。

終わりに一言

TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング

twadaさんのテスト駆動開発ライブコーディングで、Javaだと日本語でテスト書けていてうらやまだったので、Golangでも同じように出来ないか?頑張ってみたという感じでした。

先頭が数字から始まるとコンパイルエラーとなります。

Javaだとアンダーバーでしたが、Golangだと無理だったので適当な漢字(卍)を使ってみました。

余談ですが、シャープっぽく見える井口さんの井の字も候補でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golangCI、Code Climateで品質管理を行った話

こんにちは
最近アプリケーションエンジニアと言いつつ、サーバーだらけ触ってるキムです。
今回はPJの品質管理と、今回品質管理で投入したツールの設定と、使い方について共有しようと思ってます。

入る前に

  • golangciとcode climateを利用した経験になります。
  • 詳細設定は各自PJの状況に合わせて調整したほうが良いです。
  • 無理してプッシュするよりチームメンバーが納得できる範囲内で妥協することと、メンバーとの認識合わせを通じて、メンバー自ら努力するように環境を作るのが大事だと思います。
  • golangciはGo言語に特化されてるツールです。言語ごとに色んなツールが存在するので、Go言語以外の言語を使う方はそれぞれ調べtください。
  • code climate以外にもウェブを利用して、PJレポジトリを管理してくれるサービスはいくつかあります。こちらも調べてみてください。

品質管理てなんでするのか

品質管理はこんな意味を持ってます。
皆さんはコードの品質を管理してますか??
ユーザーに提供してるアプリケーションの品質を管理するのはものすごく重要なものです。 品質をちゃんと管理してないていうのは比較すると、 普段我々が使ってる色んな商品が何も品質保証無しで我々提供してると同じ意味合いになります。 そんな商品だと、皆さんは安心して購入できるでしょうか?
もちろんサービスをリリースする前には様々なテストを実装してると思います。これを専門的に行う人々をQAエンジニアと様な名前で読んでますし、彼らの存在は単純にリリース前のテストを行う人々ていう意味からはじめ、様々な役割を果たしてます。

image.png
この図は一番簡単に書いた、サービスがリリースされる前までのプロセスになります。省略された部分もあると思いますが、大きく見るとほとんどこのパターンで進めてると思います。
上記で話した、QAエンジニアが活躍するところは③の周りになります。
この記事で話したいのは図の①と②になります。
①はGolangCiの部分です、 ②はCodeClimate部分になります。
高いレベルのテストに入る前にコードレベルでの品質管理を厳しく行うことで、QAエンジニアの負担を減らせるし(簡単な動作バグや修正のデグレはまじで疲れますね。)、自分たちのコード品質に自身を持つことができ、コードを管理する基準を作ることでチーム員たちが共通の認識を持つことができます。

golangci について

Goにはbuilt-inで提供してるPackageではfmtを含めていくつか有用なLintを提供してます。
このようなツールは単純にコーディングミスを探すことから、コードのインデントを整理したり(fmt)、不要なImportを消してくれたり(goimport)、変数や・関数を定義したけど使ってないことを教えてくれたりして、よりコードをきれいに管理できるように手伝ってくれます。
その中に、Golangを使う場合一番Publicなものがgolangciていうツールであります。 このツールは簡単なCiからはじめ、いろんなLinterを組み合わせて利用することで、プロジェクトをより簡単に管理してくれます。
提供してくれるLinterもこんなに多いので、どんな役割をするのか確認しながら、本人のPJで使えそうなものを投入しましょう。

code climateにていて

code climateは私達が、基本的にCLIベースでチェックしてたことをウェブ画面で、数値化されてより理解しやすいようにやってくれるし、PJが持ってる潜在的なIssueを事前に対応できるように、表示してくれたりするUI/UXを提供してくれるサービスであります。
独自的な計算アルゴリズムを利用して、PJのメンテナンス数値や、該当Issuesを対応することにかかる予想時間を表示してくれたりします。
更にGithubとの連携を利用して簡単にCodeClimateのIssueをGithubのIssuesにしてわかりやすくしたり、PRにコメントを作成してくれるので、いろんなワークフローが作れます。

ざっくり見る全体図

image.png

事前準備

  • CodeClimateアカウントを持ってるのか
  • Code Climateと、Githubのレポジトリを連動させる。

作業の流れ

  1. Localで作業する
  2. GitにPushする
  3. Github Actionsが走ってgolangci-lint チェック、coverage reportを作成
  4. 作成したレポートをCode Climateに転送
  5. Code ClimateでPJの状態を確認

カスタム設定について

golangci

公式ドキュメントを見ると、PJのRootに下記4パターンのフォーマットに設定ファイルを準備することで、ルールをカスタマイズするのが可能になります。

  • golangci.yml
  • golangci.yaml
  • golangci.toml
  • golangci.json
## golangci.yml
## code climate 側でチェックしてることと一番関係高い部分をカスタムで入れてます。
linters-settings:
  nestif:
    # minimal complexity of if statements to report, 5 by default
    min-complexity: 4
  gocognit:
    # minimal code complexity to report, 30 by default (but we recommend 10-20)
    min-complexity: 20
  gofmt:
    # simplify code: gofmt with `-s` option, true by default
    simplify: true
  nakedret:
    # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
    max-func-lines: 100

issues:
  # Excluding configuration per-path, per-linter, per-text and per-source
  # テストコードの場合除外する。
  exclude-rules:
    - path: _test\.go
      linters:
        - errcheck
        - gocognit
        - nestif
        - gofmt
linters:
  # 利用するLinter
  enable:
    - nestif
    - gocognit
    - gofmt
    - nakedret
  fast: false

そして、このページをみると、普段提供してるDefaultのLinter以外に色んなLinterを提供してるので、この設定を追加して利用できます。

code climate

同じく公式ドキュメントを見ると会のフォーマットでチェック内容をカスタマイズするのができます。
このファイルがない場合、Defaultの値でチェックを行うし、簡単にチェックのOn/Offぐらいなら、設定ファイルを準備するより、ウェブページを利用するのがより簡単にできる。

  • .codeclimate.yml
  • .codeclimate.json

このページを見ると、各項目ごとにどんな内容をチェックするのかが、確認でき・設定するのができます。

version: "2"         # required to adjust maintainability checks
checks:
  argument-count:    # parameter count
    config:
      threshold: 4
  complex-logic:
    config:
      threshold: 15
  file-lines:
    config:
      threshold: 500
  method-complexity:
    config:
      threshold: 15
  method-count:
    config:
      threshold: 20
  method-lines:
    config:
      threshold: 50
  nested-control-flow:
    config:
      threshold: 4
  return-statements:
    config:
      threshold: 4
  similar-code:
    config:
      threshold: # language-specific defaults. an override will affect all languages.
  identical-code:
    config:
      threshold: # language-specific defaults. an override will affect all languages.
# 管理画面で除外するパターン
exclude_patterns:
  - "config/"
  - "db/"
  - "dist/"
  - "features/"
  - "**/node_modules/"
  - "script/"
  - "**/spec/"
  - "**/test/"
  - "**/tests/"
  - "**/vendor/"
  - "**/*.d.ts"
  - "**/*_mock.go"

ちょっと気になる項目がありますよね? おそらく下記2項目かと思います。 

complex-logic:      ## ロジックの複雑さ
    config:
      threshold: 4
method-complexity:  ## メソッドの複雑さ
  config:
    threshold: 5

他の項目はなんとなくわかるが、この項目だけはなんか気になりますね。
どんな内容をチェックするのか…
公式ドキュメントを見ると、CodeClimate側では2つの軸で、複雑さを計算してるといいます。
- Cognitive Complexity
- Cyclomatic Complexity

本当にざっくり説明すると Cognitive Complexityは「人間が理解するのに問題ないのか」、をチェックすることです。 そして、Cyclomatic Complexityは「マシンが理解するのに問題ないのか」をチェックする方法です。 
もし、より深く理解したいのであれば、こちらを読んでみてください。
そして、Qiitaではこのページでかならい優しく説明してくれてます。 おすすめです。

そして下記の2項目もまります。

similar-code:   ## コードのかぶり
  config:
    threshold: # language-specific defaults. an override will affect all languages.
identical-code: ## 処理のかぶり
  config:
    threshold: # language-specific defaults. an override will affect all languages.

それぞれかぶりをチェックする項目であります。 基本的に各言語の設定に頼ってます。
簡単に説明すると、

func test1(a, b) int {
    a = a + 1
    return a + b
}

func test2(a, b) int {
    a = a * 3
    return a + b
}

ほんとにざっくり書きましたが、話してる内容は似てると思います。
similarは、コード自体の重複ですね。 コードを書く時になんとなく名前だけ微妙に異なって、ほぼ同じぐらいのコードがあります。 このような場合検知され、Issuesになります。
identicalはやってる処理がほぼ似てる場合になります。
Aを取得して、Bを処理、 Cを取得して、Bを処理 のように名前だけ微妙に異なって、処理はほぼ同じ場合、このようなものがあるから、Refactorしたほうがいいよ的な案内を出してくれるものです。
基準が言語に依存されてるので、自分もよくわかりません… もし、関連情報があれば教えていただくと助かります。

まとめ、

この作業を行いながら、コード品質管理について色々勉強する機械になりました。
単純に「テストコードを書いて、カバレッジを達成する」的なエンジニアとして活動してきましたが、このプロセスと投入しながら自ら品質てなんで管理しなきゃいけないのかとかあれこれ考えるチャンスになりました。
そして、このような指標を設定して、チーム員たちの目標を設定して、より高いチームワークを持つことができる機械になると思います。
ぜひ、みなさんもコード品質について深く考えて皆さんのPJにも投入して見てください。
まだ内容も足りないし、きれいじゃない記事ですが、読んで頂いてありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAssemblyを使ってBackend(Golang)のバリデーションロジックをFrontend(Nuxt.js)のバリデーションに流用する試み

お題

ここ数年、FrontendにVue.js/Nuxt.js、BackendにGolang、間をREST-APIないしGraphQLで繋ぐという構成で開発している。
この構成でよく感じるのが、「フロントでもバックでも同じバリデーションロジックを両方で実装してるなぁ」ということ。
バリデーションと言っても、FrontendとBackendとでは厳密にはチェックすべき内容が違うのだけど、型・桁とかフォーマットとか結局どちらでも同じようなロジックを実装してたりする。
実際のところは、Frontend、Backend双方に便利なライブラリがあったりして、それほど開発負荷がかかっているわけではないのだけど、何が嫌って「同じ仕様を複数箇所で実装」していること。
そして、時々、同じ仕様のはずなのにFrontendとBackendとで微妙にバリデーション内容が違っていることが発生すること。

なんとかならないんだろうかと思っていたところに、WebAssemblyの記事を読んだことで、にわかに思いつき実装をしてみることにした。
BackendではGolangでバリデーションロジックを流用しやすい単位で関数化しておきBackendのバリデーションにはもちろんその関数を呼び出す。
そして、その関数をWebAssemblyとして使える形に変換(正直、WebAssemblyの技術仕様はまだよくわかっていない)してFrontendでも同じロジックを呼び出せるようにする。
そうしたら、同じ仕様を2箇所で実装することはなくなるんじゃないだろうか?

ちなみに、そもそも「バリデーションとは?」の定義問題や、どこで実装すべきという思想などは、この記事では触れない。

前提

以下に関して初心者向けな懇切丁寧な解説はしてないです。
・Vus.js/Nuxt.js/TypeScript/vee-validate
・Golang/gqlgen
・GraphQL
・WebAssembly/wasm

開発環境

# OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"

# バックエンド

# 言語 - Golang

$ go version
go version go1.15.2 linux/amd64

IDE - Goland

GoLand 2020.2.3
Build #GO-202.7319.61, built on September 16, 2020

今回の全ソース

https://github.com/sky0621/try-wasm/tree/v0.1.0/try02

実践

Backend

まずはBackendのバリデーション実装から。
Frontendとの繋ぎは、たまたまGraphQLを使っているけど、今回の趣旨としてはGraphQLでもREST-APIでも何でも関係ない。

tree

$ tree
.
├── cmd
│   └── server.go
├── domain
│   ├── error.go
│   └── todo.go
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── models_gen.go
├── resolver.go
├── schema.resolvers.go
├── scripts
│   └── wasm.sh
└── wasm
    └── main.go

main関数

cmd/server.go
package main

import (
    "log"
    "net/http"
    "os"

    app "github.com/sky0621/try-wasm/try02/backend"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
)

func main() {
    srv := handler.NewDefaultServer(app.NewExecutableSchema(app.Config{Resolvers: &app.Resolver{}}))

    http.Handle("/pg", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", srv)

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Resolver

今回の題材としては「TODOを登録」する機能としている。で、その(GraphQLの)リクエストがFrontendから飛んでくると発火するのがこの部分。
TODOを登録するロジック本体は domain パッケージの CreateTodo(~~) に任せる。
その結果、エラーが発生(今回はバリデーションエラーのみ想定)したら、そのエラーをGraphQL的なエラーの返し方でFrontendに返す。
ただし、通常、ここでエラーが返ることはない。
なぜなら、同じチェックをFrontendでもしている(はず)から。
Backendでこのチェックが必要なのは、Frontendを介さず外部から直接コールされることも想定(例えばそういう要件とする)するため。

schema.resolvers.go
package app

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
    "context"
    "fmt"

    "github.com/99designs/gqlgen/graphql"
    "github.com/vektah/gqlparser/v2/gqlerror"

    "github.com/sky0621/try-wasm/try02/backend/domain"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (*Todo, error) {
    todo, errs := domain.CreateTodo(domain.Todo{
        Text:   input.Text,
        UserID: input.UserID,
    })
    if errs != nil {
        for _, e := range errs {
            graphql.AddError(ctx, &gqlerror.Error{
                Message: "VALIDATION_FAILURE",
                Extensions: map[string]interface{}{
                    "status_code":   400,
                    "error_message": e.Err.Error(),
                    "field":         e.Field,
                    "value":         e.Value,
                },
            })
        }
        return nil, nil
    }

    return &Todo{
        ID:   "newID",
        Text: todo.Text,
        Done: false,
        User: &User{
            ID:   todo.UserID,
            Name: "user1",
        },
    }, nil
}

〜〜省略〜〜

domainロジック

内容はソース見ればわかるということで。
今回の肝は、バリデーションに特化した部分を ValidateTodoText(text) 関数に逃がしている点。
この関数をWebAssemblyとしても使う。

domain/todo.go
package domain

import (
    "errors"
    "unicode/utf8"
)

type Todo struct {
    Text   string `json:"text"`
    UserID string `json:"userId"`
}

func CreateTodo(todo Todo) (*Todo, []*Error) {
    /*
     * バリデーション
     */
    var domainErrors []*Error

    // ToDoテキストのバリデーション
    err := ValidateTodoText(todo.Text)
    if err != nil {
        domainErrors = append(domainErrors, err)
    }

    // MEMO: ユーザーIDのバリデーション
    // 今回はバリデーション1事例あれば十分なので省略。

    if domainErrors != nil {
        return nil, domainErrors
    }

    /*
     * MEMO: 何かしらの登録処理を行った後、結果を返却!
     * 今回の趣旨ではないので省略。
     */
    return &Todo{
        Text:   todo.Text,
        UserID: todo.UserID,
    }, nil
}

func ValidateTodoText(text string) *Error {
    // MEMO: 必須チェックを入れてみる。
    if text == "" {
        return &Error{Field: "text", Value: "nil", Err: errors.New("required")}
    }

    // MEMO: 文字列長チェックを入れてみる。
    cnt := utf8.RuneCountInString(text)
    if cnt < 4 {
        return &Error{Field: "text", Value: text, Err: errors.New("min")}
    }
    if cnt > 10 {
        return &Error{Field: "text", Value: text, Err: errors.New("max")}
    }
    return nil
}

動作確認

screenshot-localhost_8080-2020.11.27-01_03_25.png

WebAssembly

GoでのWebAssemblyファイルの作り方等は下記参照。
https://github.com/golang/go/wiki/WebAssembly#getting-started

とりあえず、以下のようなmain関数を用意して、先述の domain.ValidateTodoText(text)validateTodoText という名前で呼び出せるようにする。

wasm/main.go
package main

import (
    "syscall/js"

    "github.com/sky0621/try-wasm/try02/backend/domain"
)

func main() {
    done := make(chan struct{}, 0)
    global := js.Global()
    global.Set("validateTodoText", js.FuncOf(validateTodoText))
    <-done
}

func validateTodoText(this js.Value, args []js.Value) interface{} {
    if len(args) != 1 {
        return "ERROR: number of arguments doesn't match"
    }

    if res := domain.ValidateTodoText(args[0].String()); res != nil {
        return res.Err.Error()
    }
    return nil
}

wasmというファイル形式(?)に変換するコマンドは以下のようにShellにしておく。
作成したファイルの出力先は、Frontendから参照できる場所にしておく。

scripts/wasm.sh
#!/usr/bin/env bash
set -euox pipefail
SCRIPT_DIR=$(dirname "$0")
# shellcheck disable=SC2086
echo ${SCRIPT_DIR}
cd "${SCRIPT_DIR}" && cd ../wasm

GOOS=js GOARCH=wasm go build -o ../../frontend/static/go.wasm

Frontend

create-nuxt-appを使ってプロジェクト作成後、GraphQLやTypeScriptの設定を追加した状態。
staticディレクトリの下にBackendで生成したgo.wasmバイナリが置かれている。

tree

今回の試みに関係ない部分は極力省略している。

$ tree
.
├── codegen.yml
├── components
│   └── TodoForm.vue
├── gql-types.d.ts
├── layouts
│   ├── default.vue
│   └── error.vue
├── nuxt.config.js
├── package.json
├── pages
│   └── index.vue
├── plugins
│   └── vee-validate.ts
├── static
│   ├── go.wasm
│   ├── init_wasm.js
│   └── wasm_exec.js
├── tsconfig.json
└── yarn.lock

layout~page~component

今回の趣旨ではないのでFrontendの各ファイルの内容はさらりと。
見た目は下記。
screenshot-localhost_3000-2020.11.27-01_09_31.png

layouts/default.vue
<template>
  <v-app>
    <nuxt />
  </v-app>
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

@Component({})
export default class DefaultLayout extends Vue {}
</script>
pages/index.vue
<template>
  <TodoForm />
</template>

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import TodoForm from '~/components/TodoForm.vue'
@Component({
  components: { TodoForm },
})
export default class IndexPage extends Vue {}
</script>
components/TodoForm.vue
<template>
  <v-container>
    <ValidationObserver
      ref="observer"
      v-slot="{ invalid }"
      @submit.prevent="submit"
      slim
    >
      <v-form>
        <v-row>
          <v-col cols="5">
            <v-card>
              <ValidationProvider
                v-slot="{ errors }"
                name="TODO"
                rules="todoText"
              >
                <v-text-field
                  v-model="todoInput.text"
                  :error-messages="errors[0]"
                  label="TODO"></v-text-field>
              </ValidationProvider>
            </v-card>
          </v-col>
        </v-row>
        <v-row>
          <v-col cols="5">
            <v-card>
              <ValidationProvider
                v-slot="{ errors }"
                name="ユーザーID"
                rules="required"
              >
                <v-text-field
                  v-model="todoInput.userId"
                  :error-messages="errors[0]"
                  label="ユーザーID"></v-text-field>
              </ValidationProvider>
            </v-card>
          </v-col>
        </v-row>
        <v-row>
          <v-col cols="5">
            <v-btn @click="submit">登録</v-btn>
          </v-col>
        </v-row>
      </v-form>
    </ValidationObserver>
  </v-container>
</template>
<script lang="ts">
import { Component, Vue } from '~/node_modules/nuxt-property-decorator'
import { ValidationObserver } from 'vee-validate'
import { NewTodo } from '~/gql-types'

export class TodoInput implements NewTodo {
  text: string = ''
  userId: string = ''
}

@Component({})
export default class TodoForm extends Vue {
  $refs!: {
    observer: InstanceType<typeof ValidationObserver>
  }

  todoInput: NewTodo = new TodoInput()

  async submit() {
    console.log('Submit!')
    const valid = await this.$refs.observer.validate()
    if (valid) {
      console.log('valid!!!')
      // MEMO: GraphQLを介してバックエンドに登録内容を送信。(今回の検証範囲外なので省略)
    }
  }
}
</script>

バリデーションには vee-validate というライブラリを使っている。
今回で言うと、rules="todoText" としている箇所は「todoText」という独自ルールを当て込むという意味。
そのルールに合致しない入力の場合はバリデーションエラーとしてエラーメッセージが画面に表示される。
例えば、こう。
screenshot-localhost_3000-2020.11.27-01_12_20.png

バリデーション独自ルール

以下で独自ルール「todoText に割り当てたチェック関数内で使っている validateTodoText(value) がBackendのバリデーションロジックをWebAssemblyファイル化したものから呼び出した関数。
これにより、Frontendで「 if (value === '') { 」とか「 if (value.length < 4) { 」とかBackendと同じバリデーションロジックを書かなくて済む。

plugins/vee-validate.ts
import Vue from 'vue'
import {
  extend,
  ValidationObserver,
  ValidationProvider
} from 'vee-validate'
import { required } from 'vee-validate/dist/rules'

Vue.component('ValidationProvider', ValidationProvider)
Vue.component('ValidationObserver', ValidationObserver)

extend('todoText', {
  validate: (value: string) => {
    // @ts-ignore
    const res = validateTodoText(value)
    console.log(res)
    if (res && res !== '') {
      return false
    }
    return true
  },
  message() {
    return 'TODOは4文字以上、10文字以内です。'
  }
})
extend('required', required)

wasm呼び出し関数

記載内容はお約束コードになっている。
以下でBackend起因のwasmファイルを読み込んで使えるようになる。

static/init_wasm.js
const go = new Go(); // Defined in wasm_exec.js
const WASM_URL = 'go.wasm';

let wasm;

if ('instantiateStreaming' in WebAssembly) {
  WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
    wasm = obj.instance;
    go.run(wasm);
  })
} else {
  fetch(WASM_URL).then(resp =>
    resp.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
      wasm = obj.instance;
      go.run(wasm);
    })
  )
}

nuxt.config.js

以下のように script として「wasm_exec.js」と「init_wasm.js」が読み込まれるようにしておく。

nuxt.config.js(抜粋)
export default {
  // Global page headers (https://go.nuxtjs.dev/config-head)
  head: {
    titleTemplate: '%s - frontend',
    title: 'frontend',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    script: [
      { src: 'wasm_exec.js' },
      { src: 'init_wasm.js' },
    ],
  },
 〜〜省略〜〜

wasmファイルDLにかかるコスト

Backendのバリデーションロジックを含むwasmファイルなのだけど、1.4MB あって、TODO入力画面を開こうとしてから5秒弱もかかっている。
wasmのビルドに使うGoのランタイムを tinygo に変えると数KB〜数10KBぐらいに出来るようだけど、そうすると今度は関数内外での値の受け渡し(syscall/jsが担う部分)を行う部分が機能的に対応してないのか、ビルドに失敗する。
このあたりは諦めて掘り下げてないけど、もう少し調査してみるかな。さすがに5秒弱はプロダクトとしては使えない。。。
Screenshot at 2020-11-27 01-27-13.png

まとめ

一応、機能としては実現できた。ただ、go.wasmのDLに5秒とかかかる現状では、そのままプロダクトとして扱うのは厳しいか。
syscall/jsを使いつつもwasmの軽量化、これが実現できれば(それでも時期尚早かもしれないけど)採用もありか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでstructをmapに変換する

はじめに

似たような内容の記事はあったのですが、バージョンが変わったせいかうまく動かなくて苦労したので記事にしました。

JSONを使うバージョン

var result map[string]interface{}
indirect, _ := json.Marshal(data)
json.Unmarshal(indirect, &result)

// 値がなかったら、キーの削除
for key, value := range result {
    if value == nil {
        delete(result, key)
    }
}

これだと、time.Time型がStringになってしまい、僕の用途には合いませんでした。。

reflectを使うバージョン

result := make(map[string]interface{})

v := reflect.Indirect(reflect.ValueOf(data))
t := v.Type()
for i := 0; i < t.NumField(); i++ {
    ft := t.Field(i)
    fv := v.FieldByName(ft.Name)

    // nullの場合のみセット
    if !fv.IsNil() {
        result[ft.Name] = reflect.Indirect(reflect.ValueOf(fv.Interface()))
    }
}

reflect.Indirectは、fv.Interface()の値がポインタだった場合にそれを外す処理です。
ポインタのままmapに入れたい!って方にはいらないです。

参考文献

https://qiita.com/keitaj/items/440a50a53c8980ee338f
https://qiita.com/chimatter/items/b0879401d6666589ab71
https://qiita.com/nirasan/items/b6b89f8c61c35b563e8c

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCodeに毎日挑戦してみた 28. Implement strStr()(Python、Go)

はじめに

無料英単語サイトE-tanを運営中の@ishishowです。

プログラマとしての能力を上げるために毎日leetcodeに取り組み、自分なりの解き方を挙げていきたいと思います。

Leetcodeとは

leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。

golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)

10問目(問題28)

28. Implement strStr()

  • 問題内容

Implement strStr().

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Clarification:

What should we return when needle is an empty string? This is a great question to ask during an interview.

For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C's strstr() and Java's indexOf().

**

Example 1:

  Input: haystack = "hello", needle = "ll"
  Output: 2

Example 2:

  Input: haystack = "aaaaa", needle = "bba"
  Output: -1

Example 3:

  Input: haystack = "", needle = ""
  Output: 0

考え方

  1. haystack - needle回ループを回します ← それ以上する必要がないため

  2. 一文字ごとに判定して違ったらその判定処理を抜けます

  3. needle の文字数と一致したらその時のindexを返します

  • 解答コード
def strStr(self, haystack, needle):
    if needle == "":
        return 0
    for i in range(len(haystack)-len(needle)+1):
        for j in range(len(needle)):
            if haystack[i+j] != needle[j]:
                break
            if j == len(needle)-1:
                return i
    return -1
  • Goでも書いてみます!
func strStr(haystack string, needle string) int {
    for i := 0; i < len(haystack)-len(needle)+1; i++ {
        j := 0
        for ; j < len(needle); j++ {
            if needle[j] != haystack[i+j] {
                break
            }
        }
        if j == len(needle) {
            return i
        }
    }
    return -1
}

別解

​ (1)

class Solution(object):
    def strStr(self, haystack, needle):        
        return haystack.find(needle)

findを使うとstrstr同様にその単語のindexを表示してくれます

(2)

class Solution(object):
def strStr(self, haystack, needle):
    for i in range(len(haystack) - len(needle)+1):
        if haystack[i:i+len(needle)] == needle:
            return i
    return -1

こちらは文字列の長さで比較しています。私は最初こちらを作成しました。

(3)

import "strings"

func strStr(haystack string, needle string) int {
    return strings.Index(haystack, needle)
}

goのfind版です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む