20191011のGoに関する記事は5件です。

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

ソース読んでみたところ

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:8081

3. 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.158s

GitLab 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 ./...
  1. services に Cloud Datastore Emulator のコンテナを指定
    • ローカルでテストするときは --host-port=localhost:8081 とか指定してもいいけど、それだとコンテナ外部から通信できないので --host-port=0.0.0.0:8081 にする
  2. 環境変数に DATASTORE_PROJECT_ID, DATASTORE_PROJECT_ID を追加する
    • DATASTORE_PROJECT_IDservices で指定したエイリアスをホスト名にする
  3. エミュレーターがちゃんと起動してからテストしたいので dockerize で待つ
  4. あとはテストするだけ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GOLANGを勉強しはじめた

おひさしぶり 皆、

先週からGOLANGを勉強しはじめました。

UDEMY  で コースを 買って、毎日 1 ヴィデオ 見ていった。

今週で、3回 ホームページのデザイン はじめた。

NUXT.JS とTAILWINDCSSを けいかくしています。

少し NODE.JS わかります、

来週も、GOLANGビデオを見続けます,

コード例をとこしてみます。

またね.

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

東京高専プロコンゼミ総勢32名の名刺をHTMLで刷った

第30回高専プロコン、突如出現した移動部門で盛り上がっていますね。
私は今回自宅待機を選んだので酷い目に合わずほっとしているのですが、よく考えると参戦勢よりも関東自宅待機勢の方が酷い目に合うのでは?と思ってるところです1

さて、プロコンといえば「名刺クエスト」。高専生同士が名刺を交換し、交換した名刺の数で勝敗を決めるという、プロコンの中でも最もフェアな部門です。

東京高専の名刺のデザインは一昨年から私が担当していました。友人のMacに入っているCreative Cloudのイラレを使ってデザイン・印刷を行っていましたが、今年は彼が貸してくれないので、HTMLとCSSで書くことにしました。

この記事を参考にしました。
paper.cssを使ってブラウザだけで名刺を印刷する
https://qiita.com/okoppe8/items/abcafdad3a894bca7f38

名刺をデザインする

名刺のデザインをどうしようかな~と考えてた時に、ちょうどポケットの中に入ってたのは定期券でした。

IC定期券風名刺を刷れば面白いんじゃないか、と思いました。東京高専ですし、緑色なのはまだしもピンク色のは東京の象徴と言えます。
また、通生が比較的多い(多分)東京高専の象徴とも言えます。

そこでこんなデザインを作りました。

スクリーンショット 2019-10-11 13.26.13.png

また、一応緑色風と真面目なデザインも作っておきました。

カードの外形

91mm×55mmなので、こんな感じに指定しています。

meishi.css
card {
  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されるので、何もしなければ変なマージンが空いてしまいます。

スクリーンショット 2019-10-11 13.56.26.png
この画像の赤線の部分です。

transform: matrixを使って平行移動も噛ませることでなんとかしましたが、どうにかならないのかな、と思う仕様でした。

また、矢印はfontawesomeで実装しています。

名刺のPDF生成

名刺は一度10枚単位の名刺用紙に刷り、そこから切り取り線に沿って切り取るという方式です。
そのため、名刺用紙に刷るPDFが必要でした。

今回取ったフローとしては、以下の通りです。

  1. Google FormでSpreadsheet上にデータを集める
  2. Spreadsheet上のデータをクリップボード経由でExcelに出し、CSVで出力

それが完了すれば、次のようなコードで自動的にPDFに変換できます。

  1. CSV読み込み
  2. データに対し以下を繰り返す。
    1. 対象データをテンプレートエンジンに掛けて、HTMLを生成
    2. そのHTMLをHTTPサーバーで配信
    3. google-chromeのヘッドレスモードを使い、--print-to-pdfでPDF生成
    4. HTTPサーバーを閉じる

今思えばHTTPサーバーでまとめて配信すれば良かったのですが、プログラムの複雑性の問題や時間がなく適当に書き殴りたかったのでforループ一個で済むこの方式になりました。

まず予めHTMLのテンプレートを作成しました。上のHTMLとCSSをコピーしてきて、名前欄やID欄などをGolangのhtml/template方式で変数展開に書き換えた後、JavaScriptで10枚に増やす処理を追加しました。
するとこんな感じになりました

後はこれをHTTPサーバーで配信して、ChromeのheadlessモードでPDF生成すればPDFの出来上がりで、このPDFをサイズ100%で刷れば名刺が完成します。

何人も連続で刷る場合、PDFを連結して刷った方が良いです。Popplerに付属するpdfuniteが使いやすかったです。

IMG_20191011_143502.jpg
IMG_20191011_144057.jpg

困ったこと・反省点

  • transformの仕様
  • Go言語を選んだのはミス。ChromeとHTMLテンプレートエンジンを使うのであればNode.jsが相応しかった
  • Macの印刷設定がまいどまいどサイズ95%に戻るせいで、慣れないうちは何度かミスプリントしてしまった。
  • 全般的に人力が多かったので限界っぽくなった。次はもっと自動化したい。
  • 緑色の定期券も作ろうとしたのだが、PDFから直接印刷しようとすると変な模様が出てしまう。画像化すれば問題はないものの、プリンタの読み込みに時間がかかるので、量産できなかった。

最後に

プロコン期間中、東京高専プロコンゼミと名刺交換して伝説の高専生の本名を手に入れよう!
本記事には登場しなかった「真面目なデザイン」「緑色の定期券風」の名刺も手に入る!2


  1. 願わくばこの文が不謹慎にならないことを。 

  2. ピンク色定期券風の裏が真面目なデザインか緑色定期券になっています。緑色は超レア。 

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

どろどろの手動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の良さを活かしつつツール化してみた。

guicast.gif

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万回/年にも耐えられるようにエンジニアには筋肉が必要だ!というのもわからんでもないな。

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

cweill/gotestsでTable Driven Testsをらくらく実行

はじめに

Table Driven Tests(テーブルドリブンテスト。以下、TDT)とは、入力値と期待値などをもったテストケースを用意し、順次実行していくテスト手法です。
ここでいうTableとはエクセルのようなスプレッドシート、すなわち表のことを指し、テストケースを網羅した表を用意するようにテストコードを用意していきます。
公式でも使われています。
https://github.com/golang/go/wiki/TableDrivenTests

TDTの例

例として、挨拶メッセージを返すメソッドをテストしてみましょう。

引数の時刻によって「おはよう」「こんにちは」「こんばんは」の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.

以上

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