oinume journal

Scratchpad of what I learned

2021年の抱負

年初に新年の抱負を書こうと思っていたがずっと後回しになってしまい、1月もそろそろ終わってしまう時期になった。というわけで明けましておめでとうございます。

例によって全ては達成できないと思うけど、今年やりたいことを抱負としてまとめてみる。

習慣化

これは抱負というよりかはよりメタなものだけど、継続していきたいことは習慣化させないと続かないため、習慣化を努力しようと思っている。昨年の11月からHabitifyというiOSアプリを試していてなかなか良いなと思ったので、年初に1年単位で課金してみた。このアプリは、例えば「ジョギング」のような習慣化したいものを登録しておいて、それを通知でリマインドしたり、実際にやったことを記録して週や月単位で振り返りするようなアプリだ。自分の場合は以下のような習慣を登録している。

  • プロテインを飲む
  • 階段ジョギング
  • 筋トレ
  • iKnow(英単語学習)

このアプリ、特に何かすごい機能があるわけではないけど、リマインドを1日に2回するなど割と気が利いているのとストレスなく使えるので気に入っている。今年はこのアプリとともに習慣化に一番力を入れたいなと思っている。

ReactでSPA作れるようになる

仕事では専任のFrontend Engineerがいないため、自分もFrontのコードを触らなくてはいけないし、もともと自分ひとりでサービスを作ったりしたいので、今年はTypeScript + ReactでSPAのサービスを作れるようになりたい。

  • TypeScript
  • React
  • React Hooks
  • React Context
  • React Router
  • Jest

あたりを学べばいいのかなと漠然と思っている。余裕があったらNext.jsにもトライしてみたい。

英語

毎日iKnowで英単語をやるのと、週に1回はSkype英会話のレッスンをやるようにして、英語力を下げない努力をしようと思っている。昨年まではDMM英会話をやっていたが

  • 値段が高くなってきている
  • 英会話をやる時間を捻出するのが難しい
  • 単語を発声して覚える方が英語身についてそう(自分が発音できない単語は聞き取れないらしいので)

と思ったのでiKnowをなるべくやるようにして、週1のSkypeレッスンに切り替えた。あと、実力値を安価に測るためにTOEICを年に2回受けるつもり。

アウトプット

1ヶ月に1つはちゃんとした記事を書く。昨年に引き続き継続は力なり、ということで。

というわけで

2021年もやっていくぞい

2020年の振り返り

早いもので2020も終わってしまう。歳をとるごとに1年が過ぎていくのが早く感じる気がするけど、ここまで早く過ぎ去ってかつ内容が薄い年は初めてなのではないかと思う。ただ、多くの人にとって大変な年になったのは間違いないと思うけど、自分はソフトウェアエンジニアという職業だったおかげで、相対的に見ればCOVID-19の経済的な影響は少なかったので、ラッキーだったとは思う。あらためてテクノロジーの進化とソフトウェアエンジニアになろうと決めた2000年頃の自分に感謝したい。

1月

2019年末からインフルエンザにかかって実家に帰ったりもできず最悪のスタート。しかし、初めて家族でスキーに行ったり、その直後にGoDaysというベルリンで開催されたGoのカンファレンスに参加できたのはとてもラッキーだった。GoDaysのついでにワルシャワに1泊2日で個人旅行してきて、ポーランドという国の大変な歴史を学びつつ、社会主義から資本主義に移行している勢いも感じられたのでよかった。

振り返ると2020年は私生活ではこの1月が最高に充実していたと思う。スタートアップへの転職について考え始めたのもこの1月からだったと思う。

2月

会社の炎上プロジェクトが本格的に炎上し始めて仕事が大変になってきたのがこの時期だった。ただ、コロナも未だこの頃は猛威を奮っていなくて呑気に出社して仕事をしていた気がする。

3月〜6月

会社が原則WFHに移行し、さらに子供が保育園に行けなくなり精神的に削られる日々が続いた。子供が家にいる状態で集中して作業できる時間が確保できなくなったため、自分でコードを書くのは諦めて、担当しているmicroserviceのproductivity teamのPM的な役割に徹した。コードが書けないのは個人的にはストレスだったけど、とても強い人たちが支えてくれたので、チームとしては結構うまく立ち回れたのではないかと思っている。

また、GW前の仕事が一番炎上している時にSoftware Designの記事の締め切りがあり、生きている心地がしないぐらい忙しかった記憶がある。(みんな買ってくれ)

gihyo.jp

7月〜8月

メルペイを退職する意向を固めて、本格的に転職活動をしていた時期。コロナ渦での最終出社は寂しかったけど、あまり盛大に送り出されるのも苦手なので、あのぐらいがちょうどよかった。たまたま会社に10人ぐらい人が来ていたので久しぶりに同僚といい話ができた気がする。

8月後半にはグラファーとドクターズプライムからオファーをもらい、どちらに行くか決めきれなかったので9月と10月でそれぞれで働いてどちらかに決めることになった。悩みまくってとりあえず横浜のみなとみらいまで海を見に行ったりした。

9月

この1ヶ月はグラファーで業務委託としてお試しで働かせてもらっていた。地方自治体向けのSaaS開発の裏側と急成長しているスタートアップの裏側を何一つ隠すところなく見せてもらい、とても有意義だった。「こういうことをやって欲しい」というお題をある程度与えてもらいつつ、自分的に課題だと感じたところを見つけて直していくようなことをやった。結果としてグラファーからのオファーは断ってしまったけど、CEOの石井さんの考え方がとても好きでファンなので、副業で少しだけ手伝わせてもらっている。

10月

10月はドクターズプライムで1ヶ月働いてみた。グラファーはリモート中心で、こっちはオフィスに出社するのが中心であり、久々にオフィスメインで仕事することもあってか、人と雑談しながら仕事をするのがとても楽しかった。この頃は自分以外にまだエンジニアが誰もいなかったので、どういう風に仕事を進めればいいか悩ましいところもあったけど、他の人々が優しく接してくれたのであまり肩肘貼らずに仕事できたように思う。

11月〜12月

最終的にドクターズプライムに決めて社員として働き始めた。正直コードを書く量は前職に比べて減った気がするけど、プロダクト開発やセキュリティ強化、採用など様々なことをやらなくてはいけないカオスな状況は楽しい。メルペイで働き続けていた方がバックエンドエンジニアとしてのキャリアは積めるだろうけど、社員10名規模で技術スタックが完全一致しているスタートアップで働ける機会なんてそうそうないと思ったので転職してよかったと思う。

ライブ

今年はこれらのライブに行くはずだったけど当然全部キャンセル or リスケされた。来年こそはと思うけどワクチン次第なのでどうなることやら。

  • Knotfest
  • Green Day
  • Avril Lavigne
  • Viva La Rock

しかしストリーミングでDragon Ashのライブを3個も見れたので、これはこれでありなのでは?とも思い始めている。

最後に

何だかんだで今年は激動の1年だった。他にも大事なことがいろいろあった気がするけど思い出せないので気にしないことにします。 新型コロナに負けずに来年もよい年を迎えられると良いなと思っております。皆さんも良いお年を!!

O'Reilly Online Learningで日本語の本を読む方法

O'ReillyのOnline Learning(旧O'Reilly Safari Books Online)は月額$49でオライリーの本や動画などが見放題になるエンジニア向けのサブスクを提供している。以前は英語の本しか読めなかったが、いつからか日本語の本も読めるようになっていたのでメモ。

www.oreilly.com

Sign Inして、左のメニューのSettingsをクリックするとLanguage Preferencesがあるので、ここでJapaneseにチェックを入れて下のUpdate Preferencesをクリックして保存する。

これでHomeに行き、例えばGraphQLで検索すると検索結果の画面でBooksのタブがあるのでこれを選択する。そうするとLanguageの選択ができるので、ここでJapaneseを選ぶと日本語の本だけに絞ることができる。(手っ取り早く検索結果を表示したい時はこちら)

目的の本が見つかったらそれをPlaylistに入れるなりすればOK。なお、AndroidのO'Reillyのアプリからだと日本語の本をダウンロードして読むことができたが、iOSはエラーになって読めなかった。

2021年2月時点では合計57冊の日本語の本が読めるらしい。個人的に良さそうだなと思ったものをいくつか書いておく。

なお、裏技としてACMの会員になると年$99でO'Reilly Online Learningにアクセスできるようになるので、4冊ぐらい読めば元は取れることになる。ACMの登録方法についてはここによくまとまっているので参考になると思う。

それでは素敵なO'Reillyライフを!

ドクターズプライムに入社しました

これはなに?

入社エントリー&会社紹介です。表題の通りで、10月をもってメルカリ/メルペイを退職し、11月からドクターズプライムという会社で働いています。

なにやってるの?

Backend Engineerとして、救急車のたらい回しをなくすためのプロダクトを開発しています。救急車のたらい回しが発生する理由としては以下のスライドに書いてある通りなのですが、これを解決するのがDr.'s Primeという医師の採用サービスになります。

自分も子供を持つ親なので何度か病院のお世話になったことはあるし、子供が救急車で運ばれて手術&入院したこともあるので、救急医療に関しては一当事者として良くしていきたいという思いがありました。ドクターズプライムは救急車の搬送を断らないための仕組みを採用サービスとして第三者の立場から提供していて、素直にいいソリューションだなと思っています。

入社の経緯

今年に入ってからスタートアップで働くと言うことを意識し始めて、何社か話を聞かせてもらいました。時系列にするとこんな流れです。退職すること自体は昨年から別の理由で検討していたのですが、今年に入ってからスタートアップ良さそう!と思うようになりました。

  • 1月:スタートアップでDXをやっている会社に興味を持ち始める
  • 6,7月:メルペイを退職する意向を固めて、本格的に転職活動を始める
  • 8月:2社からオファーをもらって悩み始める
  • 10月末:入社を決める

8月は2社からオファーをもらっていて悩んでいた時期で、そのうちの一つがドクターズプライムでした。2社のうちどちらにするかを悩んでいたところ、「両方で働いてみて決めればいいんじゃない?」という提案をしてもらい、ありがたく両方の会社で1ヶ月ずつ働くことにしました。ちなみにもう一つは地方自治体向けのSaaSを提供しているグラファーという会社です。

最終的には、より会社規模が小さいドクターズプライムを選びました。エンジニアが自分を含めて2名しかいないため、必然的にプロダクト開発チームを作るところからスタートになります。1ヶ月お試しで働いてみてこのメンバーならチーム作りを含めて楽しくやれそうだと感じたこと、HRT(Humility, Respect, Trust)をはじめとしてカルチャーを大事にしていて、実際に中で働いてそれを体験できたことが決断の後押しになりました。

ちなみにグラファーもいい会社です。こちらはリモート中心の働き方でしたが、相談事があったらすぐにZoom MTGが開催されたり、障害が発生した場合はポストモーテム(振り返り)をきちんと行い再発防止に努めていたり、プロフェッショナルさを感じることが多かったです。たまにオフィスに集まると和気藹々とランチを一緒に食べたり、スタートアップとしての一体感も強いです。

取り組んでいる分野に違いはあれど、どちらの会社も日本に存在するペインを猛烈なスピードで解決するスタートアップであり、その両方で働く機会をもらったことはとてもいい経験になりました。両社とも楽しく仕事させてもらったので、分身できるのなら両方でフルタイムで働きたかったぐらいですw

ドクターズプライムでやっていきたいこと

創業時のリーンなプロダクト開発の結果として、GAS + Spreadsheet + Google Forms のシステムで売り上げが立つという、ある意味嬉しい誤算の状態になっていました。しかし、このGASベースのシステムもそろそろ綻びが見え始めており、一般的なFrontend + Backendの構成に置き換えていくことが当面やっていくことになります。

これ以外にも

  • 新しい病院向けプロダクトの開発
  • GCPの設定をterraformで管理したい
  • もっとTDDを押し進めたい
  • 社内の情報機器の管理をもっと効率的にやりたい
  • 勤怠システムの打刻がめんどいのでSlackで打刻できるようにしたい

などなど、やりたいことは山のようにあります。しかし、まだBackend engineer2名という体勢なので採用も頑張らなくてはなりません。特に初期設計から実装までまるっとお任せできる強いFrontend Engineerの助けを必要としています。(マジで助けてくれ!)

その他にも多数の職種を募集しているので、興味がある人は以下の採用ページをみてもらえればと。オープンポジションもあるので、事業に興味はあるけど自分がマッチするポジションのイメージがなくても大丈夫です。

採用情報 | 株式会社ドクターズプライム

などと採用の話になってしまいましたが、とりあえず私は生きていて浅草で働いているので近くにいる人はぜひランチでも行きましょう!

f:id:oinume:20201109182914j:plain
オフィス近くから見えるスカイツリー

moq - gomockを使わないMock生成

Goでよく使われるMockの生成ツールとしてgomockがある。個人的にはgomockが生成したコードでモックを書くのが好きではないので、代替としてmoqを使うやり方を取り上げてみようと思う。

題材

GitHubのAPIを使って指定したリポジトリのブランチを出力するServiceを考えてみる。このServiceには以下の引数owner, repoで指定されたリポジトリのブランチをwに出力するというメソッドを持っている。

PrintBranches(ctx context.Context, w io.Writer, owner, repo string) error

service.go

このServiceの実装コードは以下のようになっていて、githubClientを使ってGitHub APIを呼び出し引数のリポジトリのブランチの一覧を取得している。

github/client.go

次にServiceから参照されているgithub.Client interfaceについて説明する。これは以下のように純粋なinterfaceとして定義している。そして、ユニットテストではGitHub APIへのアクセスをモックにしたいので、このinterfaceが定義しているListBranchesを実装するモックをgo:generateで生成するようにしている。

package github

//go:generate moq -out=client_test.moq.go . Client
//go:generate mockgen -destination=client_test.gomock.go -package=github . Client

import "context"

type Client interface {
    ListBranches(ctx context.Context, owner, repo string) ([]string, error)
}

gomockを使ったテストコード

それではgomockを使ってService.PrintBranchesのテストコードを書いてみよう。以下のようにgithub/client.go にgo generateでmockgenを呼び出すコードを追加し、go generate ./mock/github を実行すると、./mock/github/client_test.mock.go が生成される。

//go:generate mockgen -destination=client_test.gomock.go -package=github . Client

そして、以下が実際にgomockが生成するMockを使って書いたservice.goのテストコードである(service_test.go)。

ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGitHubClient := github.NewMockClient(ctrl)
mockGitHubClient.
    EXPECT().
    ListBranches(context.Background(), "oinume", "playground-go").
    Return([]string{"main", "feature/xyz"}, nil)
s := Service{githubClient: mockGitHubClient}
out := new(bytes.Buffer)
if err := s.PrintBranches(context.Background(), out, "oinume", "playground-go"); err != nil {
    t.Fatal(err)
}
fmt.Printf("out = %v\n", out.String())

注目して欲しいのは、以下のEXPECT()以降の部分。

mockGitHubClient.
    EXPECT().
    ListBranches(context.Background(), "oinume", "playground-go").
    Return([]string{"main", "feature/xyz"}, nil)

このEXPECT()が返すのは*MockClientMockRecorder型で、これに定義されているメソッドListBranchesのシグネチャはListBranches(arg0, arg1, arg2 interface{}) *gomock.Callになっている。これは本来のListBranchesのシグネチャとは異なるものになり、引数の型がinterface{}になっているため、オリジナルの引数の型であるstring以外の型も渡せてしまう。テストコードの実行には問題がないかもしれないが、本来であればClient interfaceに定義されている引数の型と同じにしておいた方が変な誤解を生まなくてすむと思う。

また、MockClientMockRecorder.ListBranchesが返す*gomock.Callに対してモックが返す値をReturn([]string{"main", "feature/xyz"}, nil)のようにセットする必要があるが、戻り値の数を間違えてもコンパイル時にはエラーにならず、実行時にエラーになることもイケてない点だ。

また、Mockのために以下のようなboilerplateのコードを毎回書くのも個人的には好きではない。

ctrl := gomock.NewController(t)
defer ctrl.Finish()
...

moqを使ったテストコード

では次に、moqが生成したコードとそれを使った場合のコードを見てみる。

まずmoqが生成したコードは以下のようになっている。

type ClientMock struct {
    // ListBranchesFunc mocks the ListBranches method.
    ListBranchesFunc func(ctx context.Context, owner string, repo string) ([]string, error)

    // calls tracks calls to the methods.
    calls struct {
        // ListBranches holds details about calls to the ListBranches method.
        ListBranches []struct {
            // Ctx is the ctx argument value.
            Ctx context.Context
            // Owner is the owner argument value.
            Owner string
            // Repo is the repo argument value.
            Repo string
        }
    }
    lockListBranches sync.RWMutex
}

// ListBranches calls ListBranchesFunc.
func (mock *ClientMock) ListBranches(ctx context.Context, owner string, repo string) ([]string, error) {
    if mock.ListBranchesFunc == nil {
        panic("ClientMock.ListBranchesFunc: method is nil but Client.ListBranches was just called")
    }
    callInfo := struct {
        Ctx   context.Context
        Owner string
        Repo  string
    }{
        Ctx:   ctx,
        Owner: owner,
        Repo:  repo,
    }
    mock.lockListBranches.Lock()
    mock.calls.ListBranches = append(mock.calls.ListBranches, callInfo)
    mock.lockListBranches.Unlock()
    return mock.ListBranchesFunc(ctx, owner, repo)
}

次に上記のClientMockを使ったコードはこんな感じ。

githubClient := &github.ClientMock{
    ListBranchesFunc: func(ctx context.Context, owner string, repo string) ([]string, error) {
        return []string{"main", "feature/xyz"}, nil
    },
}
s := Service{githubClient: githubClient}
out := new(bytes.Buffer)
if err := s.PrintBranches(context.Background(), out, "oinume", "playground-go"); err != nil {
    t.Fatal(err)
}
fmt.Printf("out = %v\n", out.String())

moqの場合はモックにしたいメソッドをstructの初期化時にセットする。これはただのメソッドなので、中身の実装は好きなように書けばよく、引数の型や戻り値の型も元のClient interfaceと全く同じである。つまり、戻り値の数を間違えてもコンパイル時に検出してくれる。個人的にはこのmoqを使ったコードの方が、boilerplateなコードもなく純粋にモックにフォーカスできるのではないかと思う。

まとめ

gomockではなくmoqを使うとモックを使ったテストコードがよりわかりやすく、かつ使い方を間違えた時の実行時のエラーもなくなることが判ったのではないかと思う。個人的にはモック化する対象が少なければmoqを使わずに、moqが生成するような該当メソッドを置き換えるfuncを手で書いてもいいのではないかと思う。というわけで、gomock割といろんなところで使われているけどより良いツールがあるよ、という紹介でした。おしまい。