如何测试Laravel社交名媛

时间:2016-02-09 14:13:32

标签: php laravel testing mockery

我有一个使用社交网站的应用程序,我想为Github身份验证创建测试,所以我使用了Socialite Facade来模拟调用Socialite driver方法,但是当我运行我的测试时,它告诉我我试图获得null类型的值。

以下是我写的测试

public function testGithubLogin()
{
    Socialite::shouldReceive('driver')
        ->with('github')
        ->once();
    $this->call('GET', '/github/authorize')->isRedirection();
}

以下是测试的实施

public function authorizeProvider($provider)
{
    return Socialite::driver($provider)->redirect();
}

我理解为什么它可能会返回这样的结果,因为Sociallite::driver($provider)返回Laravel\Socialite\Two\GithubProvider的实例,并且考虑到我无法实例化此值,因此无法指定返回类型。我需要帮助来成功测试控制器。感谢

4 个答案:

答案 0 :(得分:9)

$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$provider->shouldReceive('redirect')->andReturn('Redirected');
$providerName = class_basename($provider);
//Call your model factory here
$socialAccount = factory('LearnCast\User')->create(['provider' => $providerName]);

$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
// Get the api user object here
$abstractUser->shouldReceive('getId') 
             ->andReturn($socialAccount->provider_user_id)
             ->shouldReceive('getEmail')
             ->andReturn(str_random(10).'@noemail.app')
             ->shouldReceive('getNickname')
             ->andReturn('Laztopaz')
             ->shouldReceive('getAvatar')
             ->andReturn('https://en.gravatar.com/userimage');

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

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

// After Oauth redirect back to the route
$this->visit('/auth/facebook/callback')
// See the page that the user login into
->seePageIs('/');

注意:use您班级顶层的社交名录套餐

  

使用Laravel \ Socialite \ Facades \ Socialite;

我有同样的问题,但我能够使用上述技术解决它; @ceejayoz。我希望这会有所帮助。

答案 1 :(得分:9)

嗯,这两个答案都很棒,但是它们有很多不需要的代码,我能够从中推断出我的答案。

这就是我需要做的全部。

首先模拟社交名流用户类型

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

其次,设置方法调用的期望值

$abstractUser
   ->shouldReceive('getId')
   ->andReturn(rand())
   ->shouldReceive('getName')
   ->andReturn(str_random(10))
   ->shouldReceive('getEmail')
   ->andReturn(str_random(10) . '@gmail.com')
   ->shouldReceive('getAvatar')
   ->andReturn('https://en.gravatar.com/userimage');

第三,您需要模拟提供者/用户调用

Socialite::shouldReceive('driver->user')->andReturn($abstractUser);

然后你写下你的断言

$this->visit('/auth/google/callback')
     ->seePageIs('/')

答案 2 :(得分:4)

这可能更难做到,但我相信这会使测试更具可读性。希望你能帮助我简化我即将描述的内容。

我的想法是存根http请求。考虑到facebook,有两个:1)/oauth/access_token(获取访问令牌),2)/me(以获取有关用户的数据)。

为此我暂时将php附加到mitmproxy以创建vcr灯具:

  1. 告诉php使用http代理(将以下行添加到.env文件中):

    HTTP_PROXY=http://localhost:8080
    HTTPS_PROXY=http://localhost:8080
    
  2. 告诉php代理证书在哪里:将openssl.cafile = /etc/php/mitmproxy-ca-cert.pem添加到php.ini。或者curl.cainfo,就此而言。

  3. 重新启动php-fpm
  4. 开始mitmproxy
  5. 使您的浏览器也通过mitmproxy连接。
  6. 使用Facebook登录您正在开发的网站(此处没有TDD)。

    z mitmproxy C< 0.18}中按mitmproxy清除请求(流)列表,然后在需要时重定向到Facebook。或者,使用f命令l mitmproxy< 0.18}和graph.facebook.com来过滤掉额外的请求。

    请注意,对于Twitter,您需要league/oauth1-client 1.7或更新版本。一个从guzzle/guzzle切换到guzzlehttp/guzzle。否则您将无法登录。

  7. 将数据从mimtproxy复制到tests/fixtures/facebook。我使用了yaml格式,现在看来是这样的:

    -
        request:
            method: GET
            url: https://graph.facebook.com/oauth/access_token?client_id=...&client_secret=...&code=...&redirect_uri=...
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: access_token=...&expires=...
    -
        request:
            method: GET
            url: https://graph.facebook.com/v2.5/me?access_token=...&appsecret_proof=...&fields=first_name,last_name,email,gender,verified
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: '{"first_name":"...","last_name":"...","email":"...","gender":"...","verified":true,"id":"..."}'
    

    为此,如果您已E> = 0.18,则可以使用命令mitmproxy。或者,使用命令P。它将请求/响应复制到剪贴板。如果您希望mitmproxy将其保存到文件中,则可以使用DISPLAY= mitmproxy运行它。

    我认为无法使用php-vcr的录音设备,因为我没有测试整个工作流程。

  8. 通过这种方式,我能够编写以下测试(是的,它们可以用点替换所有这些值,可以随意复制)。

    请注意但是,灯具取决于laravel/socialite的版本。我在facebook上遇到了问题。在版本2.0.16 laravel/socialite开始执行post requests以获取访问令牌。还有facebook网址中的api version

    这些灯具适用于2.0.14。处理它的一种方法是在laravel/socialite文件的require-dev部分中具有composer.json依赖性(使用严格的版本规范)以确保socialite具有正确的版本开发环境(希望composer将忽略生产环境中require-dev部分中的那个。)考虑到您在生产环境中composer install --no-dev

    AuthController_HandleFacebookCallbackTest.php

    <?php
    
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    use Illuminate\Support\Facades\Auth;
    use VCR\VCR;
    
    use App\User;
    
    class AuthController_HandleFacebookCallbackTest extends TestCase
    {
        use DatabaseTransactions;
    
        static function setUpBeforeClass()
        {
            VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl'])
                ->enableRequestMatchers([
                    'method',
                    'url',
                ]);
        }
    
        /**
         * @vcr facebook
         */
        function testCreatesUserWithCorrespondingName()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals('John Doe', User::first()->name);
        }
    
        /**
         * @vcr facebook
         */
        function testCreatesUserWithCorrespondingEmail()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals('john.doe@gmail.com', User::first()->email);
        }
    
        /**
         * @vcr facebook
         */
        function testCreatesUserWithCorrespondingFbId()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(123, User::first()->fb_id);
        }
    
        /**
         * @vcr facebook
         */
        function testCreatesUserWithFbData()
        {
            $this->doCallbackRequest();
    
            $this->assertNotEquals('', User::first()->fb_data);
        }
    
        /**
         * @vcr facebook
         */
        function testRedirectsToHomePage()
        {
            $this->doCallbackRequest();
    
            $this->assertRedirectedTo('/');
        }
    
        /**
         * @vcr facebook
         */
        function testAuthenticatesUser()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(User::first()->id, Auth::user()->id);
        }
    
        /**
         * @vcr facebook
         */
        function testDoesntCreateUserIfAlreadyExists()
        {
            $user = factory(User::class)->create([
                'fb_id' => 123,
            ]);
    
            $this->doCallbackRequest();
    
            $this->assertEquals(1, User::count());
        }
    
        function doCallbackRequest()
        {
            return $this->withSession([
                'state' => '...',
            ])->get('/auth/facebook/callback?' . http_build_query([
                'state' => '...',
            ]));
        }
    }
    

    tests/fixtures/facebook

    -
        request:
            method: GET
            url: https://graph.facebook.com/oauth/access_token
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: access_token=...
    -
        request:
            method: GET
            url: https://graph.facebook.com/v2.5/me
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: '{"first_name":"John","last_name":"Doe","email":"john.doe\u0040gmail.com","id":"123"}'
    

    AuthController_HandleTwitterCallbackTest.php

    <?php
    
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    use Illuminate\Support\Facades\Auth;
    use VCR\VCR;
    use League\OAuth1\Client\Credentials\TemporaryCredentials;
    
    use App\User;
    
    class AuthController_HandleTwitterCallbackTest extends TestCase
    {
        use DatabaseTransactions;
    
        static function setUpBeforeClass()
        {
            VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl'])
                ->enableRequestMatchers([
                    'method',
                    'url',
                ]);
        }
    
        /**
         * @vcr twitter
         */
        function testCreatesUserWithCorrespondingName()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals('joe', User::first()->name);
        }
    
        /**
         * @vcr twitter
         */
        function testCreatesUserWithCorrespondingTwId()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(123, User::first()->tw_id);
        }
    
        /**
         * @vcr twitter
         */
        function testCreatesUserWithTwData()
        {
            $this->doCallbackRequest();
    
            $this->assertNotEquals('', User::first()->tw_data);
        }
    
        /**
         * @vcr twitter
         */
        function testRedirectsToHomePage()
        {
            $this->doCallbackRequest();
    
            $this->assertRedirectedTo('/');
        }
    
        /**
         * @vcr twitter
         */
        function testAuthenticatesUser()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(User::first()->id, Auth::user()->id);
        }
    
        /**
         * @vcr twitter
         */
        function testDoesntCreateUserIfAlreadyExists()
        {
            $user = factory(User::class)->create([
                'tw_id' => 123,
            ]);
    
            $this->doCallbackRequest();
    
            $this->assertEquals(1, User::count());
        }
    
        function doCallbackRequest()
        {
            $temporaryCredentials = new TemporaryCredentials();
            $temporaryCredentials->setIdentifier('...');
            $temporaryCredentials->setSecret('...');
            return $this->withSession([
                'oauth.temp' => $temporaryCredentials,
            ])->get('/auth/twitter/callback?' . http_build_query([
                'oauth_token' => '...',
                'oauth_verifier' => '...',
            ]));
        }
    }
    

    tests/fixtures/twitter

    -
        request:
            method: POST
            url: https://api.twitter.com/oauth/access_token
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: oauth_token=...&oauth_token_secret=...
    -
        request:
            method: GET
            url: https://api.twitter.com/1.1/account/verify_credentials.json
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: '{"id_str":"123","name":"joe","screen_name":"joe","location":"","description":"","profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/456\/userpic.png"}'
    

    AuthController_HandleGoogleCallbackTest.php

    <?php
    
    use Illuminate\Foundation\Testing\DatabaseTransactions;
    use Illuminate\Support\Facades\Auth;
    use VCR\VCR;
    
    use App\User;
    
    class AuthController_HandleGoogleCallbackTest extends TestCase
    {
        use DatabaseTransactions;
    
        static function setUpBeforeClass()
        {
            VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl'])
                ->enableRequestMatchers([
                    'method',
                    'url',
                ]);
        }
    
        /**
         * @vcr google
         */
        function testCreatesUserWithCorrespondingName()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals('John Doe', User::first()->name);
        }
    
        /**
         * @vcr google
         */
        function testCreatesUserWithCorrespondingEmail()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals('john.doe@gmail.com', User::first()->email);
        }
    
        /**
         * @vcr google
         */
        function testCreatesUserWithCorrespondingGpId()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(123, User::first()->gp_id);
        }
    
        /**
         * @vcr google
         */
        function testCreatesUserWithGpData()
        {
            $this->doCallbackRequest();
    
            $this->assertNotEquals('', User::first()->gp_data);
        }
    
        /**
         * @vcr google
         */
        function testRedirectsToHomePage()
        {
            $this->doCallbackRequest();
    
            $this->assertRedirectedTo('/');
        }
    
        /**
         * @vcr google
         */
        function testAuthenticatesUser()
        {
            $this->doCallbackRequest();
    
            $this->assertEquals(User::first()->id, Auth::user()->id);
        }
    
        /**
         * @vcr google
         */
        function testDoesntCreateUserIfAlreadyExists()
        {
            $user = factory(User::class)->create([
                'gp_id' => 123,
            ]);
    
            $this->doCallbackRequest();
    
            $this->assertEquals(1, User::count());
        }
    
        function doCallbackRequest()
        {
            return $this->withSession([
                'state' => '...',
            ])->get('/auth/google/callback?' . http_build_query([
                'state' => '...',
            ]));
        }
    }
    

    tests/fixtures/google

    -
        request:
            method: POST
            url: https://accounts.google.com/o/oauth2/token
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: access_token=...
    -
        request:
            method: GET
            url: https://www.googleapis.com/plus/v1/people/me
        response:
            status:
                http_version: '1.1'
                code: 200
                message: OK
            body: '{"emails":[{"value":"john.doe@gmail.com"}],"id":"123","displayName":"John Doe","image":{"url":"https://googleusercontent.com/photo.jpg"}}'
    

    请注意。确保您需要php-vcr/phpunit-testlistener-vcr,并且phpunit.xml中有以下一行:

    <listeners>
        <listener class="PHPUnit_Util_Log_VCR" file="vendor/php-vcr/phpunit-testlistener-vcr/PHPUnit/Util/Log/VCR.php"/>
    </listeners>
    

    运行测试时,$_SERVER['HTTP_HOST']未设置也存在问题。我在这里谈论config/services.php文件,即关于重定向网址。我像这样处理它:

     <?php
    
    $app = include dirname(__FILE__) . '/app.php';
    
    return [
        ...
        'facebook' => [
            ...
            'redirect' => (isset($_SERVER['HTTP_HOST']) ? 'http://' . $_SERVER['HTTP_HOST'] : $app['url']) . '/auth/facebook/callback',
        ],
    ];
    

    不是特别漂亮,但我找不到更好的方法。我打算在那里使用config('app.url'),但它在配置文件中不起作用。

    UPD 您可以通过删除此方法,运行测试以及使用vcr记录更新灯具的请求部分来摆脱setUpBeforeClass部分。实际上,整个事情可能只用vcr完成(没有mitmproxy)。

答案 3 :(得分:1)

我实际上已经创建了返回虚假用户数据的Fake类,因为我有兴趣测试我的逻辑,而不是测试Socialite或供应商是否正常工作。

// This is the fake class that extends the original SocialiteManager
class SocialiteManager extends SocialiteSocialiteManager
{
    protected function createFacebookDriver()
    {
        return $this->buildProvider(
            FacebookProvider::class, // This class is a fake that returns dummy user in facebook's format
            $this->app->make('config')['services.facebook']
        );
    }

    protected function createGoogleDriver()
    {
        return $this->buildProvider(
            GoogleProvider::class, // This is a fake class that ereturns dummy user in google's format
            $this->app->make('config')['services.google']
        );
    }
}

这是其中一个伪造的提供程序的样子:

class FacebookProvider extends SocialiteFacebookProvider
{
    protected function getUserByToken($token)
    {
        return [
            'id' => '123123123',
            'name' => 'John Doe',
            'email' => 'test@test.com',
            'avatar' => 'image.jpg',
        ];
    }
}

当然,在测试类中,我用版本替换了原来的SocialiteManager:

public function setUp(): void
    {
        parent::setUp();

        $this->app->singleton(Factory::class, function ($app) {
            return new SocialiteManager($app);
        });
    }

这对我来说很好。无需嘲笑任何东西。