PHPUnit单元测试:在方法中处理session_start()

问题描述 投票:0回答:1

我在./src/PCMagas/Dropbox.php的文件中有以下类。我需要测试它:

namespace PCMagas;
define("API_OAUTH_TOKEN_URL","https://api.dropboxapi.com/oauth2/token");

use \GuzzleHttp\Client; 
use \GuzzleHttp\RequestOptions;

class Dropbox
{
    /**
     * @param String $appid The Dropbox Application Id.
     * @param String $secret The dropbox Secret
     * @param Client $httpClient The interface used to consume the Dropbox Rest API
     */
    public function __construct($appId,$secret,Client $httpClient)
    {
        $this->appId=$appId;
        $this->secret=$secret;
        $this->httpClient=$httpClient;
    }

    /**
     * Common Logic for Handling Http Error
     * @param Integer $code
     * @throws Exception
     */
    private function httpErrorHandling($code)
    {
        switch($code){
            case 400:
                throw new Exception('Invalid HttpRequest to DropBoxApi');
            case 401:
                throw new Exception('Invalid Dropbox Token');
            case 403:
                throw new Exception('Access Denied');
            case 429:
                throw new Exception('Try again later (after a 10th cup of coffee)');
            case 409:
                throw new Exception('Api user provided error');
            //Treat all 500 error code (seems kinda ugly)
            case 500:
            case 501:
            case 502:
            case 503:
            case 504:
            case 505:
            case 506:
            case 507:
            case 508:
            case 510:
            case 511:
                throw new Exception('Internal Dropbox Error');
        }
    }

    /**
     * @param String $code
     * @return String
     * @throws InvalidArgumentException In case that the code is not correctly Provided.
     * @throws Exception if any error occured when token cannot be fetched
     */
    public function getToken($code)
    {
        //If code get token from code
        //Else get token from $session
        //Not satisfiable thows Esception
        session_start();
        if(!empty($_SESSION['token'])){
            return $_SESSION['token'];
        }

        if(empty($code)){
            throw new \InvalidArgumentException('Please provide a code fetched from Dropbox Athorization.');
        }

        if(empty($_SESSION['redirect_url'])){
            throw new \Exception('Cannot find the url that Dropbox Redirected From');
        }

        $response = $this->httpClient->request("POST",API_OAUTH_TOKEN_URL,[
            RequestOptions::FORM_PARAMS =>[
                'code'=>$code,
                'grant_type'=>'authorization_code',
                'redirect_uri'=>$_SESSION['redirect_url']
            ],
            RequestOptions::AUTH=>[$this->appId,$this->secret]
        ]);

        //Call method and let it blow up
        $this->httpErrorHandling($response->getStatusCode());

        $body=$response->getBody()->getContents();
        $body=json_decode($body,true);
        $_SESSION['token']=$body['access_token'];
        return $_SESSION['token'];
    }
}

然后我尝试单元测试getToken这样的方法(文件位于./tests/DropBoxTest.php

namespace PCMagas\Tests;

use PHPUnit\Framework\TestCase;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Client;

use PCMagas\Dropbox;

define('ERROR_CODES',[400,401,403,409,429,500,501,502,503,504,505,506,507,508,510,511]);
define('ERROR_CODE_LENGTH',count(ERROR_CODES));

final class DropBoxTest extends TestCase
{

    private function mockErrorGuzzle()
    {
        $responses=array_map(function($statusCode){
            return new Response($statusCode);
        },ERROR_CODES);
        $handler = new MockHandler($responses);
        $client = new Client(['handler'=>$handler]);
        return $client;
    }

    public function testHttpErrorOnTonenFetch()
    {
        $guzzle=$this->mockErrorGuzzle();
        $dropBox=new Dropbox("dummyappId","dummySecret",$guzzle);
        for($i=0;$i<ERROR_CODE_LENGTH;$i++) {
            $this->expectException(\Exception::class);
            $dropBox->getToken("dummyCode");
        }
    }
}

我的文件结构是:

|- src
|-- PCMagas
|---- Dropvox.php
|- tests
|-- DropBoxTest.php

而我的phpunit.xml是:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
     backupStaticAttributes="false"
     bootstrap="./vendor/autoload.php"
     cacheTokens="false"
     colors="true"
     convertErrorsToExceptions="true"
     convertNoticesToExceptions="true"
     convertWarningsToExceptions="true"
     forceCoversAnnotation="false"
     mapTestClassNameToCoveredClassName="false"
     processIsolation="false"
     stopOnError="false"
     stopOnFailure="false"
     stopOnIncomplete="false"
     stopOnSkipped="false"
     verbose="false">
    <testsuites>
        <testsuite name="Application Unit Tests">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

虽然我的composer.json有以下输入:

{
    "require": {
        "guzzlehttp/guzzle": "~6.0",
    },
    "require-dev": {
        "phpunit/phpunit": "~6.0",
        "laravel/homestead": "^7.20"
    },
    "autoload": {
        "psr-4":{
            "PCMagas\\": "src/PCMagas"
        }
    },
    "autoload-dev": {
        "psr-4": { "PCMagas\\Tests\\": "tests/" }
    }
}

但是当我尝试运行单元测试时,我收到以下错误:

时间:621毫秒,内存:4.00MB

有1个错误:

1)PCMagas \ Tests \ DropBoxTest :: testHttpErrorOnTonenFetch session_start():标头已经发送时无法启动会话

/home/vagrant/code/双人床/PCM A咖S/Dropbox.PHP:84 /home/vagrant/code/tests/Dropbox test.PHP:36

你知道如何解决这个错误吗?

php unit-testing session phpunit
1个回答
0
投票

一个好主意是使用Adapter模式并创建一个会话适配器(在我的情况下,我PSR-4在./src/PCMagas/Session.php自动加载它):

namespace PCMagas;

/**
 * A simple Session Adapter in order to offer easyness on Unit testing.
 */
class Session
{

    private $started=false;

    /**
     * Start the session
     * @return Session
     */
    public function start()
    {
        if(!$this->started){
            session_start();
            $this->started=true;
        }
        return $this;
    }

    /**
     * Sets or replaces a session value.
     *  
     * @param String|Integer $key The Session Item
     * @param Mixed $value The value of this Session Item
     * @return Session
     */
    public function setItem($key,$value)
    {
        $_SESSION[$key]=$value;
        return $this;
    }


    /**
     * Returns an Item of a session.
     * @param String|Integer $key
     * @throws Exception
     * @return Mixed
     */
    public function getItem($key)
    {    
        if(!isset($_SESSION[$key])){
            throw Exception("Session item $key does not exist");
        }

        return $_SESSION[$key];
    }

    /**
     * Check if a Session has a Key
     * @param String|Integer $key
     * @return Boolean
     */
    public function has($key)
    {
        return isset($_SESSION[$key]);
    }


    /**
     * @return Session
     */
    public function end()
    {
        session_destroy();
        $this->started=false;
        return $this;
    }
}

然后通过依赖注入会话实例将qazxsw poi类重构为此:

DropBox

然后你就可以对它进行单元测试:

namespace PCMagas;

define("API_OAUTH_TOKEN_URL","https://api.dropboxapi.com/oauth2/token");

use \GuzzleHttp\Client; 
use \GuzzleHttp\RequestOptions;
use PCMagas\Session;

class Dropbox
{

    /**
     * @var Session
     */
    private $session=null;

    /**
     * @var Client
     */
    private $httpClient=null;

    /**
     * @var String
     */
    private $appId=null;

    /**
     * @var String
     */
    private $secret=null;

    /**
     * @param String $appid The Dropbox Application Id.
     * @param String $secret The dropbox Secret.
     * @param Client $httpClient The interface used to consume the Dropbox Rest API.
     * @param Session $session The session Adapter in order to have ease in Testing.
     */
    public function __construct($appId,$secret,Client $httpClient,Session $session)
    {
        $this->appId=$appId;
        $this->secret=$secret;
        $this->session=$session;
        $this->httpClient=$httpClient;
    }

    /**
     * Common Logic for Handling Http Error
     * @param Integer $code
     * @throws Exception
     */
    private function httpErrorHandling($code)
    {
        switch($code){
            case 400:
                throw new Exception('Invalid HttpRequest to DropBoxApi');
            case 401:
                throw new Exception('Invalid Dropbox Token');
            case 403:
                throw new Exception('Access Denied');
            case 429:
                throw new Exception('Try again later (after a 10th cup of coffee)');
            case 409:
                throw new Exception('Api user provided error');
            //Treat all 500 error code (seems kinda ugly)
            case 500:
            case 501:
            case 502:
            case 503:
            case 504:
            case 505:
            case 506:
            case 507:
            case 508:
            case 510:
            case 511:
                throw new Exception('Internal Dropbox Error');
        }
    }

    /**
     * @param String $code
     * @return String
     * @throws InvalidArgumentException In case that the code is not correctly Provided.
     * @throws Exception if any error occured when token cannot be fetched
     */
    public function getToken($code)
    {
        //If code get token from code
        //Else get token from $session
        //Not satisfiable thows Esception
        $this->session->start();
        if($this->session->has('token')){
            $token=$this->session->getItem('token');
            $this->session->end();
            return $token;
        }

        if(empty($code)){
            throw new \InvalidArgumentException('Please provide a code fetched from Dropbox Athorization.');
        }

        if(!$this->session->has('redirect_url')){
            throw new \Exception('Cannot find the url that Dropbox Redirected From');
        }

        $response = $this->httpClient->request("POST",API_OAUTH_TOKEN_URL,[
            RequestOptions::FORM_PARAMS =>[
                'code'=>$code,
                'grant_type'=>'authorization_code',
                'redirect_uri'=>$this->session->getItem('redirect_url')
            ],
            RequestOptions::AUTH=>[$this->appId,$this->secret]
        ]);

        //Call method and let it blow up
        $this->httpErrorHandling($response->getStatusCode());

        $body=$response->getBody()->getContents();
        $body=json_decode($body,true);
        $this->session->setItem('token', $body['access_token'])->end();
        return $body['access_token'];
    }
}

因此,助记符规则是:无论你不能/难以模拟,make和Adapter首先将它放入自定义类和API,然后模拟适配器;)。

唯一的缺点是适配器可能未经测试,因此您可以对其进行集成测试,也可以通过未加载到生产代码中的示例或两者的组合手动测试它。

© www.soinside.com 2019 - 2024. All rights reserved.