開発備忘録 2018/10/25

RSpec で example の一覧を出力する

RSpec の -f (--format) オプションを使って出力形式を指定します。
指定できる形式として p (progress) や d (documention) などがありますが、今回は d を指定します。(デフォルトは p)

bundle exec rspec -f d spec/controller/hoge_controller_spec.rb

GET #index
  renders the :index template

他にも色々オプションがあるみたいですが、まだ試せていないので後日書きます。

Faker でダミーデータをつくる

github.com

ランダムな数字

Faker::Number.number(10) # 10桁のランダムな数字の組み合わせ

=> "1968353479"

ランダムな文字列

Faker::Lorem.characters(10) # 10桁のランダムな英数字の組み合わせ

=> "ang9cbhoa8"

他にも Faker::Games::Pokemon や Faker::Games::Zelda など、変わり種がたくさんありますが、ドキュメントを検索する上ではノイズですね……。

開発備忘録 2018/10/24

MySQL で SELECT の結果に対して UPDATE を適用する

statustemp のレコードを全て active にする。

UPDATE
  users,
  (
    SELECT users.id
    FROM users
    WHERE status = 0 # 0: temp, 1: active, 2: leaved
  ) AS temp_users
SET users.status = 1
WHERE users.id = temp_users.id

FactoryGirl の transient について

FactoryGirl において一時的な attribute を定義することができる。
transient で定義した attribute はモデルを build (create) した際にセットされることはなく、また attribute_for で attribute を取り出すときには無視される。

何のためにこの機能が提供されているかというと DRY に書くためである。
例えば、以下の FactoryGirl のコードを DRY にする方法について考えてみる。

factory :character do
  name { "Jack (job)" }

  trait(:knight) do
    name { "Jack (knight)" }
  end

  trait(:fighter) do
    name { "Jack (fighter)" }
  end

  trait(:wizard) do
    name { "Jack (wizard)" }
  end
end

job に応じて name の表記が微妙に変わるというものだが、今回の場合だと name { "Jack" } の部分が重複していて DRY ではない。
これが transient を使うとこう書ける。

factory :character do
  transient do
    job "job"
  end

  name { "Jack (#{job})" }

  trait(:knight) do
    job "knight"
  end

  trait(:fighter) do
    job "fighter"
  end

  trait(:wizard) do
    job "wizard"
  end
end

job を transient に定義しておいて、trait で job の値を更新するというわけだ。
ちなみに job の値を外から渡したい場合はこう書く。

create(:character, job: 'thief')

また callback で transient を利用したい場合は第二引数が transient なのでそれを利用する。

factory :character do
  ...

  after(:create) do |character, evaluator|
    character.name += " #{evaluator.job}"
  end
end

開発備忘録 2018/10/23

Web サイトを訪れたユーザーを特定するための方法

見出しの内容について本日相談を受けました。
私は Web にはあまり詳しくはないのですが、色々調べたので今日はそれについて書いていこうと思います。

Cookie

まずは Cookie を利用した方法です。

サイトへのアクセス時にランダムな文字列を生成し、Cookie へセットします。
ユーザーは次回のアクセス時にその Cookie を渡して通信を行うので、その Cookie の値からユーザーを判断するといった方法です。

少し前までは広告からアクセスしてきたユーザーを特定するためにこの方法が使われたりしていましたが、最近はサードパーティ Cookie (違うドメインCookie) を禁止するブラウザが増えてきたので、あまり使われなくなっているみたいです。

ブラウザフィンガープリント (デバイスフィンガープリント)

フィンガープリントを利用した方法です。

そもそもフィンガープリントって……?

フィンガープリントとはコンテンツの内容をハッシュ化したハッシュ値のことで、コンテンツの改ざん、すり替えを防ぐために使われます。
例えば、メールを送信するときに、送信者は本文の内容のハッシュ値を本文に加えて送信します。そして受信者は受け取ったメールの本文をハッシュ化し、それを送られてきたメールにくっついてきたハッシュ値と同値であるかを見ることで第三者に改ざんされていないかをチェックすることができます。

ブラウザフィンガープリントも考え方としてはほぼ同じで、こちらはブラウザから取得できる情報 (ブラウザの名前、OS のバージョン、使用しているプラグインの情報など) を組み合わせてハッシュ化したデータのことを指します。

ブラウザフィンガープリントは情報の組み合わせ方次第ではかなり高い精度でユーザーを区別することができるようです。
ただ OS のバージョンが上がったりするとブラウザフィンガープリントの値も変わってきてしまうので、厳密にユーザーを区別したい場合にはこの方法は向かないと思います。

端末識別子

端末識別子 (MACアドレスや UDID) を使って区別する方法ですが、こちらはオススメしません。

iOS に関しては Apple が公式に端末の UDID や MACアドレスの取得を禁止していますし (API は残っているが無効な文字列を返す・使用するだけでリジェクト対象)、AndroidMACアドレスの取得は現在できないようになっています。

昔はこの方法を取ることもできたという、一応ご紹介です。

開発備忘録 2018/10/22

[RSpec] テストを保留にする skip と pending の違い

skip

example は実行されず、そのステータスは pending になります。

describe "an example" do
  it "is skipped" do
    skip
  end
end

=> 1 example, 0 failures, 1 pending

pending

example でエラーが発生するとそのステータスは pending となり、パスすると逆にステータスが failure になります。

describe "an example" do
  it "is implemented but waiting" do
    pending("something else getting finished")
    fail
  end
end

=> 1 example, 0 failures, 1 pending

describe "an example" do
  it "is implemented but waiting" do
    pending("something else getting finished")
    expect(1).to be(1)
  end
end

=> 1 example, 1 failure

使い分け

何らかの事情でいまはパスさせることができないけどいつか修正するよ、といったテストには pending を使い、それに当てはまらない場合は skip を使うのがいいと思います。

任意のクラスを継承しているかを調べる

ActiceRecord を継承した User クラスが存在し、それが ActiveRecord::Base を継承するのかを知りたい。

class User < ActiveRecord::Base; end

is_a?

オブジェクトが引数で受け取ったクラスのインスタンスであれば true、そうでなければ false を返します。

user = User.find(1)

user.is_a?(ActiveRecord::Base)
=> true

ancestors.include?

インスタンスからではなく、クラス自体が任意のクラスを継承しているかを知りたいとき。
ちなみに ancestors メソッドは親クラスを配列にして返します。

User.ancestors.include?(ActiveRecord::Base)
=> true

開発備忘録 2018/10/19

Ruby の代入演算子 ||=

Ruby の実装でたまに見かけるこの記述について調べてみた。

user ||= fetch_user

上記の式は user が未定義だと fetch_user が呼び出されて user に代入され、そうでないなら user を返す。
何故こうなるのかが分からなかったが、どうやらこの式は以下のように展開されるらしい。

user || (user = fetch_user)

Ruby の場合 || は左辺が真なら右辺は評価しないという性質があるので、user が定義済みなら user を返し、そうでないなら fetch_user が呼び出され代入される、という仕組みになっていたようだ。

RSpec のモック化

Twitter からユーザーのツイートを取得するクライアントが実装されており、それを使ってユーザーのツイート一覧を表示する Controller があるとする。

class TweetsController < ApplicationController
  def index
    twitter_client = TwitterClient.new
    render json: { tweets: twitter_client.fetch_tweets }
  end
end

この Controller のテストを書くときに、ツイートを取得する部分 (twitter_client.fetch_tweets) をモック化する必要があるが、RSpec の場合はこう書く。

tweet_mock = %i[
  'こんにちは'
  'はじめまして'
]
allow(twitter_client).to receive(:fetch_tweets).and_return(tweet_mock)

これにより fetch_tweetstweet_mock を返すようになり、テストが書けるようになる。
ちなみに allowexpect と書くこともでき、両者の違いは allowfetch_tweets が呼びだされていなくてもエラーにはならないが、expect の方は fetch_tweets が呼び出されていないとエラーになる。

Ruby における Hash の定義方法

{'key' => 'value'}
=> {"key"=>"value"}

{:key => 'value'}
=> {:key=>"value"}

{key: 'value'}
=> {:key=>"value"}

最後の書き方は Ruby 1.9 以降の書き方ですが、いまはもうこの書き方が主流な感じがしますね。
最初と最後の書き方では key が文字列になるかシンボルになるかの違いがありますが、可能なら key はシンボルで定義した方がいいと思います。

Ruby のシンボルはただの数値 (:key なら 256348 という感じ) なので、文字列の生成と比べてコストが低いです。
またシンボルは何回呼び出してもひとつしか生成されない (皆んなが 1 つのオブジェクトを参照する) ので、そういった意味でもシンボルの方がパフォーマンスが高いです。

以上の理由から、hash を定義するときは key はなるべくシンボルで定義した方がいいですね。

開発備忘録 2018/10/18

acts_as_paranoid で Rails に soft delete を追加する

ActiveRecorddestroy は物理削除でモデルを削除しようとします。
acts_as_paranoid という gem を使うことでこの挙動を論理削除に変えることができます。

github.com

使い方は非常にシンプルでモデルに acts_as_paranoid を追加するだけ。

class Paranoiac < ActiveRecord::Base
  acts_as_paranoid
end

これにより、destroy を呼び出すと物理削除ではなく、deleted_at カラムに timestamp を記録するような挙動に変わります。
ちなみに deleted_at 以外のカラムも指定可能で、また型も time 以外にも booleanstring を指定することが可能です。

destroy の挙動の変化以外にも default_scopedeleted_at が NULL 以外を取得するようになります。
削除済みのレコードも取りたい場合は Paranoiac.with_deleted で取得が可能です。

この他にも色々機能があるみたいですが、それはまた追々記事していこうかと思います。

ところで acts_as_paranoid と似たような機能を提供する gem に paranoia というものがありますね。

github.com

Paranoia is a re-implementation of acts_as_paranoid for Rails 3/4/5

と書いているのでたぶん paranoia の方が後発だけど、いまは acts_as_paranoid も Rails 4/5 サポートしてますし、結局どっちがいいんでしょう……。

開発備忘録 2018/10/17

In-App Purchase における自動更新型サブスクリプションの状態更新通知について

In-App Purchase では、サブスクリプションの状態に関する変更を App Store が通知してくれます。(※自動更新型のみ)
開発者はその通知を受け取り適切な処理を行うことで、サブスクリプションの状態を正しく更新することができます。
(以前はこの状態の更新を通知してくれる仕組みがなかったため、自分たちのサーバーから定期的に Apple のサーバーにポーリングする仕組みを整える必要がありました。)

App Store が POST してくれる通知の種類は以下の 5 つです。

種類 説明
INITIAL_BUY 初回の購読
CANCEL Apple カスタマーサポートによって購読がキャンセルされた
RENEWAL 期限切れになっていた購読が再び更新された
INTERACTIVE_RENEWAL 期限切れになっていた購読をユーザーが意図的に更新した
DID_CHANGE_RENEWAL_PREF プランを変更した

それぞれの通知について詳しく解説します。

INITIAL_BUY

アプリケーションが提供するプランをユーザーが初めて購読した際に通知されます。

CANCEL

Apple カスタマーサポートが購読をキャンセルした際に通知されます。
例えば、子供が誤ってサブスクリプションを登録してしまった場合などに、Apple に問い合わせて事情を説明することで購読をなかったことにしてくれるみたいですが、恐らくその際に通知されるものだと思われます。(実際に試したわけではないので、間違っていたらすみません……。)

ちなみにユーザーが設定アプリから自発的に購読をキャンセルした場合は通知されません。

RENEWAL

決済に不備があり購読が中断された後、Apple は何度か決済のリトライを行います。
その間にユーザーが決済情報を変更し、購読の更新に成功した場合にこの通知が POST されてきます。

INTERACTIVE_RENEWAL

期限切れになっていた購読をユーザーが自発的に更新した際に通知されます。
例えば期限切れになった後に、アプリケーションのプランの購入画面から再度購入した場合などに通知されます。

また、購読の更新に失敗し、期限切れになった場合にも通知されます。

DID_CHANGE_RENEWAL_PREF

プランを変更した際に通知されます。

自動更新型のサブスクリプションサブスクリプション間にランク付けを行うことができます。
例えば、ベージックプランやプレミアムプランといった感じでグレードに違いをもたせます。

ちなみにグレードがベーシック < プレミアムだとして、ベーシックからプレミアムに移行した場合は「アップグレード」といって、即座にプランが変更されます。
一方でプレミアムからベーシックに移行した場合は「ダウングレード」といい、こちらは次回の購読の更新時にプランが切り替わります。
どちらの場合でも POST されてくる通知の種類は同じ (DID_CHANGE_RENEWAL_PREF) です。