Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP Day Verona 2015 - PHP object mocking framework world

PHP Day Verona 2015 - PHP object mocking framework world

Heard about PHPSpec? Well its PHP object mocking framework called Prophecy is quite nice. We'll discover its API, similarities and improvements regarding the one from PHPUnit. Finally, we'll take a look at the integration of Prophecy in PHPUnit.

Sarah KHALIL

May 15, 2015
Tweet

More Decks by Sarah KHALIL

Other Decks in Technology

Transcript

  1. WHO AM I? • Head of • Trainer & Developer

    • Enjoying sharer • Contributor to
  2. TODAY, HOPEFULLY, WE LEARN NEW THINGS 1. Terminology about objects

    doubling 2. PHPUnit implementation 3. Prophecy implementation 4. The differences between the two philosophies
  3. Stubs are objects that implement the same methods as the

    real objects. Returns a specific value.
  4. Mocks are pre-programmed with expectations which forming a specification of

    the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting. Has expectation.
  5. • Generates an object. • All methods return NULL. •

    You can describe the expected behavior of your object.
  6. namespace PoleDev\AppBundle\Security; use Guzzle\Service\Client; use Symfony\[…]\Router; use Symfony\[…]\Response; use Symfony\[…]\Request;

    use Symfony\[…]\SimplePreAuthenticatorInterface; use Symfony\[…]\AuthenticationFailureHandlerInterface; use Symfony\[…]\TokenInterface; use Symfony\[…]\UserProviderInterface; use Symfony\[…]\AuthenticationException; use Symfony\[…]\UrlGeneratorInterface; use Symfony\[…]\HttpException; use Symfony\[…]\PreAuthenticatedToken; class GithubAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { // Some code… }
  7. private $client; private $router; public function __construct( Client $client, Router

    $router, $clientId, $clientSecret ) { $this->client = $client; $this->router = $router; $this->clientId = $clientId; $this->clientSecret = $clientSecret; }
  8. function createToken(Request $request, $providerKey) { $request = $this->client->post(…); $response =

    $request->send(); $data = $response->json(); if (isset($data[‘error']) && !is_null($this->logger) { $message = ‘An error occured…’; $this->logger->notice($message); throw new HttpException(401, $message); } return new PreAuthenticatedToken( ‘anon.’, $data[‘access_token’], $providerKey ); }
  9. PART 1/3: GET ACCESS TOKEN $url = $this->router->generate(‘admin’,[], true); $endpoint

    = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); createToken()
  10. PART 2/3: IF ERROR FROM GITHUB, EXCEPTION if (isset($data['error'])) {

    $message = 'An error occured during authentication with Github.'; $this->logger->notice($message, [ 'HTTP_CODE_STATUS' => 401, ‘error' => $data['error'], 'error_description' => $data['error_description'], ]); throw new HttpException(401, $message); } createToken()
  11. public function createToken(Request $request, $providerKey) { $request = $this->client->post('/login/oauth/access_token', array(),

    array ( 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $this->router->generate('admin', array(), UrlGeneratorInterface::ABSOLUTE_URL) )); $response = $request->send(); $data = $response->json(); if (isset($data['error'])) { $message = sprintf('An error occured during authentication with Github. (%s)', $data['error_description']); $this->logger->notice( $message, array( 'HTTP_CODE_STATUS' => Response::HTTP_UNAUTHORIZED, 'error' => $data['error'], 'error_description' => $data['error_description'], ) ); throw new HttpException(Response::HTTP_UNAUTHORIZED, $message); } return new PreAuthenticatedToken( ‘anon.', $data[‘access_token'], $providerKey ); } We need to test the result of createToken method
  12. LET’S GET OUR DUMMIES AND CALL OUR METHOD TO TEST

    public function testCreateToken() { $githubAuthenticator = new GithubAuthenticator( $client, $router, '', '' ); $token = $githubAuthenticator ->createToken($request, ‘secure_area') ; }
  13. public function testCreateToken() { $githubAuthenticator = new GithubAuthenticator( $client, $router,

    '', '' ); $token = $githubAuthenticator ->createToken($request, ‘secure_area') ; } Where do they come from?
  14. $client = $this->getMock(‘Guzzle\Service \Client’); $router = $this->getMock('Symfony\Bundle \FrameworkBundle\Routing\Router'); $request =

    $this->getMock('Symfony\Component \HttpFoundation\Request'); This a dummy Step 1 fulfill the contract
  15. We must disable the use of the original Router constructor

    as we don’t actually care. Step 2 get the router
  16. $ phpunit -c app/ src/PoleDev/AppBundle/Tests/ Security/GithubAuthenticatorTest.php PHP Fatal error: Call

    to a member function send() on null in /[…]/ AppBundle/Security/ GithubAuthenticator.php on line 43 Step 2 get the router
  17. 4 OBJECTS TO FAKE public function createToken(Request $request, $providerKey) {

    // … $url = $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); } 1 3 2 4 Step 3 fake data from Github
  18. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); } 1 Step 3 fake data from Github
  19. We need to stub the call to the method $router->generate()

    it needs to return an url 1 Step 3 fake data from Github
  20. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $request->send(); $data = $response->json(); } 2 Step 3 fake data from Github
  21. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $request->send(); $data = $response->json(); } 3 Step 3 fake data from Github
  22. We need to stub the call to the method $client->post(),

    it needs to return a $guzzleRequest 3 Step 3 fake data from Github
  23. $endpoint = '/login/oauth/access_token'; $client ->method('post') ->with($endpoint, [], [ 'client_id' =>

    '', 'client_secret' => '', 'code' => '', 'redirect_uri' => 'http://domain.name' ]) ->willReturn($guzzleRequest) ; 3 STUB THE GUZZLECLIENT::POST Step 3 fake data from Github
  24. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); } 4 Step 3 fake data from Github
  25. This method must return a: Guzzle\Http\Message\Response $response Let’s go for

    it! Step 3 fake data from Github STUB THE GUZZLEREQUEST::SEND 4
  26. Hurray! The original code is running with our dummies and

    stubs. But we do not test anything.
  27. public function createToken(Request $request, $providerKey) { $request = $this->client->post('/login/oauth/access_token', array(),

    array ( 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $this->router->generate('admin', array(), UrlGeneratorInterface::ABSOLUTE_URL) )); $response = $request->send(); $data = $response->json(); if (isset($data['error'])) { $message = sprintf('An error occured during authentication with Github. (%s)', $data['error_description']); $this->logger->notice( $message, array( 'HTTP_CODE_STATUS' => Response::HTTP_UNAUTHORIZED, 'error' => $data['error'], 'error_description' => $data['error_description'], ) ); throw new HttpException(Response::HTTP_UNAUTHORIZED, $message); } return new PreAuthenticatedToken( ‘anon.', $data[‘access_token'], $providerKey ); } We need to test the result of createToken method
  28. $token = $githubAuthenticator->createToken($request, ‘secure_area’); $this->assertSame( ‘a_fake_access_token', $token->getCredentials() ); $this->assertSame( ‘secure_area',

    $token->getProviderKey() ); $this->assertSame( ‘anon.', $token->getUser() ); $this->assertEmpty($token->getRoles()); $this->assertFalse($token->isAuthenticated()); $this->assertEmpty($token->getAttributes()); Test that the TOKEN is what we need to be. Step 4 assertions
  29. Duck! Our token has its credentials at null. You need

    to provide it as the real code would have done it. Github returns an access token at that point.
  30. PROPHECY Object used to describe the future of your objects.

    $prophecy = $prophet->prophesize(‘YourClass’);
  31. Note that the prophet won’t use the original constructor by

    default. beConstructedWith($object) beConstructedThrough(‘method’, [$argument]) in PhpSpec
  32. STUB 1/2 • Get a stub out from the Router

    of Symfony. • $router->generate() must return 'http://www.google.com'
  33. A promise is a piece of code allowing a method

    call with a certain argument (if there is one), returns always the same value.
  34. $prophecy->willReturn(‘my value’); Returns the value ‘my value’. $prophecy->willReturnArgument(); Returns the

    first method argument. $prophecy->willThrow(‘ExceptionClass’); Throws an exception. $prophecy->will($callback) $prophecy->will(new Prophecy\Promise \ReturnPromise(array(‘my value’)); === $prophecy->willReturn(‘my value’); PROMISE - API https://github.com/phpspec/prophecy/blob/master/src/Prophecy/Prophecy/MethodProphecy.php Details about the implementation:
  35. NO HARD CODE • Prophecy offers you plenty of methods

    to « wildcard » the arguments. • Any argument is ok for the method you are « stubbing ». Prophecy\Argument::any()
  36. ARGUMENT THE API • Pretty complete • To go further

    with this: • https://github.com/phpspec/prophecy#arguments-wildcarding
  37. We expect to have that method generate() called at least

    one time. How we call it in real life ? Predictions!
  38. Verifies that a method has been called during the execution

    $em = $prophet->prophesize('Doctrine\ORM\EntityManager'); $controller->createUser($em->reveal()); $em->flush()->shouldHaveBeenCalled(); Exemple taken from the official prophecy repository
  39. namespace PoleDev\AppBundle\Security; use Guzzle\Service\Client; use Symfony\[…]\Router; use Symfony\[…]\Response; use Symfony\[…]\Request;

    use Symfony\[…]\SimplePreAuthenticatorInterface; use Symfony\[…]\AuthenticationFailureHandlerInterface; use Symfony\[…]\TokenInterface; use Symfony\[…]\UserProviderInterface; use Symfony\[…]\AuthenticationException; use Symfony\[…]\UrlGeneratorInterface; use Symfony\[…]\HttpException; use Symfony\[…]\PreAuthenticatedToken; class GithubAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { // Some code… }
  40. private $client; private $router; public function __construct( Client $client, Router

    $router, $clientId, $clientSecret ) { $this->client = $client; $this->router = $router; $this->clientId = $clientId; $this->clientSecret = $clientSecret; }
  41. function createToken(Request $request, $providerKey) { $request = $this->client->post(…); $response =

    $request->send(); $data = $response->json(); if (isset($data[‘error']) && !is_null($this->logger) { $message = ‘An error occured…’; $this->logger->notice($message); throw new HttpException(401, $message); } return new PreAuthenticatedToken( ‘anon.’, $data[‘access_token’], $providerKey ); }
  42. PART 1/3: GET ACCESS TOKEN $url = $this->router->generate(‘admin’,[], true); $endpoint

    = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); createToken()
  43. public function createToken(Request $request, $providerKey) { $request = $this->client->post('/login/oauth/access_token', array(),

    array ( 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $this->router->generate('admin', array(), UrlGeneratorInterface::ABSOLUTE_URL) )); $response = $request->send(); $data = $response->json(); if (isset($data['error'])) { $message = sprintf('An error occured during authentication with Github. (%s)', $data['error_description']); $this->logger->notice( $message, array( 'HTTP_CODE_STATUS' => Response::HTTP_UNAUTHORIZED, 'error' => $data['error'], 'error_description' => $data['error_description'], ) ); throw new HttpException(Response::HTTP_UNAUTHORIZED, $message); } return new PreAuthenticatedToken( ‘anon.', $data[‘access_token'], $providerKey ); } We need to test the result of createToken method
  44. FIRST, GET THE PROPHET namespace PoleDev\AppBundle\Tests\Security; class GithubAuthenticatorTest extends \PHPUnit_Framework_TestCase

    { private $prophet; public function testCreateToken() { } public function setUp() { $this->prophet = new \Prophecy\Prophet; } public function tearDown() { $this->prophet = null; } }
  45. public function testCreateToken() { $githubAuthenticator = new GithubAuthenticator( $client, $router,

    '', '' ); $token = $githubAuthenticator ->createToken($request, ‘secure_area') ; } We need to create those dependencies.
  46. LET’S GET OUR DUMMIES AND CALL OUR METHOD TO TEST

    public function testCreateToken() { $clientObjectProphecy = $this->prophet->prophesize( ‘Guzzle\Service\Client’ ); $client = $clientObjectProphecy->reveal(); // … } This a prophecy This a dummy
  47. $routerObjectProphecy = $this->prophet ->prophesize('Symfony\Bundle \FrameworkBundle\Routing\Router'); $router = $routerObjectProphecy->reveal(); $requestObjProphecy =

    $this->prophet ->prophesize('Symfony\Component \HttpFoundation\Request'); $request = $requestObjProphecy->reveal(); Step 1 fulfill the contract
  48. $ phpunit -c app/ src/PoleDev/AppBundle/Tests/ Security/GithubAuthenticatorTest.php PHP Fatal error: Call

    to a member function send() on null in /[…]/ AppBundle/Security/ GithubAuthenticator.php on line 43 Step 2 get the router
  49. 3 OBJECTS TO FAKE public function createToken(Request $request, $providerKey) {

    // … $url = $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); } 2 1 3 Step 3 fake data from Github
  50. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $request->send(); $data = $response->json(); } 1 Step 3 fake data from Github
  51. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $request->send(); $data = $response->json(); } 2 Step 3 fake data from Github
  52. We need to stub the call to the method $client->post(),

    it needs to return a $guzzleRequest 2 Step 3 fake data from Github
  53. $clientObjectProphecy = $this->prophet ->prophesize(‘Guzzle\Service\Client'); $clientObjectProphecy ->post('/login/oauth/access_token', [], [ 'client_id' =>

    ' ', 'client_secret' => ' ', 'code' => ' ', 'redirect_uri' => ' ' ]) ->willReturn($guzzleRequest) ; $client = $clientObjectProphecy->reveal(); STUB THE GUZZLECLIENT::POST 2
  54. public function createToken(Request $request, $providerKey) { // … $url =

    $this->router->generate(‘admin’,[], true); $endpoint = ‘/login/oauth/access_token’; $guzzleRequest = $this->client->post($endpoint,[], [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $request->get('code'), 'redirect_uri' => $url ]); $response = $guzzleRequest->send(); $data = $response->json(); } 3 Step 3 fake data from Github
  55. This method must return a: Guzzle\Http\Message\Response $response Let’s go for

    it! 3 Step 3 fake data from Github STUB THE GUZZLECLIENT::SEND
  56. $token = $githubAuthenticator->createToken($request, ‘secure_area’); $this->assertSame( ‘a_fake_access_token', $token->getCredentials() ); $this->assertSame( ‘secure_area',

    $token->getProviderKey() ); $this->assertSame( ‘anon.', $token->getUser() ); $this->assertEmpty($token->getRoles()); $this->assertFalse($token->isAuthenticated()); $this->assertEmpty($token->getAttributes()); Test that the TOKEN is what we need to be. Step 4 assertions
  57. The Prophecy mock library philosophy is around the description of

    the future of an object double through a prophecy. Prophecy
  58. The first step is to get a mock, then describe

    the future of the double of the object. PHPUnit PHPUnit
  59. $prophet = new \Prophecy\Prophet; $prophecy = $prophet->prophesize(‘Foo\Bar’); $dummy = $prophecy->reveal();

    ——————————————————————————————————————— $dummy = $this->getMock(‘Foo\Bar’); PHPUnit Prophecy