Socialite導入時のテストコード例

こんにちはー!むちょこです。今日も今日とてPHPが楽しいです🙌

今回は、以前書いたSocialite導入の記事を基にしたテストコード例をご紹介してみようと思います。

まだ読んでいない方は先にざっと目を通していただことをオススメします。

Socialiteの使い方を世界一丁寧に解説した

解説が要らない方は、最後の完成コードまでスキップしてください:)

環境

Laravel Framework 6.18.20

基にした記事とLaravelのバージョンが変わってしまったのですが、どちらでも同様に動くと思います(もし動かなかったらごめんなさい。リクエストフォームからご連絡いただけると嬉しいです)。

新しいテストクラスの作成

Laravelには、テストクラスを作成するコマンドが用意されています。

コマンド
php artisan make:test OAuthTest

これを実行して、tests/Feature/OAuthTest.phpが作成されたことを確認しましょう。

tests/Feature/OAuthTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class OAuthTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testExample()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

自動生成されているtestExample()メソッドは使わないので、消していただいて構いません。

MEMO

Laravelのテストに関する日本語訳ドキュメントはこちらをご覧ください。

https://readouble.com/laravel/6.x/ja/testing.html

TwitterのOAuth画面にリダイレクトするテスト

まずは、未ログインユーザが「Twitterで登録/ログイン」ボタンを押した後にTwitterのOAuthページにきちんとリダイレクトされることを確認してみます。

実装コード

tests/Feature/OAuthTest.php
    /**
     * 未ログインユーザはTwitterログインページにアクセスすると、TwitterのOAuth画面にリダイレクトする
     *
     * @test
     */
    public function guestRedirectToTwitterOAuthWhenAccessTwitterLogin()
    {
         $response = $this->get('/login/twitter')
                            ->assertStatus(302);
         $location = $response->headers->get('location');
         $this->assertRegExp('/https:\/\/api\.twitter\.com\/oauth\/authenticate\?oauth_token=.*/', $location);

    }

解説

内容を分割して解説していきますね!

$response = $this->get('/login/twitter')
                          ->assertStatus(302);

/login/twitterにアクセスしたとき、302ステータスが返ってくることを確認しています。

302ステータスは、リダイレクトしたことを表しているステータスです。

また、この後アクセスした結果を詳しく見るために$response変数に代入しておきます。

$location = $response->headers->get('location');

結果のヘッダーに含まれているリダイレクト先のURLを取得しています。

$this->assertRegExp('/https:\/\/api\.twitter\.com\/oauth\/authenticate\?oauth_token=.*/', $location);

assertRegExp()メソッドは、第二引数の値($location)が第一引数の正規表現(/https:\/\/api.twitter.com\/oauth\/authenticate\?oauth_token=.*/)と一致するかどうかを確認するアサートメソッドです。

これで、未ログインユーザはTwitterログインページにアクセスすると、TwitterのOAuth画面にリダイレクトすることのテストが書けました。

ログイン済みユーザはホームにリダイレクトされるテスト

今度は、ログイン済みユーザがTwitterの認証画面にアクセスしようとすると/homeにリダイレクトされることをテストしてみます。

実装コード

tests/Feature/OAuth.php
    /**
     * ログイン済みユーザがTwitterの認証画面にアクセスしようとするとホームにリダイレクトされる
     *
     * @test
     */
    public function userRedirectToHomeWhenAccessTwitterLogin()
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)
                        ->get('/login/twitter')
                        ->assertStatus(302)
                        ->assertRedirect('/home');
    }

解説

$user = factory(User::class)->create();

factory()ヘルパ関数を使って、架空のユーザを生成しています。

UserのFactoryクラスは予め用意されているので、自分で作る必要はありません。

MEMO

Factoryについてもっと知りたい方は、こちらをご覧ください。

https://readouble.com/laravel/6.x/ja/database-testing.html#writing-factories

$response = $this->actingAs($user)
    ->get('/login/twitter')
    ->assertStatus(302)
    ->assertRedirect('/home')

actingAs()メソッドを使うことで、Factoryで生成したユーザがログインしている状態で/login/twitterにアクセスしていることになります。

そして、その結果302ステータスが返ってくることとリダイレクト先が/homeであるをアサーションしました。

Socialiteのモックを作成する

Socialiteのテストに入る前に、テストの度にTwitterのAPIを叩いて先方に迷惑がかかることのないようモックを用意します。

実装コード

tests/Feature/OAuth.php
use Socialite;
use Mockery;

class OAuthTest extends TestCase
{
    use WithFaker;
    use RefreshDatabase;

    private function createSocialiteMock()
    {
        $user = Mockery::mock('Laravel\Socialite\Two\User');
        $user->shouldReceive('getEmail')
                        ->andReturn($this->faker->unique()->safeEmail)
                        ->shouldReceive('getName')
                        ->andReturn($this->faker->name);

        $provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
        $provider->shouldReceive('user')->andReturn($user);

        Socialite::shouldReceive('with')
            ->andReturn($provider);

        return $user;
    }

解説

use Socialite;
use Mockery;

モックを作るために必要なSocialiteクラスとMockeryクラスをインポートしておきます。

use WithFaker;
use RefreshDatabase;

WithFakerトレイトは、$this->fakerでFakerを使えるようになります。ユーザの情報を適当に自動生成するために使います。

RefreshDatabaseトレイトは、テスト毎にデータベースをリセットしてくれます。データベースを扱うテストのときは、他のテストに影響を与えないようこれでリセットします。

MEMO

Fakerについて知りたい方は、この辺りが参考になるかもしれません。
https://readouble.com/laravel/5.5/ja/database-testing.html#writing-factories
https://github.com/fzaninotto/Faker

RefreshDatabaseについての日本語訳ドキュメントはこちらです。
https://readouble.com/laravel/6.x/ja/database-testing.html#resetting-the-database-after-each-test

$user = Mockery::mock('Laravel\Socialite\Two\User');

Laravel\Socialite\Two\Userオブジェクトをモックでシュミレートします。

$user->shouldReceive('getEmail')
    ->andReturn($this->faker->unique()->safeEmail) 
    ->shouldReceive('getName')
    ->andReturn($this->faker->name);

shouldReceive()の引数として渡されたメソッド名が呼び出されると、andReturn()の値が返されます。

ここでは、getEmail()メソッドの返り値が$this->faker->unique()->safeEmail、getName()メソッドの返り値が$this->faker->nameになるように設定しました。

$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('user')->andReturn($user);

同様にLaravel\Socialite\Contracts\Providerオブジェクトのモックを作成し、user()メソッドが呼ばれたら先ほど作成したLaravel\Socialite\Two\Userオブジェクトのモックが返るようにします。

Socialite::shouldReceive('with')
            ->andReturn($provider);

最後に、Socialiteのwith()メソッドの返り値にLaravel\Socialite\Contracts\Providerオブジェクトのモックを設定します。

ここでは基の記事のコントローラで\Socialite::with($provider)->user();という感じでwith()メソッドを使っているためにこのように設定しましたが、driver()メソッド等他のメソッドを使っている方は適宜合わせて変更してくださいね。

return $user;

ユーザ情報はFakerで自動生成されるため、このメソッドを実行したテストでユーザ情報のアサーションができるように$userを返しておきます。

Twitterアカウントでユーザ登録ができるテスト

先ほど作ったcreateSocialiteMock()メソッドを使って、未登録ユーザがTwitterアカウントで登録できることをテストします。

実装コード

tests/Feature/OAuthTest.php
    /**
     * 未登録ユーザはTwitterアカウントでユーザ登録ができる
     *
     * @test
     */
    public function guestCanRegisterWithTwitterAccount()
    {
        $twitterAccount = $this->createSocialiteMock();

        $this->get('/login/twitter/callback')
               ->assertStatus(302)
               ->assertRedirect('/home');

        $this->assertDatabaseHas('users', [
           'name' => $twitterAccount->getName(),
           'email' => $twitterAccount->getEmail()
        ]);

        $this->assertAuthenticated();

    }

解説

$twitterAccount = $this->createSocialiteMock();

createSocialiteMock()メソッドを実行してモックを作成し、擬似的にOAuth認証が済んだことにします。また、認証されたユーザ情報を受け取っておきます。

$this->get('/login/twitter/callback')
    ->assertStatus(302)
    ->assertRedirect('/home');

OAuth認証が完了してコールバックURLに戻ってきた後、/homeにリダイレクトされることを確認しています。

$this->assertDatabaseHas('users', [
    'name' => $twitterAccount->getName(),
    'email' => $twitterAccount->getEmail()
]);

assertDatabaseHas()メソッドは、第一引数に渡されたテーブルの中に第二引数のデータが存在することをテストするメソッドです。

Fakerで自動生成されたユーザ情報がきちんとusersテーブルに保存されているかどうかを確認しています。

$this->assertAuthenticated();

assertAuthenticated()メソッドでユーザが認証されていることを確認します。

MEMO

assertAuthenticated()メソッドを含む認証系のアサーションメソッド一覧はこちらをご覧ください。

https://readouble.com/laravel/6.x/ja/http-tests.html#authentication-assertions

これで、未登録ユーザがTwitterアカウントで登録され、ログイン状態になるまでのテストができました。

Twitterアカウントでログインできるテスト

最後に、既にユーザ登録が済んでいるアカウントに、同じメールアドレスが使われているTwitterアカウントからログインできることをテストします。

実装コード

tests/Feature/OAuthTest.php
    private function createSocialiteMock($email = '')
    {
        if (!$email) {
            $email = $this->faker->unique()->safeEmail;
        }

        $user = Mockery::mock('Laravel\Socialite\Two\User');
        $user->shouldReceive('getEmail')
                        ->andReturn($email)
                        ->shouldReceive('getName')
                        ->andReturn($this->faker->name);

        // ...(省略)
    }

    /**
     * 登録済みユーザはTwitterアカウントでログインできる
     *
     * @test
     */
    public function registeredGuestCanLoginWithTwitterAccount()
    {
        $user = factory(User::class)->create();

        $twitterAccount = $this->createSocialiteMock($user->email);

        $this->get('/login/twitter/callback')
               ->assertStatus(302)
               ->assertRedirect('/home');

        $this->assertAuthenticatedAs($user);

    }

解説

※説明が重複するところは省略しています。

    /**
     * Socialiteのモックを作成
     *
     * @param string $email
     */
    private function createSocialiteMock($email = '')
    {
        if (!$email) {
            $email = $this->faker->unique()->safeEmail;
        }

        $user = Mockery::mock('Laravel\Socialite\Two\User');
        $user->shouldReceive('getEmail')
                        ->andReturn($email)
                        ->shouldReceive('getName')
                        ->andReturn($this->faker->name);

        // ...(省略)
    }

同じメールアドレスを持つTwitterアカウントを用意するための処理を追加しました。

createSocialiteMock()メソッドの引数にメールアドレスの値を取り、値がなければこれまで通り、あればその値をgetEmail()メソッドの返り値に設定します。

$user = factory(User::class)->create();

$twitterAccount = $this->createSocialiteMock($user->email);

予めユーザを作成し、作成したユーザのメールアドレスをcreateSocialiteMock()メソッドの引数に渡します。

$this->assertAuthenticatedAs($user);

作成したユーザでログイン状態になっていることを確認しました。

完成コード

こうして完成したテストコードの全体像はこちらです!

tests/Feature/OAuthTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Socialite;
use App\User;
use Mockery;

class OAuthTest extends TestCase
{
    use WithFaker;
    use RefreshDatabase;

    /**
     * 未ログインユーザはTwitterログインページにアクセスすると、TwitterのOAuth画面にリダイレクトする
     *
     * @test
     */
    public function guestRedirectToTwitterOAuthWhenAccessTwitterLogin()
    {
         $response = $this->get('/login/twitter')
                          ->assertStatus(302);
         $location = $response->headers->get('location');
         $this->assertRegExp('/https:\/\/api\.twitter\.com\/oauth\/authenticate\?oauth_token=.*/', $location);

    }

    /**
     * ログイン済みユーザがTwitterの認証画面にアクセスしようとするとホームにリダイレクトされる
     *
     * @test
     */
    public function userRedirectToHomeWhenAccessTwitterLogin()
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)
                        ->get('/login/twitter')
                        ->assertStatus(302)
                        ->assertRedirect('/home');
    }

    /**
     * Socialiteのモックを作成
     *
     * @param string $email
     */
    private function createSocialiteMock($email = '')
    {
        if (!$email) {
            $email = $this->faker->unique()->safeEmail;
        }

        $user = Mockery::mock('Laravel\Socialite\Two\User');
        $user->shouldReceive('getEmail')
                        ->andReturn($email)
                        ->shouldReceive('getName')
                        ->andReturn($this->faker->name);

        $provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
        $provider->shouldReceive('user')->andReturn($user);

        Socialite::shouldReceive('with')
            ->andReturn($provider);

        return $user;
    }

    /**
     * 未登録ユーザはTwitterアカウントでユーザ登録ができる
     *
     * @test
     */
    public function guestCanRegisterWithTwitterAccount()
    {
        $twitterAccount = $this->createSocialiteMock();

        $this->get('/login/twitter/callback')
               ->assertStatus(302)
               ->assertRedirect('/home');

        $this->assertDatabaseHas('users', [
           'name' => $twitterAccount->getName(),
           'email' => $twitterAccount->getEmail()
        ]);

        $this->assertAuthenticated();

    }

    /**
     * 登録済みユーザはTwitterアカウントでログインできる
     *
     * @test
     */
    public function registeredGuestCanLoginWithTwitterAccount()
    {
        $user = factory(User::class)->create();

        $twitterAccount = $this->createSocialiteMock($user->email);

        $this->get('/login/twitter/callback')
               ->assertStatus(302)
               ->assertRedirect('/home');

        $this->assertAuthenticatedAs($user);

    }
}

難しく感じたときは

まだテストコードを書いたことがなく、今回の記事が難しく感じた方は先にこちらを試していただくと良いかもしれません:)

Laravel ではじめる PHPUnit 入門