読者です 読者をやめる 読者になる 読者になる

スタイルガイドジェネレータHologramとBrowserStackでクロスブラウザチェックをする

こんにちは、デザインチームのミヤギ@_ringogirlです。

弊社では現在スタイルガイドの構築をHologramというgemを使って進めています。

構築の過程で機能の拡張が必要になりプラグインを作ることにしましたが、 Hologramのプラグインに関する記事が少ないため、作り方をまとめてみました。

今回はサンプルとしてBrowserStackを使って複数のブラウザからスクリーンショットを撮り、スタイルガイドに貼り付けるプラグインを作ってみます。

Hologram

Ruby製のスタイルガイドジェネレーターです。CSSに書いたコメントからスタイルガイドを生成してくれます。 SassやLESSにも対応しています。

trulia/hologram

Hologramのプラグイン

READMEには載っていませんがHologramはプラグインを作る機能を提供しています。 このプルリクエストでプラグインを作る機能が追加されました。 コメントにサンプルのプラグインもあるので、気になる方はぜひ参考にしてみてください。

BrowserStackを使ってスクリーンショットを撮るプラグインを作る

BrowserStackを使って複数ブラウザからCSSコンポーネントのスクリーンショットを撮り、ドキュメントに貼り付けるプラグインを作っていきます。

Hologramの初期設定や使い方については以下のブログ記事が参考になります。

プラグインファイルの準備

Hologramはrubyのファイルを直接指定してプラグインを読み込みます。 今回は設定ファイルと同じディレクトリにhologram_screenshot.rbというファイルを作成しました。 設定ファイルに以下のように追記します。

設定ファイル(hologram_config.yml)

plugins:
  - hologram_screenshot.rb

プラグインファイルの実装

プラグインにはフックできる場所いくつかあり、それらを使ってプラグインの処理を実装していくことになります。

まず、Hologram::pluginクラスを継承します。 クラス名はファイル名をアッパーキャメルケースにしないと読み込まれないので注意してください。

class HologramScreenshot < Hologram::plugin
  def initialize(config, args)
    @name = 'screenshot'

    super(config, args)
  end
end

@nameはプラグイン名で、後ほど使用します。

block(comment_block, file_name, has_plugin)

CSSのドキュメントコメントがパースされた後に実行されるフックです。 パースされた情報をもったcomment_blockや、コメントが書かれていたCSSのパスfile_nameが引数として与えられます。

スクリーンショットを撮るときにスタイルが適用されるように、ファイルを読み込みます。 今回はscssでスタイルを書いたので、トランスパイルしておきます。

def block(comment_block, file_name, has_plugin)
  @sass = Sass::Engine.for_file(file_name, {}).render
end

plugin(plugin_data, comment_block, file_name)

ドキュメントコメントに以下のように書くとその内容がplugin_dataに渡されます。 initializeで定義した@nameと同じものを[[[plugin:PLUGIN_NAMEの形式で記述します。

[[[plugin:screenshot
<button class="btn btn-primary">Primary Button</button>
<a class="btn btn-primary" href="#">Primary Link Button</a>
]]]

[[[plugin:PLUGIN_NAME]]]コメントはplugin関数の戻り値に置き換えられます。 今回はスクリーンショットを埋め込みたいので、<img>タグを文字列として返してみます。

def plugin(plugin_data, comment_block, file_name)
  # スクリーンショット用のhtmlを生成する
  build_html(plugin_data)

  # BrowserStack Screenshot APIでスクリーンショットを生成
  screenshots = @browserstack.screenshots

  # スクリーンショットから<img>タグを生成して返す
  # 内部で@sassを使ってスタイルを適用する
  build_styleguide_html(screenshots)
end

build_htmlではBrowserStackがスクリーンショットを撮るためのWebサーバーに表示するHTMLを作っています。 長くなってしまったため具体的なコードはここでは省きますが、処理としては以下の様なことをしています。

  • WEBrickをlocalhost:3000で起動する
  • BrowserStackLocalを起動し、localhostにアクセスできるようにする
  • Servletで/にアクセスしたときにplugin_dataを含むHTMLをbuild_htmlで作る (その時に@sassも埋め込んでスタイルを適用する)
  • BrowserStack Screenshot APIでhttp://localhost:3000/のスクリーンショットを撮る

finalize

プラグインの最後で実行されます。 pagesにはドキュメントとして生成されたHTMLファイル名や、comment_blockと同じ値が渡されています。

def finalize(pages)
  p "Complete screenshot."
  p "Generated screenshots were in #{pages.keys.first}."
end

プラグインファイル

省いたところもありますが、プラグインは以下のようになりました。 基本的にはこのような構成にするとプラグインが動作します。

require 'hologram'
require 'sass'

class HologramScreenshot < Hologram::plugin
  def initialize(config, args)
    @name = 'screenshot'

    super(config, args)
  end
  
  def block(comment_block, file_name, has_plugin)
    @sass = Sass::Engine.for_file(file_name, {}).render
  end
  
  def plugin(plugin_data, comment_block, file_name)
    # スクリーンショット用のhtmlを生成する
    build_html(plugin_data)

    # BrowserStack Screenshot APIでスクリーンショットを生成
    screenshots = @browserstack.screenshots

    # スクリーンショットから<img>タグを生成して返す
    # 内部で@sassを使ってスタイルを適用する
    build_styleguide_html(screenshots)
  end
  
  def finalize(pages)
    p "Complete screenshot."
    p "Generated screenshots were in #{pages.keys.first}."
  end
  
  ## WEBRickの起動
  
  ## WEBRickのServletで`/`アクセス時の処理
  
  ## BrowserStackLocalの起動
end

プラグインを実行してみる

ドキュメントの準備

ドキュメント用にbutton.scssを用意して以下のようにしました。 bootstrapを読み込んで、.btn-primaryの色を変えています。

button.scss

/*doc
---
title: Buttons
name: button
category: Components
---

ボタンの コンポーネントです。

```html_example
<button class="btn btn-primary">Primary Button</button>
<a class="btn btn-primary" href="#">Primary Link Button</a>
```

[[[plugin:screenshot
<button class="btn btn-primary">Primary Button</button>
<a class="btn btn-primary" href="#">Primary Link Button</a>
]]]

*/

.btn-primary {
  background-color: #e91e63;
  border-color: #c2185b;
}

本題からは逸れますが、上記のようにbootstrapのクラスを上書きするのは好ましくないので、bootstrap-sassなどを使ってSassの変数で上書きすると無駄にクラスを増やさないですみます。

プラグインの実行

実際にプラグインを動作させるには以下のコマンドを実行します。

hologram -c hologarm_config.yml --screenshot

@nameで設定した値をオプションとして渡すとプラグインがアクティブになります。

以下が実際にプラグインを動作させて生成されたドキュメントです。 (HologramのテーマであるCortanaを使っています。)

f:id:ringo_girl:20151112135517p:plain

HTMLのコードの下にそれぞれスクリーンショットが貼り付けられているのがわかると思います。 今回はWindows7 IE8.0Windows7 firefox37.0でスクリーンショットを撮ってみました。 コンポーネント単位ですが複数ブラウザでの見え方がわかるので、レイアウトの崩れなどが事前に発見できるかもしれません。

まとめ

Hologramは十分便利なのですが、プラグインで拡張していくことで、さらに便利で使いやすいスタイルガイドを作ることができるのではないでしょうか。

ベロシティを目標にする危険性について学んだことをアジャイルサムライ風に振り返ってみた

こんにちは、Forkwell事業部の中の人です。

本日は、つい最近 Forkwell Jobs の開発現場で起きたエピソードについてご紹介します。

きっかけはあるメンバーの一言でした。

「複数メンバーの加入から1ヶ月以上がたった、ベロシティもかなり安定してきた。よし、ここらでおれたちのベロシティをあげて、ビジネスサイドの連中をあっと言わせてやろうぜ!

この一言で息巻いた我々は、ベロシティをあげることを目標に設定し、ビジネスサイドのホワイトボードに ◯pt / 10pt のような数字を書き加えました

しかし、すぐに色々な現場を経験してきた他事業部のエンジニアの面々から、ベロシティをあげることを目標にすることには弊害があることを教わることになりました…

ベロシティを目標にする弊害

  • 目先のポイントを増やすためにとりあえずリリースして残タスクを先に積む事案が発生、後々の作業が滞る
  • 目先のポイントを増やすために、レビューが甘くなり、バグが増える
  • ポイントの見積もりが甘くなり、見積もり結果が信頼できなくなる

つまり、ベロシティを上げようとする行為が結果的に不正確なベロシティを招き、正確な見積もりができなくなる事態を招く可能性があるということでした。

ベロシティを目標指標にすることは推奨されていないらしい… でも、チームの生産性を向上させることには妥協したくない…

悩んだ我々は、アジャイルサムライのレビューにも参加していたという、当社のマスター・センセイである @idesaku に教えを請うことにしました。

マスター・センセイと熱心な弟子

弟子: ベロシティを上げることを目的にしてはいけない!とはいったものの、では何を指標にしたらいいのでしょうか?

センセイ: ベロシティをあげることを目的にするのではなく、それを落としている障害を取り除く発想が必要じゃ。 アジャイルでは、チームメンバーは常に全力で取り組んでいるように扱うし、実際にお前らは自主的に勉強し、成長しようとしておる。 はっぱをかけて全力を引き出すという発想自体がアジャイル思想にマッチしない。

メンバーが全力で頑張っている以上、できることは障害を取り除くことである。

弟子: ありがとうございます。

では、毎週振り返りで行っているKPTのP(Problem)で 自分たちのベロシティを下げているものがないか、洗い出すようにします。 その上で、取り除けるのもがあればそれをT(Try)として取り組むようにします。

センセイ: 加えて、コスト・納期がどれだけ短縮できるかを考えるより、今までと同じ期間で新しくできるようになったことがどれだけか、というのを考えることが効果的じゃ。

弟子: ありがとうございます。

では、毎週行っているKPTのK(Keep)で、 今までと同じ期間で新しくできるようになったことは何かを、洗い出すようにします。 そのKの蓄積が、自分たちの成長だと考えます。

弟子: 一方で、ベロシティ以外の指標で、我々がビジネスサイドにも、しっかりやっていることを伝える方法はないでしょうか。

センセイ: 成果を伝えることだ。ベロシティは成果ではない。成果とはFeature(顧客へ届けた価値)のことだ。

弟子: バグの改善やリファクタリングは成果として示せないのでしょうか。

センセイ: バグは、過去に書いたものが間違っていただけで、それを直したからといって成果とはいえない。 しかし、どれだけ業務をこなしたかを伝えるにはいいかもしれない。

弟子: ありがとうございます。

では、今後はどれだけポイントをつぶせたかではなく、 どんな価値を顧客に提供できたかを、ポストイットを貼ることで報告するようにいたします。

f:id:grooves:20151020104547j:plain

いかがでしたでしょうか。 今回の事例が、皆さんの現場の参考になれば幸いです。

参考図書

Forkwell Jobs で人事向けSlack連携機能(Slack Button)を実装した話

Ruby on Rails

こんにちは、Forkwell事業部の正徳です。 先日、Forkwell Jobsの採用担当者様向けにSlack通知機能をリリースしましたので、お知らせさせて頂きます。

f:id:tshotoku:20151001144051p:plain

また、Slack ButtonをRailsで実装する方法も後述しているので、エンジニアの方々にも参考になるかと思います。

どんな機能?

Forkwell Jobs では、ユーザーが掲載されている求人に対して「話を聞きたい」「応募」のアクションを行うことができます。

f:id:tshotoku:20151001144143p:plain

このアクションが発生したとき、採用担当者様にメール通知だけではなく、Slack にも通知する機能です。

なぜ作ったか?

もともと、通知にはメール通知しか使用できませんでした。 ほとんどの企業では採用活動のためにメーリングリストを作成し、そのメールアドレスを使用しています。

しかし、弊社groovesのようにSlackを全社導入している企業の場合、Slack通知の方が気づきやすいのではないでしょうか? ユーザーのアクションに素早く気付けることは大きな利点になると考え、試しに実装してみました。

テスト運用期間

実装できたので、まずは社内で1ヶ月ほどテスト運用をしてみました。

f:id:tshotoku:20151001144202p:plain

ヒアリング、ブラッシュアップ

弊社の採用担当者にヒアリングを行いました。その結果、下記のような感想を頂けました。

  • メールよりもSlackの方が気づきやすい
  • 面接の関係者への連絡がスムーズに進む
    • エンジニアにもエントリーがあったことがすぐに伝わる

また、他エンジニアからのフィードバックにも対応し、UI/UXをブラッシュアップしました。

  • 人事担当者がWebHook URLを直接入力するのは大変
    • 対応:「Slack Button」の導入
  • 通知のテストをAjaxで行いたい(初期verでは普通に画面遷移してました)
    • 対応: Ajax化

ちなみに、Slack Buttonを押した後の設定画面は下記のようになります。

f:id:tshotoku:20151001144210p:plain

締め

Slack通知機能の設定は人事向けの管理画面から簡単に設定できます。 Forkwell Jobsで求人を掲載している企業の採用担当者様はぜひお試しください。

(エンジニア向け)Slack Button の実装方法

せっかくなので、Slack Buttonを使ってみたいエンジニア向けに、導入手順もまとめておきます。参考にどうぞ。

まぁ、基本的にSlack Buttonの手順に沿って設定すれば動きます。

1. アプリ登録

Step1. の説明にあるように、Create your appからアプリ登録を行います。

f:id:tshotoku:20151001144221p:plain

ちなみに、この画面で入力した項目は後から変更が可能です。

2. Slack Button の配置

Step2 にタグが表示されるので、コピーして使います。

f:id:tshotoku:20151001144232p:plain

Forkwell Jobsではhamlを使ったり、 return_uri を使っている関係で少し変更して使いました。

%a.add_to_slack(href="https://slack.com/oauth/authorize?scope=incoming-webhook&client_id=#{Settings.slack.client_id}&redirect_uri=#{callback_admin_slack_notification_url(@company.user_name)}")
  %img(alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x")

3. WebHook URLの取得

Slack Buttonを押した後、ユーザーが Team や channel の選択をすると、 return_uri に戻ってきます。 戻ってきたらoauth.accessを参考に、WebHook URLを取得します。

  • Controller
def callback(code)
  redirect_uri = callback_admin_slack_notification_url(username: @company.user_name)

  if @company.update_slack_url_by(code, redirect_uri)
    notice = 'Webhook URLの設定に成功しました'
  else
    notice = 'Webhook URLの設定に失敗しました'
  end

  redirect_to edit_admin_slack_notification_path(username: @company.user_name), notice: notice
end
  • Model
def update_slack_url_by(code, redirect_uri)
  ret = false
  params = {
    client_id: Settings.slack.client_id,
    client_secret: Settings.slack.client_secret,
    code: code,
    redirect_uri: redirect_uri
  }
  uri = URI.parse "https://slack.com/api/oauth.access?#{params.to_query}"
  res = Net::HTTP.get(uri)
  json = ActiveSupport::JSON.decode res

  ret = update slack_url: json['incoming_webhook']['url'] if json['ok']
rescue => e
  Airbrake.notify e
ensure
  return ret
end

Chrome拡張の継続的デリバリー

Node.js

こんにちは、なんとかChrome拡張を公開できて、少し安心してる正徳です。

昨日、Forkwell JobsのChrome拡張の記事を公開しましたが、本記事では技術的な話を書いてみたいと思います。

リポジトリ

リポジトリはGitHubのgrooves/forkwell_for_chromeで公開しています。 実際のコードを参考にしたい方はどうぞ。

Haml, Sass, CoffeeScript を使う

社内の他エンジニア・デザイナーも触りやすいように、Forkwell Jobsで使用している技術に合わせました。

それぞれ、gulp.jsを使ってHTML, CSS, JavaScriptに変換しています。

mocha + power-assert を使ったテスト

mochapower-assertでテストを実行できるように環境を整えました。

詳細なテストの環境についてはリポジトリを読んで頂くとして、要点だけいくつか書いておきます。

mochaでクライアントサイドJavaScriptのテストをするとエラー

Chrome拡張のJavaScriptはHTML上で動く、普通のクライアントサイドJavaScriptです。 このため、Node.jsで動くmochaを使ってテストしても…

  • ReferenceError: window is not defined
  • Error: jQuery requires a window with a document

などのエラーが出てしまいます。 また、クライアントサイドのJavaScriptでは globalmodule.exports も使えません。

browserify + jsdom を使う

上述した問題に対応するため、Browserify を使い、クライアントサイドのJavaScriptでもNode.jsっぽく書けるようにしました。 また、jQueryがwindowを使うためtmpvar/jsdomも導入しておきます。

これにより、requireを使ったり、global.$をテスト側で上書きしたりが出来るようになります。

こんな感じ。

global.$ = require('jquery')(require('jsdom').jsdom().parentWindow)
Service = require 'src/coffee/models/service'

プロジェクトルートをNODE_PATHに追加

テストを書く際にrequire '../../src/coffee/models/service'のように書くと分かりづらいので、プロジェクトルートをNODE_PATHに追加しています。

test/mocha_resolve_test_path.coffee

path = require 'path'

process.env.NODE_PATH = [
  process.env.NODE_PATH,
  path.resolve(__dirname, '..')
].join(':')

require('module').Module._initPaths()

これをmochaの実行時に引数で渡すと効くようになります。

$ mocha --require test/mocha_resolve_test_path.coffee

Coveralls の設定

カバレッジを出したくて導入しました。少しハマった所があったので、そこだけ書いておきます。

istanbul-jsがCoffeeScriptに対応していない

JavaScriptでカバレッジを計測するためにistanbul-jsを導入したのですが、CoffeeScriptではうまく動きませんでした。 これ解決するためにはbenbria/coffee-coverageを別途インストールする必要があります。

gulpfile.coffeeを無視する

coffee-coverageのデフォルト設定だとgulpfile.coffeeのカバレッジまで測定されるため、これを対象外にします。

coffee-coverage-loader.js

var coffeeCoverage = require('coffee-coverage');
var coverageVar = coffeeCoverage.findIstanbulVariable();
var writeOnExit = coverageVar == null ? true : null;

coffeeCoverage.register({
    instrumentor: 'istanbul',
    basePath: process.cwd(),
    exclude: ['/test', '/node_modules', '/.git', '/gulpfile.coffee'],
    coverageVar: coverageVar,
    writeOnExit: writeOnExit ? ((_ref = process.env.COFFEECOV_OUT) != null ? _ref : 'coverage/coverage-coffee.json') : null,
    initAll: (_ref = process.env.COFFEECOV_INIT_ALL) != null ? (_ref === 'true') : true
});

デフォルト設定であるcoffee-coverage/register-istanbulの代わりに、作成したcoffee-coverage-loader.jsの設定を読み込むようにします。

$ mocha --require test/coffee-coverage-loader.js

Google APIを使い、Chromeウェブストアに自動デプロイ

Chrome拡張のzipファイルを作成し、手動でアップロードするのは非常に面倒なので、Google APIと werckerで自動デプロイの環境を作りました。

Google Developers Consoleの登録

まず、Google APIを使うためにGoogle Developers Consoleでプロジェクトを登録します。

手順はUsing the Chrome Web Store Publish API - Google Chromeに書いてあるので、この手順通りに作業を進めるとOAuth 2.0のCLIENT_IDCLIENT_SECRET, REFRESH_TOKENが手に入ります。

アクセストークンの有効期限

Google APIのアクセストークンの有効期限は 40分 です。 このため、毎回のデプロイ時にアクセストークンを再取得する必要があります。

参考にしたAPIのドキュメント一覧

wercker.ymlの例

werckerで以下の環境変数を設定してあります。

環境変数の名前 内容
GITHUB_TOKEN GitHubのpush権限のあるユーザーのtoken
APP_ID Chromeストアに公開しているアプリID
GOOGLE_APP_ID Google API の CLIENT_ID
GOOGLE_APP_SECRET Google API の CLIENT_SECRET
GOOGLE_REFRESH_TOKEN Google API の REFRESH_TOKEN

wercker.yml

deploy:
  steps:
    - script:
        name: get app_version from gulp
        code: export APP_VERSION=$(npm run app_version --silent)
    - script:
        name: configure git
        code: |
          git config --global user.email "pleasemailus@wercker.com"
          git config --global user.name "wercker"
          git remote set-url origin https://$GITHUB_TOKEN@github.com/$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY.git
    - script:
        name: create tag
        code: git tag $APP_VERSION -m "$APP_VERSION release"
    - script:
        name: push tag
        code: git push origin $APP_VERSION
    - github-create-release:
        token: $GITHUB_TOKEN
        tag: $APP_VERSION
    - github-upload-asset:
        token: $GITHUB_TOKEN
        file: dist-$APP_VERSION.zip
    - script:
        name: Refresh a access token
        code: |
          JSON=`curl -d "client_id=$GOOGLE_APP_ID" -d "client_secret=$GOOGLE_APP_SECRET" -d "refresh_token=$GOOGLE_REFRESH_TOKEN" -d "grant_type=refresh_token" https://www.googleapis.com/oauth2/v3/token`
          export GOOGLE_APP_TOKEN=`node -p "($JSON).access_token;"`
    - script:
        name: Upload a package to update an existing store item
        code: |
          curl -H "Authorization: Bearer $GOOGLE_APP_TOKEN" -H "x-goog-api-version: 2" -X PUT -T dist-$APP_VERSION.zip -v https://www.googleapis.com/upload/chromewebstore/v1.1/items/$APP_ID
    - script:
        name: Publish an item to the public
        code: |
          curl -H "Authorization: Bearer $GOOGLE_APP_TOKEN" -H "x-goog-api-version: 2" -H "Content-Length: 0" -X POST -v https://www.googleapis.com/chromewebstore/v1.1/items/$APP_ID/publish

こんな感じのwercker.ymlを作成すると、自動で

  • GitHub のTagの作成
  • GitHub のReleasesの作成
  • Chromeウェブストアにzipのアップロード
  • Chromeウェブストアで新しいバージョンを公開

が行われます。便利です。

まとめ

今回のChrome拡張は出来たばかりで、これから少しずつ改良していきたいと思っています。

要望などあれば是非、Twitterで#forkwellのハッシュタグを付けてツイート頂くか、公式アカウントの@Forkwell_jaにご連絡ください。

最後になりましたが、この記事がChrome拡張を作る方、作っている方の参考になれば幸いです。

Chrome拡張からForkwell Jobsの求人を検索できるようになりました

こんにちは、正徳です。

本日、Forkwell JobsのChrome拡張をChromeウェブストアで公開しました。

インストール方法

ChromeウェブストアのこのページからChromeに追加してください。

Chrome拡張で何ができるの?

スクリーンショット

f:id:tshotoku:20150511212131p:plain

主な機能

主な機能は下記の2つです。

  1. Forkwell Jobs の求人の検索
  2. 閲覧ページに関連する求人の表示

1. 求人の検索

キーワード検索ができるようになりました。 検索すると、Forkwell Jobsの検索結果のページが開きます。

2. 閲覧ページに関連する求人の表示

閲覧ページの運営会社の求人を表示します。

例えば、画像のように NewsPicksのサイトを閲覧していると、 NewsPicksの運営会社である 株式会社ユーザベース 社の求人が表示されます。

f:id:tshotoku:20150511211854p:plain

うまく動かない、要望があるとき

Twitterで#forkwell のハッシュタグを付けてツイートするか、公式アカウントの@Forkwell_jaにご連絡ください。

もしくはGitHubのIssuesに登録して頂いても大丈夫です。

むしろ、自分で機能を直したい(追加したい)

ソースコードはgrooves/forkwell_for_chromeで公開しております。

READMEの更新から、機能の追加まで、プルリクエストお待ちしております。

いまさら聞けないfactory_girl入門

Ruby on Rails

thoughtbot/factory_girlを活用して日々テストを書いているRailsエンジニアの方々、こんにちは、正徳です。

Forkwellでもfactory_girlを使ってテストを書いています。 このfactory_girlには便利機能が多く、とても使いやすいのですが、女心並みに複雑*1なので、一通りの機能をブログにまとめてみました。

factory_girl初心者から、中級者の参考になれば幸いです。

参考にしたページ

このブログの内容は全てfactory_girlのGETTING_STARTEDに記載されています。

読まれた事のない方は、是非一読する事をおすすめします。

目次

factory_girlのインストールと設定

factory_girlをRailsで使う場合のインストール方法と設定を紹介します。

インストール

いつものように、Gemfileにfactory_girl_railsを追加し、bundle installします。

group :development, :test do
  gem "factory_girl_rails", "~> 4.0"
end

本記事を書いた時のGETTING_STARTEDには"~> 4.0"と書かれていましたが、最新のバージョンが変わっている可能性があります。 インストールする前に最新のバージョンを確認した方が良いです。

設定

GETTING_STARTEDに従って設定します。

# spec/support/factory_girl.rb
RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end

spec/support/*のファイルはデフォルトで読み込まれないため、rails_helper.rbのコメントになっている箇所を修正します。

# spec/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

factory_girlの使い方(Using factories)

user has_many jobs through favoritesというシンプルなモデル構造を例にして、FactoryGirlの説明をしていきます。

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :jobs, through: :favorites
end

# app/models/favorite.rb
class Favorite < ActiveRecord::Base
  belongs_to :user
  belongs_to :job
end

# app/models/job.rb
class Job < ActiveRecord::Base
end

build

factoryの対象はDBに保存せず、 関連するレコードをDBに保存する という振る舞いのストラテジです。 バリデーションのテストなど、関連レコードがDBに保存されている必要があるテストで使うと良いです。

favorite = build :favorite
#<Favorite id: nil, ...>
favorite.user
#<User id: 1, ...>

create

factoryの対象、関連レコードの 両方ともDBに保存する という振る舞いをするストラテジです。 whereなどDBからデータを取得するようなテストで使用するのが良いです。

favorite = create :favorite
#<Favorite id: 1, ...>
favorite.user
#<User id: 1, ...>

attributes_for

factoryの対象の属性を Hashで返す という振る舞いをするストラテジです。

user_attrs = attributes_for :user
{:user_name=>"tshotoku", :admin=>true, :first_name=>"Shotoku", ...}

これはコントローラのテストでpostする時に便利です。

context 'with valid attributes' do
  before { post :create, user: attributes_for(:user) }
  it { expect(response).to be_redirect }
end

context 'with invalid attributes' do
  before { post :create, user: attributes_for(:user, first_name: nil) }
  it { expect(response).to render_template(:new) }
end

build_stubbed

factoryの対象、関連レコードの両方とも DBに保存しない という振る舞いをするストラテジです。 DBのアクセスを必要とせず、インスタンスの属性だけでテストが可能な場合に使うと良いです。

ちなみに、idには適当な値が設定されます。

favorite = build_stubbed :favorite
#<Favorite id: 1003, ...>
favorite.user
#<User id: 1001, ...>

遅延評価(Lazy Attributes)

属性に現在時刻やメソッド呼び出しなどを使う場合、遅延評価する必要があります。

factory :job do
  # ...
  published_at { Time.now }
  pending_mail_token { Job.generate_token }
end

関連(Associations)

belongs_toの関連とfactoryが同名の場合、シンプルに関連を定義することができます。

factory :job do
  company
end

factoryと属性の名前が異なる場合、属性を上書きしたい場合などは下記のような指定も可能です。

factory :job do
  association :author, factory: :company, name: 'grooves'
end

ストラテジの変更

buildを使う際に関連レコードをDBに保存したくない場合、ストラテジを指定することができます。

association :author, factory: :company, strategy: :build

後述する特性(Traits)と組み合わせることで、通常のテストでは関連レコードを作り、特定のテストでは関連レコードを作成しない、といった制御も可能です。

factory :job do
  association :author, factory: :company

  trait :with_new_author do
    association :author, factory: :company, strategy: :build
  end
end

job = build :job
job.new_record? # => true
job.author.new_record? # => false

job_with_new_author = build :job, :with_new_author
job_with_new_author.new_record? # => true
job_with_new_author.author.new_record? # => true

連番(Sequences)

ユニークキー制約などで属性を変えたい場合、factory_girlによって連番を生成することができます。

factory :user do
  # person_1, person_2, person_3, ...
  sequence(:email) { |n| "person_#{n}@example.com" }
end

ちなみに、初期値を変えたり、数値以外を使うこと*2も可能です。

初期値を変える例

factory :user do
  # person_1000, person_1001, person_1002, ...
  sequence(:email, 1000) { |n| "person_#{n}@example.com" }
end

数値以外を使う例

factory :user do
  # person_a, person_b, person_c, ...
  sequence(:email, 'a') { |n| "person_#{n}@example.com" }
end

コールバック(Callbacks)

下記4つのコールバックがあり、各処理の前後で特定の処理を行うことができます。

  • before(:create) - create の実行前
  • after(:build) - build, create の実行後
  • after(:create) - create の実行後
  • after(:stub) - build_stubbed の実行後
factory :user do
  after(:build) { |user| generate_hashed_password(user) }
end

特性(Traits)

複数の属性をグループ化し、factoryに適用できます。

factory :job do
  title 'Title'

  trait :published do
    published_at { Time.now }
    title { "Job published at #{published_at}" }
  end

  trait :unpublished do
    published_at nil
  end

  trait :general_salary_range do
    salary_lower 400
    salary_upper 600
  end

  trait :rich_salary_range do
    salary_lower 700
    salary_upper 1_000
  end
end

Traitは組み合わせて使う事ができます。

job1 = create :job, :published, :general_salary_range
#<Job id: 1, title: "Job published at 2015-04-28 12:03:28", published_at: "2015-04-28 12:03:28", salary_lower: 400, salary_upper: 600>
job2 = create :job, :unpublished, :rich_salary_range
#<Job id: 2, title: "Title", published_at: nil, salary_lower: 700, salary_upper: 1000>

一時的な変数(Transient)

本来のクラスには存在しないが、Factoryでのみ使いたい一時的な変数を定義することができます。 これはコールバックと組み合わせる時に便利です。

factory :user do
  transient do
    friends_count 0
  end

  after(:create) do |user, evaluator|
    if evaluator.friends_count > 0
      create_list :friendship, evaluator.friends_count, user: user
    end
  end
end

複数レコードの生成(Building or Creating Multiple Records)

複数のレコードを生成したいときに使います。

built_users = build_list :user, 25
created_users = create_list :user, 25

また、createなどと同じく属性を上書きすることもできます。

# 10人の紳士が作られる
gentlemen = create_list :user, 10, age: 30, gender: :man

ActiveRecord以外での利用(Custom Construction)

一般的なRubyのクラス、ActiveModelのクラスでもinitialize_withを使うことでfactory_girlを利用することができます。

# app/models/user.rb
class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

# spec/factories/users.rb
factory :user do
  name "talkto_me"

  initialize_with { new name }
end

build(:user).name
#<User name: "talkto_me">

ActiveModelのように、コンストラクタにハッシュを渡せる場合はattributesを使うことも出来ます。

factory :user do
  name "talkto_me"
  age 17

  initialize_with { new attributes }
end

build(:user)
#<User name: "talkto_me", age: 17>

*1:女心並みって書いてあるけど、女心が分かる訳ではない

*2:#nextをサポートしているオブジェクトが指定可能です

Go言語で働かナイト!に運営として参加しました

エンジニアチームの @talkto_me です。 4/9(木)に弊社主催のエンジニア向けキャリアイベントの Go言語で働かナイト! Forkwellキャリア談義 #3 に運営として参加しました。

株式会社エウレカエンジニアマネージャーの金子様、株式会社白ヤギコーポレーションCEOのシバタ様、株式会社はてなチーフエンジニアの松木様、また、パネルディスカッションのモデレータとして、株式会社Gunosyの横道様という錚々たるメンバーに登壇いただき大盛況のイベントとなりました。

詳細についてはイベントレポートを公開しておりますが、エンジニアとして興味深かった内容について紹介させていだきます。

学習コストが低い

Go言語の学習コストの低さについて度々言及していました。 公演で紹介のあった A tour of Go をやってわかったのですが、言語仕様は複雑ではなく、書いていてとっつきやすさを感じました。フォーマットを統一していること、標準で様々なコマンドを用意していること、各エディタのプラグインを公開していることも導入のしやすさの一因だと思います。もちろん、開発時はGo言語の考え方に則った設計をする必要があるため、様々な試行錯誤が必要になります。

Go言語を習得することでプログラミングにおける低レイヤの知識も習得できるため、チームとしての開発力も上昇するという意見もありました。

A tour of Go は日本語版もありますので、興味のある方はぜひ取り組んでみてください。

日本語のドキュメント不足

各社ともに国内のGo言語の開発をリードしている企業様でしたが、Go言語で開発する上で、日本語の情報量の少なさは障壁の一つになっているという懸念を抱いていました。

実際、中国語のドキュメントにしか最新のことが書いていない、というケースも紹介いただき、実運用となると気が引けてしまうこともあるかもしれません。

それでも、Go言語の勉強会やGo Conferenceなどを通じて国内コミュニティの活動も活発になってきていることもあり、積極的に改善しようとする意識を感じました。

モバイルアプリケーションの開発言語としての可能性

Go1.4で部分的にAndroidのサポートが始まりました。 Go言語を開発した企業がGoogleであることや、Go言語の開発環境としてIntelliJ IDEAが対応していることもあり、Go言語によるモバイルアプリケーション開発の時代も遠くないのではないか、という話がありました。 クロスコンパイルでiOSもサポートされることになれば、モバイルアプリケーション開発における標準言語となっていく可能性もあります。

最後に

Forkwell Jobsでは4/20現在、登壇いただいた白ヤギコーポレーション様、Gunosy様等Go言語による求人を掲載しております。ぜひご覧ください。 また、今後も様々なキャリア談義を行っていきますので、ご興味のある方はForkwellキャリア談義のイベントページよりグループにご参加いただきますと、イベント情報を受け取ることができます。

最後になりますが、登壇者の皆様、会場を提供していただいたSansan株式会社様、ありがとうございました。