一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

プロセス置換 (Process Substitution)について

bashzshにはプロセス置換 (Process Substitution)という機能があるのだが、あまり使うことがなく頻繁に忘れるのでメモ。

プロセス置換 (Process Substitution)とは

コマンドの結果をファイルのように扱うことができる機能。以下のように書く。

  • <(list)
    • リストの結果を入力ファイルに置き換える
  • >(list)
    • リストの結果を出力ファイルに置き換える

これはリストの結果をもう一方のプロセスに送るために/dev/fd/<n>のファイルディスクリプタを使っているっぽい。

使用例

例えば commというコマンドがある。これは comm file1.txt file2.txt のようにしてファイル1と2を良い感じに比較したりできるが、プロセス置換を使えばファイルを用意せず使用することができる。

$ comm <(ls -l) <(ls -la) 
total 0
-rw-r--r--@ 1 username  staff   0 Jun  6 17:29 file1.txt
drwxr-xr-x  3 username  staff  96 May 18  2019 tmp
    total 16
    drwx------+   6 username  staff   192 Jun  6 17:29 .
    drwxr-xr-x+ 132 username  staff  4224 Jun  6 16:25 ..
    -rw-r--r--@   1 username  staff     0 Jun  6 17:29 .secret.txt
    -rw-r--r--@   1 username  staff     0 Jun  6 17:29 file1.txt
    drwxr-xr-x    3 username  staff    96 May 18  2019 tmp

またディレクトリ間の差分を見たい場合など、diffコマンドをよく使うがこれも以下のようにプロセス置換を使うことができてよい。

$ diff <(ls $first_directory) <(ls $second_directory)

他にも例えば、k8sのドキュメントでもbashzshの自動補完の項で echo 'source <(kubectl completion bash)' >>~/.bash_profileのようにプロセス置換を使っていたり。

まとめ

ファイルしか受け付けないようなコマンドでもプロセス置換を使えば一時ファイルを作成する必要がないので便利。>(list)の方は使い道あまり思いつかなかったw

参照

英語で1on1ミーティングができるようになるまでにやったこと

今、自分の所属する会社では外国籍のエンジニアも働いていて、彼らとは普段英語でコミュニケーションを取っています。

具体的にはSlack上でのチャットや彼らが参加するミーティング、1on1などを英語で行っていて、全く流暢ではないですが、現状問題なくコミュニケーションは取れていると思います。

初めて海外からエンジニアをチームに迎え入れたのが約1年半前。当初から分かっていたことではあったけど、特にリスニングとスピーキングが大きな課題となり、ミスコミュニケーションによって開発に支障をきたすようなケースもありました。言いたいことも言えないこんな開発環境はPOISO...

ただ色々と試行錯誤を繰り返し、今日なんとか30分~1時間程度の1on1ミーティングぐらいであれば英語でもこなせるようになりました。

今回はこれまでどのようにして自分が英語を勉強してきたかをざっくり書いてみたいと思います。「TOEIC900点です」みたいな定量的な実績も残していないし、誰かの参考になるかは分からないけど「英語、意外となんとかなるよ」的なメッセージを少しでも伝えられたらと思います。

はじめに

対象読者と当初の自分の英語力は以下のような感じ。

対象読者

  • 仕事で英語が必要な人、もしくはこれから積極的に使っていきたい人
    • これはあくまでエンジニア同士の会話のように、ある程度コンテキストが共有されている前提でのコミュニケーションを想定している (必要最低限の英単語や言い回しさえ知っていれば、それなりに会話ができるような状況)
    • 海外ドラマや海外映画のようなレベルの英会話を達成するには今回書いているような勉強量では不十分だと思います、すみません

当初の英語力

  • 留学経験とか一切なし
  • TOEICスコアは多分550ぐらい
    • 受験したのは5年以上前、院試で必要だっただけ
  • 英語のドキュメントは必要であれば読む
  • Gitのコミットメッセージやコードコメント程度は英語で書く
  • 英語を話すこと自体に特に抵抗はなかった
    • 恥ずかしくて話せないみたいなメンタリティではなかった

準備編

ここからは海外から同僚がジョインするまでの数ヶ月 (Visaの発行とかあるので)でどのような英語対策を行ったかという話。

文法の復習

「日本人は英語を話すことに抵抗がある、間違ってもいいからまずは話せ」みたいな意見をよく聞く一方で、「文法を捨てて英会話に特化して勉強していると、いずれ成長が鈍化する」みたいな記事をどこかで見た気がして、まずは中高6年で習ったであろう文法をざっくり復習してみることにした。

選んだ本はこちら。

一億人の英文法 ――すべての日本人に贈る「話すため」の英文法(東進ブックス)

一億人の英文法 ――すべての日本人に贈る「話すため」の英文法(東進ブックス)

この本は英文法について「ネイティブがどのような感覚でそれを使っているか」みたいな視点で解説されているということで選んだのだが、当たりだった。受験で高得点を取るためではなく、純粋に英語を話せるようになるための本だった。

例文も This is a pen. みたいな何の役にも立たなそうなものではなく、もっと日常で出てきそうなものばかりで大変参考になった。

ただしこの本、辞書みたいに分厚くて何度も繰り返し読むのは非常にツラいので、2週間ぐらいかけて読みながら各文法の要点をEvernoteにまとめていき、エビングハウスの忘却曲線に沿った周期でそのノートを何度もチェックするようにした。

英語上達完全マップとの出会い

次に色んな人の英語勉強法を検索する中で、英語上達完全マップというサイトを発見した。

自分も今まで受験向けの英語教育しか受けてこず、実際に英語を使う (特にスピーキング)経験が圧倒的に不足しているという所に課題感を持っていたので、ここで書かれていた内容は腑に落ちた。

自分はこちらに書かれている、音読パッケージと瞬間英作文を重点的にやっていこうと決めた。文法に関しては、1億人の英文法ですでにちょっと復習していたので、それ以上は特に何もしていない。

音読パッケージ

まずは音読パッケージの本を1冊買って、それを何度も繰り返すことにした。

選んだ本はこちら。

みるみる英語力がアップする音読パッケージトレーニング(CD BOOK)

みるみる英語力がアップする音読パッケージトレーニング(CD BOOK)

上記の完全マップの人が書いた本だったので、まあ間違いないだろうという感じで選んだ。学習方法は完全マップや本の中で説明されているので、素直にそれに従った。

毎朝、会社に行く前の2~30分で1セクション進めるようにして、それを5周ぐらいは続けたと思う。ただ実際はそんなストイックなものではなく、だるい日や少し寝坊した日は本能に従って休んだりも結構した。

そのうち丸暗記に近い感じになって飽きてきたので、別のシリーズも買って、同じように進めた。今でも継続している。

ぐんぐん英語力がアップする音読パッケージトレーニング 中級レベル(CD BOOK)

ぐんぐん英語力がアップする音読パッケージトレーニング 中級レベル(CD BOOK)

瞬間英作文

音読パッケージと合わせて瞬間英作文のトレーニングもスタートした。

選んだ本はこちら。

会話できる英文法大特訓 CD付

会話できる英文法大特訓 CD付

こちらも受験英語的な例文ではなく、より生の会話に近いような例文が多い印象だったので選んだ。進め方はこれまた上述の完全マップを完全に参考にした。

家ではすでに音読パッケージの学習をしていたので、気分を変えたいなと思い、こちらは通勤時、駅まで歩く間とかジョギングしながらとか、動きながら進めるようにした。周囲に人がいない時はちょっと声に出して発音の練習をしたりもした。

これは多分日本人に一番足りていないスキルで、ストレスなく英語が出てくるようになるまでかなり苦労したので10周ぐらいした。今は忘れてきたなという頃にたまに復習する程度で、以下の本あたりを検討中。

瞬時に出てくる英会話フレーズ大特訓

瞬時に出てくる英会話フレーズ大特訓

実践編

音読パッケージと瞬間英作文を1周終えたぐらいの時期、Visaを無事取得した同僚がジョインして、実践形式での英語コミュニケーションがスタートした。

日々のSlackの開発チャンネルやGitHubでのレビューなどがすべて英語と化し、開発環境の構築を英語でサポートするなど、「英語話せないとガチでヤバい」という状況が整った。

正直、これが一番最高なトレーニングであることは間違いないので、オンライン英会話を始めるとか英語のミートアップに参加するとか、何かしら実践できる環境を見つけることが上達への近道だと思う。

読み書きのためのツール

英語を読み書きする機会が増えてからは、以下のツールにはとてもお世話になっている。

  • Google翻訳

    • 精度も日々向上しているし、もはや「メインエディタかよ」ってぐらい使っている
    • 自分はいつも英文はGoogle翻訳上で書いていて、自分の書いた英文の日本語訳が意味不明じゃなければコピーアンドペーストみたいな感じで、バリデーション用途で使用している
      • Slackのようなチャットでのコミュニケーションだと、さすがにそんな悠長なことやってられないので時と場合による
  • Grammarly

    • aやtheが抜けているみたいな細かい文法ミスを都度指摘してくれるので、Google翻訳と合わせていつも使っている (Google翻訳以外でも動作はする)
    • たまに納得のいかない指摘をされることもあるので、そんな時は無視

発音

実際に英語で話す中でたまに発音が思ったように通じないことがあったため、発音も改めて勉強してみることにした。

発音の勉強にはこちらのサイトを参考にした。

タイトルにもある通り、3時間でざっと学習できるのでオススメ。これは3周ぐらいやって、あとはたまに自分の発音が通じなかった時とかに復習している。

あと、これも日本語の弊害で例えばgenreという単語は日本語で「ジャンル」と読みがちだが、実際は「ジョンラ」みたいな発音だったり、既存の日本語知識が邪魔をしてくるケースもあるので、新しく英単語を覚える時は必ず発音やアクセントを確認したほうが良いと思う。

よく英語ペラペラの日本人が英単語の部分だけ英語ネイティブっぽく発音しているのをイキってるだけだと思っていたけど、そういう言語間の発音の違いを吸収するのが大変だっただけなのかな?なんて思ったりした。

英語のPodcastを聴く

音読パッケージと瞬間英作文をそれぞれ2~3周ぐらい終え、ちょっと実践的な英語にも慣れてきたタイミングで英語のPodcastをたまに聴くようになった。

自分はソフトウェアエンジニアなので、テック系のPodcastの中で面白そうなもの、そして中でも興味のあるトピックの回を何度も繰り返し聴いたりした。日本語で聴いてもよくわからないようなものは絶対に英語でも理解できないので、自分が使い慣れた技術について語っているようなものがオススメ。

元々よく聞いていたRebuild.fmの英語回やThe Changelog, GCPのPodcast などを好んで聴いた。

一言一句完璧に聴き取るのは基本的に不可能だし、聴き取れない部分は何度聴いても聴き取れなかったりするので、文字起こしがある (もしくは購入できる)ものを選ぶのがオススメ。聴き取れなかった箇所を確認しながら聴くのが良いと思う。

会話から学んだ英単語や言い回しをストックする

実際に日常で飛び交った英単語を都度調べて単語帳にストックするようにした。自分はWeblioを使っていて「同僚が使っていて自分が知らなかった単語を調べる -> 単語帳に登録 -> 夜寝る前とか暇な時に見返す」みたいにして覚えている。

これは学校の単語テストの前に頑張って暗記した時のような雑な記憶ではなく、実際に単語が使用されたシチュエーションとともにエピソード記憶として残りやすいので結構定着した。

仕事でエンジニアリングについての議論をする程度であれば、例えばloose couplingとかtechnical debtみたいな技術的な単語は一度使ったら結構忘れないものだし、It makes sense! みたいなよく使う言い回しとかも何度か使っていれば英語として自然に出てくるようになった。

If I were a bird, ... みたいなメルヘンチックな言い回しとか業務内でめったに使わないだろうし、シンプルで便利な言い回しだけ覚えておけばコミュニケーションは十分取れると思う。

Twitterで著名な海外のエンジニアのツイートをチェックする

Twitterのリスト機能を使って有名なOSSのコミッターなど、著名な海外のエンジニアの方を追加しまくって、英語の技術情報だけが流れてくるタイムラインを用意した。

1投稿あたりの文字数が少ないので、洋書を読むとかよりも格段にハードルが低く、フランクなネイティブらしい言い回しなども目にすることができるので良かった。

どちらかというと英語の勉強というよりは情報収集のために見ている感じ。英語のリーディングに関しては普段ドキュメントなどを読む機会も多いので、あまり力は入れていない。

英語でブログを書いてみる

すでにコードレビューやチャットで英語のライティングの機会はあったが、よりアウトプットの場を増やすべく英語のテックブログも始めた。わりと海外で使われていそうなdev.toを利用した。

ただ超大作みたいな記事は心が折れて書けないので、簡単な技術メモ程度の記事をたまに書くようにしている。

まとめ

色々と書いてきたけど、結局英語上達するには以下のことに尽きるかなという気がしました。

  • 日々の習慣の中に英語学習を組み込んで、地道に反復練習を継続することが重要
  • モチベーションを継続するために「英語ができないとやばい」環境に身を置いたり、英語力を向上する目的 (アメリカ人の恋人がほしいとか)を明確化するのが重要
  • 実際に英語を使うことが最も重要

「いや英語使う環境なんてねーよ」というエンジニアの方、Twitterとかでご連絡いただければ自分の会社のことも含め、色々とお話できるのでお気軽にどうぞ。

この記事が何かしらの参考になれば幸いです。

参照

factory_botでactive_model_serializers用のPORO (Plain-Old Ruby Object)のテストデータを作成する

最近、DBの存在しないRailsプロジェクト下でAPIを作る機会がありました。

外部からfetchしてきたデータからPORO (Plain-Old Ruby Object)を作って、 active_model_serializersでひたすらシリアライズしまくるみたいな感じです。 特にテストまわりで若干の工夫が必要だったので、その時のメモです。

環境

現時点でまだ本リリースされていないRails 6を無駄に使っている。

構成

  • app/models
    • POROを定義していく
  • app/serializers
  • app/controllers
    • エンドポイントを定義していく

モデル

サンプルとしてURLとサイズの情報を持つImageモデルを定義してシリアライズするケースを考えてみる。

こちらに書かれているようにactive_model_serializersではPORO用に ActiveModelSerializers::Modelを定義してくれているので、シンプルなものであればこれを使って簡単に実装できる。

よりファンキーな実装が求められる状況であっても、 こちらの仕様に沿った自前モデルさえ作れれば問題なくワークすると思う。

# app/models/image.rb
class Image < ActiveModelSerializers::Model
  attributes :url, :size
end

# app/models/image/size.rb
class Image::Size < ActiveModelSerializers::Model
  attributes :width, :height
end

リアライザ

# app/serializers/image_serializer.rb
class ImageSerializer < ActiveModel::Serializer
  attributes :url, :type
  has_one :size
end

# app/serializers/image/size_serializer.rb
class Image::SizeSerializer < ActiveModel::Serializer
  attributes :width, :height
end

has_oneみたいにリレーションを定義しておくと、includeオプションとかが良い感じに使える。

例えば、 render json: image, include: '*'とするとsizeが含まれたJSONが返るし、 render json: image, include: ''とすると sizeが含まれないJSONが返る。

コントローラ

コントローラではインスタンスを作って、そいつをrenderメソッドに渡せばactive_model_serializersが最適なシリアライザを見つけてシリアライズ -> JSONを返却してくれる。

実際のプロジェクトでは外部からデータを取ってくる実装 (下の fetch_data_somehowの部分) が一番ツラかったのだが、本題ではないので割愛。

# app/controllers/v1/images_controller.rb
module V1
  class ImagesController < ApplicationController
    def show
      render json: image, include: params[:include]
    end

    private

    def image
      @image ||= Image.new(image_attrs)
    end
    
    def image_attrs
      @image_attrs ||= fetch_data_somehow
    end
  end
end

テストまわり

ActiveRecordのモデルのテストとかだと factory_botを使って

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end
> user = FactoryBot.create(:user)

みたいにさくっとテストデータを用意できると思う。

ただ今回作成したPOROの場合、createメソッドを呼んでも保存する場所がない。また、ActiveModelSerializers::ModelActiveModelをベースにしているのでnewのタイミングで attributesを渡してやらないと attributes = {}アサインされてしまって、そのままだと意図通りにシリアライズされない。

# spec/factories/images.rb
FactoryBot.define do
  factory :image do
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

# spec/factories/image/sizes.rb
FactoryBot.define do
  factory :image_size, class: 'Image::Size' do
    width { rand(100..500) }
    height { rand(100..500) }
  end
end
> image = FactoryBot.create(:image) 
=> NoMethodError: undefined method `save!`...

> image = FactoryBot.build(:image)
> image.attributes 
=> {}

> image.to_json 
=> "{}"

factory_botはイニシャライザをオーバライドできる initialize_withcreateをスキップできる skip_createを提供してくれているので、これらをありがたく使わせてもらい解決できた。

# spec/factories/images.rb
FactoryBot.define do
  factory :image do
    skip_create
    initalize_with { new(attributes) }
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

# spec/factories/image/sizes.rb
FactoryBot.define do
  factory :image_size, class: 'Image::Size' do
    skip_create
    initalize_with { new(attributes) }
    width { rand(100..500) }
    height { rand(100..500) }
  end
end
> image = FactoryBot.create(:image) 
=> #<Image:...> `build`と同じ結果になり、エラーは発生しない

> image = FactoryBot.build(:image)
> image.attributes 
=> { "url" => ..., "size" => ... }

> image.to_json 
=> "{\"url\":...,\"size\":...}"

毎回initialize_withskip_createを書くのはだるいので、最終的には以下のようなDSLを用意してそいつを使うことにした。

# config/initializers/factory_bot.rb
if defined?(FactoryBot)
  module FactoryBot
    module Syntax
      module Default
        class DSL
          # Custom DSL for ActiveModelSerializers::Model
          # Original: https://github.com/thoughtbot/factory_bot/blob/v5.0.2/lib/factory_bot/syntax/default.rb#L15-L26
          def serializers_model_factory(name, options = {}, &block) 
            factory = Factory.new(name, options)
            proxy = FactoryBot::DefinitionProxy.new(factory.definition)
            if block_given?
              proxy.instance_eval do
                skip_create
                initialize_with { new(attributes) }
                instance_eval(&block)
              end
            end
            FactoryBot.register_factory(factory)

            proxy.child_factories.each do |(child_name, child_options, child_block)|
              parent_factory = child_options.delete(:parent) || name
              serializers_model_factory(child_name, child_options.merge(parent: parent_factory), &child_block)
            end
          end
        end
      end
    end
  end
end
# spec/factories/images.rb
FactoryBot.define do
  serializers_model_factory :image do
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

あとは以下のようにテストで使える。

# spec/serializers/image_serializer_spec.rb

require 'rails_helper'

RSpec.describe ImageSerializer, type: :serializer do
  let(:resource) { ActiveModelSerializers::SerializableResource.new(model, options) }
  let(:model) { build(:image) }
  let(:options) { { include: '*' } }

  describe '#url' do
    subject { resource.serializable_hash[:url] }

    it { is_expected.to eq model.url }
  end
  ...
end

まとめ

POROかわいい。

参照

既存のGitリポジトリのコミットをベースに新規にGitリポジトリを作成する

たまたまこういうことをする必要があったのでメモです。

(例) すでに運用中のbarリポジトリの既存のコミットをinitial commitとして、新規にfooリポジトリを作成するケース

$ cd /path/to/bar 
$ git remote add foo {foo's URL}  # Add foo's remote repository
$ git checkout -B foo $(git rev-list HEAD --max-parents=0) # Create foo branch from the initial commit of bar
$ git merge origin --squash  # Checkout all file changes
$ git commit --amend -C HEAD 
$ git push foo HEAD:master

まとめ

git rev-list知らなかった...

Golang + echoなREST APIサーバで、テスト実行時に自動でAPIドキュメントを生成できるようにする

最近、Golang (+echo) でREST APIサーバを開発する機会があったのですが、テストを書いたらAPIドキュメントを自動生成するような仕組みを作るために試行錯誤したのでメモです。

方針

  • APIドキュメントの生成にはtest2docを利用
    • テストを実行するとAPI Blueprint形式でファイルを自動生成してくれそう
    • 該当するメソッドの上にコメントを書くことで最低限の説明は記述できそう
    • READMEには gorilla/muxjulienschmidt/httprouterのサンプルしか載っておらず echoでうまく動くかは試してみるしかなさそう
  • テストから生成された .apibファイルをaglioみたいなツールにかませばHTMLファイルとしてAPIドキュメントができそう

プロジェクト構成

github.com/danimal141/rest-api-sampleという名前で実装していく。とりあえずユーザー一覧を返すようなエンドポイント /api/v1/usersを実装して、APIドキュメントを自動生成する方法を考える。

余談だが、Golangのパッケージ依存管理にdepを使ってみたので、それ関連のファイルも混ざっている。

.
├── Gopkg.lock
├── Gopkg.toml
├── api
│   ├── all.apib
│   ├── router
│   │   └── router.go
│   ├── v1
│   │   ├── users.go
│   │   ├── users_test.go
│   │   ├── init_test.go
├── doc
├── gulpfile.js
├── main.go
├── models
│   ├── users.go
├── node_modules
├── package.json
└── vendor

APIサーバ実装

まずはAPIサーバをざっと実装してみる。

models/users.go

package models

import "fmt"

type User struct {
    Id       int
    UserName string
}

func SampleUsers() []User {
    users := make([]User, 0, 10)
    for i := 0; i < 10; i++ {
        users = append(users, User{Id: i, UserName: fmt.Sprint("testuser", i)})
    }
    return users
}

ユーザーのStructを定義。サンプル実装なのでDBに保存等はしていない。

api/v1/users.go

package v1

import (
    "errors"
    "fmt"
    "net/http"
    "strconv"

    "github.com/danimal141/rest-api-sample/models"
    "github.com/labstack/echo"
)

type paginationParams struct {
    Pagination string `query:"pagination"`
}

/*

### Query parameter

key        |value  |description
----------:|------:|----------------------------
pagination |false  |ページネーション機能は未実装なのでfalseが必須

*/
func UsersIndex(c echo.Context) error {
    if err := validatePaginationParams(c); err != nil {
        return err
    }
    return c.JSON(http.StatusOK, models.SampleUsers())
}

func UsersShow(c echo.Context) error {
    users := models.SampleUsers()
    id, err := strconv.Atoi(c.Param("user_id"))
    if err != nil {
        return err
    }
    if id > len(users)-1 {
        err := fmt.Errorf("user_id=%d is not found", id)
        return echo.NewHTTPError(http.StatusNotFound, err.Error())
    }
    return c.JSON(http.StatusOK, users[id])
}

func validatePaginationParams(c echo.Context) error {
    p := new(paginationParams)
    if err := c.Bind(p); err != nil {
        return err
    }
    if p.Pagination != "false" {
        err := errors.New("pagination must be false, because pagination is not supported yet")
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    return nil
}

ユーザー一覧情報を返すUserIndexとユーザー詳細情報を返すUsersShowを定義。

後でどのようにAPIドキュメント反映されるかを確認するため、一覧はページネーションが未実装であることを確認する?pagination=falseが必須であるとする。メソッドの上にコメントをつけているのも後でドキュメントに反映するためである。

api/router/router.go

package router

import (
    "github.com/danimal141/rest-api-sample/api/v1"
    "github.com/labstack/echo"
)

func NewRouter() *echo.Echo {
    e := echo.New()
    e1 := e.Group("/api/v1")
    e1.GET("/users", v1.UsersIndex)
    e1.GET("/users/:user_id", v1.UsersShow)
    return e
}

ルーティングの定義。

main.go

package main

import "github.com/danimal141/rest-api-sample/api/router"

func main() {
    r := router.NewRouter()
    r.Logger.Fatal(r.Start(":8080"))
}

これで go run main.goしてlocalhost:8080/api/v1/users/1などを確認するとJSONが返却されるはずである。

では次にこのAPIのテストを書いてAPI Blueprintファイルを自動生成する仕組みを作ってみる。

テスト

api/v1/init_test.go

package v1_test

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

    "github.com/adams-sarah/test2doc/test"
    "github.com/danimal141/rest-api-sample/api/router"
    "github.com/labstack/echo"
)

var server *test.Server

func TestMain(m *testing.M) {
    var err error
    r := router.NewRouter()
    test.RegisterURLVarExtractor(makeURLVarExtractor(r))
    server, err = test.NewServer(r)
    if err != nil {
        log.Fatal(err.Error())
    }

    // Start test
    code := m.Run()

    // Flush to an apib doc file
    server.Finish()

    // Terminate
    os.Exit(code)
}

func makeURLVarExtractor(e *echo.Echo) func(req *http.Request) map[string]string {
    return func(req *http.Request) map[string]string {
        ctx := e.AcquireContext()
        defer e.ReleaseContext(ctx)
        pnames := ctx.ParamNames()
        if len(pnames) == 0 {
            return nil
        }

        paramsMap := make(map[string]string, len(pnames))
        for _, name := range pnames {
            paramsMap[name] = ctx.Param(name)
        }
        return paramsMap
    }
}

こちらはドキュメント生成に必要な設定等を記述している。

ここで重要なのがvar server *test.Serverで、server.Finish()を呼ぶことでテスト時のリクエスト、レスポンスを元に.apibファイルを生成してくれる。

また test.RegisterURLVarExtractor(makeURLVarExtractor(r))はリクエストのURLに含まれるパラメータ関連の情報を教えてあげるためのもので、これを呼んでおかないとテスト実行時にPanicする。

具体的には /api/v1/users/1というリクエストで/api/v1/users/:user_idのテストをした場合、makeURLVarExtractorの返り値はmap[user_id:1] になる。そして/api/v1/users/{user_id}というエンドポイントのuser_idのExampleは1のような情報がドキュメントに反映される。

api/v1/users_test.go

package v1_test

import (
    "net/http"
    "testing"
)

func TestUsersIndex(t *testing.T) {
    url := server.URL + "/api/v1/users?pagination=false"
    res, err := http.Get(url)
    if err != nil {
        t.Errorf("Expected nil, got %v", err)
    }
    if res.StatusCode != http.StatusOK {
        t.Errorf("Expected status code is %d, got %d", http.StatusOK, res.StatusCode)
    }
}

func TestUsersShow(t *testing.T) {
    url := server.URL + "/api/v1/users/1"
    res, err := http.Get(url)
    if err != nil {
        t.Errorf("Expected nil, got %v", err)
    }
    if res.StatusCode != http.StatusOK {
        t.Errorf("Expected status code is %d, got %d", http.StatusOK, res.StatusCode)
    }
}

今回はエラーケースは省略しているが、例えば /api/v1/users?pagination=trueなどとしてテストすればドキュメントにBadRequestな感じで反映される。

ここまでで go test ./api/v1を実行するとapi/v1.apibが作成されるようになる。

api/all.apib

FORMAT: 1A

<!-- include(./v1/v1.apib) -->

一応all.apibを用意して、将来的に./v2/v2.apibなどを追加できるような構成を意識してみた。

APIドキュメント生成

gulpaglioを導入して、テストでapibが更新されるのをWatchしてHTMLを作成するようにする。

gulpfile.js

const gulp = require('gulp')
const aglio = require('aglio')
const gaglio = require('gulp-aglio')
const rename = require('gulp-rename')
const fs = require('fs')
const includePath = process.cwd() + '/api'
const paths = aglio.collectPathsSync(fs.readFileSync('api/all.apib', {encoding: 'utf8'}), includePath)

gulp.task('build', () =>
  gulp.src('api/all.apib')
    .pipe(gaglio({template: 'default'}))
    .pipe(rename('out.html'))
    .pipe(gulp.dest('doc'))
)

gulp.task('watch', () =>
  gulp.watch(paths, ['build'])
)

gulp.task('default', ['build', 'watch'])

あとはgulpを立ち上げつつ、サーバのテストを実行すればdoc/out.htmlが更新されるようになる。

ちなみに こんな感じのドキュメントが生成される。

まとめ

ほぼtest2docに助けられた感はありますが、テストによるAPIドキュメントの自動生成が実現できました。

サンプルコードを一応こちらにあげておきますので、何かしらのお役に立てれば幸いです。

参照