こんにちは、エンジニアの神山です。
最近、テストカバレッジを上げるためRSpecを書きまくっています。ちなみに最初は90%でしたが、苦闘の末95%まで上がりました。結構骨が折れましたね。
その中でも大変だったのがFacebookログイン部分のテストです。外部APIを使っており、そこの部分のテストの書き方が分からなくて悩んでいました。
色々と調べてみるとモックを使うとうまいことテスト出来るよという文献を見つけました。
ということで今回は、外部API部分のテストの問題点、モックとは何か、またそれをどのようにテストに使うのかにフォーカスして記事を書きました。
外部API部分のテストの問題点
今回悩んだのは外部APIを使用している部分のテストをどのように書くかということです。
例えば、「ログインしようとしているユーザーのFacebookのアカウント情報を取得し、すでにDBに登録されていればログインさせる」ということです。
しかしこのテストを行うには、実際にFacebookAPIよりアカウント情報を取得しなくてはいけません。
しかしテストで外部APIを利用することには、幾つかのデメリットがあります。
- 外部APIやその周辺で、予測や対応ができないエラーが起きることがある
- 外部APIの使用に制限があったり、金銭などが発生する場合がある
- 外部APIに紐付いているプロダクトにリアルに影響が起こる(Facebookの場合、実際に投稿がされてしまうなど)
では、どうすれば外部APIを利用せずに、外部API部分を使用している部分のテストを出来るのでしょうか。
そのようなときに使うのが、「モック」です。
モックとは何か。
モックを直感的に説明すると、「本物のふりをするニセモノのプログラム」です。下記の記事の言葉を使わせて頂きました。
例えばユーザーのFacebook情報を取得するとき、FacebookAPIにリクエストが発生したら予め用意したニセモノのプログラムを呼ぶようにすることが出来ます。
また「aが呼ばれたときに本当はbを返すのだけど、ここのテストだけはcを返したい」というときに使用したりもします。
つまりモックとは、外部APIなどテストを行うために必要だけれど使用できない、再現することが難しい場合に、その役割を担ってくれるものです。
ちなみにモックとスタブの違いは?
モックと似た言葉にスタブというものがあります。気になって調べてみたのですが、両者の違いは使用目的だそうです。基本的にどちらもニセモノを作り出すことには変わらないようです。
ただその違いを明確に理解するのはなかなか難しく、またあまり意識する必要もないという意見もあったので、今回は保留にしました。時間があるときに色々と調べてみようと思います。
ちなみに、分かりやすく書いてある記事があったので、載せておきます。
モックの準備
`Omniauth`部分のテストに関しては以下に従っております。
まずモックにすべきところを整理しましょう。
今回はユーザーのFacebook情報を取得する際にFacebookAPIを用います。そしてFacebookに登録されている名前やEmailを取得します。
そのため、今回モックにするところはFacebookAPIにリクエストがあったときに返す部分です。
以下をHelperに記します。
def facebook_mock(name, email)
OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new(
provider: 'facebook',
uid: 1234567890,
info: {
name: name,
email: email
},
credentials: {
token: 'hogepiyo1234'
}
)
end
OmniAuth.config.test_mode = true
たとえば、`facebook_mock('foo', 'bar')`とすれば、以下の値が得られます。
{ provider: 'facebook', uid: 1234567890, info: { name: 'foo', email: 'bar' } , credentials: { token: 'hogepiyo1234' } }
また`OmniAuth.config.test_mode = true`はOmniauthを用いたテストの際に必要になるので、一緒に記述しておいてください。これを記述すると、FacebookAPIにリクエストが送られそうになると、それを中止してすぐにコールバックしてくれます。
RSpec
では、Facebookログイン部分の一連のテストを書いていきます。
まず、ユーザーを作成します。
let(:user) { User.create(name: 'hoge', email: 'hoge@hoge.com') }
テストの前に`OmniAuth.config.mock_auth[:facebook]`を初期化し、`facebook_mock`をセットします。
OmniAuth.config.mock_auth[:facebook] = nil
Rails.application.env_config['omniauth.auth'] = facebook_mock(
name: user.name,
email: user.email
)
準備はこれだけです。では実際にFacebookログインをしてみます。今回は分かり易くボタンを押してFacebookログインをする形式にしてみました。
click_link 'Facebookを利用してログインする'
さて、ボタンが押されると通常はFacebookAPIにリクエストが走りますが、テストではすぐにコールバックされます。
ただモックのおかげで、`request.env['omniauth.auth']`には下記の値が入っております。
{ provider: 'facebook', uid: 1234567890, info: { name: 'hoge', email: 'hoge@hoge.com' } , credentials: { token: 'hogepiyo1234' } }
そのため、`request.env['omniauth.auth']['info']`で`name: 'hoge', email: 'hoge@hoge.com'`の組み合わせを取得できるようになります。あとはこれをDBと照合して、有効であるかどうかを確かめればテスト終了です。
テスト全体は以下のようになります。
discribe 'login via Facebook' do
let(:user) { User.create(name: 'hoge', email: 'hoge@hoge.com') }
before do
OmniAuth.config.mock_auth[:facebook] = nil
Rails.application.env_config['omniauth.auth'] = facebook_mock(
email: user.email,
name: user.name
)
click_link 'Facebookを利用してログインする'
end
it 'should succeed' do
expect(page.status_code).to eq 200
end
end
ちなみに検証部分がステータスコードの判別しかないですが、ログインされているかどうかを簡単に確認できる指標が欲しいですね。
例えば、`expect(current_user).to eq user`のようなものがあれば。。
以上、Mockを使ったRspecの書き方でした。読んで頂きありがとうこざいました。
アドバイスなどありましたら是非お願いします!
We're hiring!
Cluexではビジネスサイド、エンジニアサイド共にメンバーを募集しています!
お気軽にご連絡下さい!
参考文献
http://qiita.com/jnchito/items/640f17e124ab263a54dd
http://jp.corp-sansan.com/blog/hitokoto/2012/120122.html
http://uehaj.hatenablog.com/entry/20090427/1240815860
https://github.com/omniauth/omniauth/wiki/Integration-Testing