Testing Part 2: Faking the Event Lookup

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Let's run the test. Copy its method name, then open your terminal. It looks like PHPUnit installed just fine. So, run:

./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted

Oh no! It blew up! Hmmm:

Unknown database 'stripe_recording_test'

Ah, my bad!

I setup our project to use a different database for testing... and I forgot to create it! Do that with:

./bin/console doctrine:database:create --env=test

And to create the tables, run:

./bin/console doctrine:schema:create --env=test

Try the test again:

./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted

Another error! Scroll to the top! The webhook returned a 500 error. And if you look closely at the dumped response HTML, you can see the reason:

No such event: evt_00000000000000

Ah, the id of the fake event that we're sending is evt_00000000000000. That's not a real event in Stripe, and so when the WebhookController reads this and uses Stripe's API to fetch this event, it's not there:

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 22
$stripeEvent = $this->get('stripe_client')
->findEvent($eventId);
... lines 25 - 39
}
... lines 41 - 60
}

It's kind of funny: we added this API lookup to prevent a third-party from sending fake events... and now it's stopping us from doing exactly that. Dang!

Faking things in the Test Environment

Hmm, how to fix this? In the real world, we do want to use stripe's API to fetch the Event object. But in the test environment, this would all work if our code would simply use the JSON we're sending it as the event, and skip the lookup.

Let's do it! We'll set a special configuration variable in the test environment only, then use that to change our logic in the controller.

Open app/config/config.yml and add a new parameter: verify_stripe_event set to true:

... lines 1 - 7
parameters:
... line 9
verify_stripe_event: true
... lines 11 - 80

Copy that, and open config_test.yml. Add a parameters key, paste this parameter, but override it to be false:

... lines 1 - 3
parameters:
verify_stripe_event: false
... lines 6 - 25

Now, in WebhookController, we just need an if statement: if $this->getParameter('verify_stripe_event') is true, then keep the normal behavior. Otherwise, set $stripeEvent to json_decode($request->getContent()):

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 22
if ($this->getParameter('verify_stripe_event')) {
$stripeEvent = $this->get('stripe_client')
->findEvent($eventId);
} else {
// fake the Stripe_Event in the test environment
$stripeEvent = json_decode($request->getContent());
}
... lines 30 - 44
}
... lines 46 - 65
}

OK, this is not technically perfect: the first $stripeEvent is a \Stripe\Event object, and the second will be an instance of stdClass. But, since you fetch data off both the same way, it should work.

Let's see if does! Try the test again:

./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted

This time, no errors! And the dumped response content looks perfect: event handled.

Refreshing the Data after the Test

But, the test didn't pass:

Failed asserting that true is false on line 43

It looks like our webhook is not working, because the subscription is still active. But actually, that's not true: Doctrine is tricking us! In reality, the database has been updated to show that the Subscription is canceled, but this Subscription object is out-of-date. Query for a fresh one with $subscription = $this->em - I set the EntityManager on that property in setup() - then ->getRepository('AppBundle:Subscription') with find($subscription->getId()):

... lines 1 - 9
class WebhookControllerTest extends WebTestCase
{
... lines 12 - 22
public function testStripeCustomerSubscriptionDeleted()
{
... lines 25 - 31
$client->request(
'POST',
'/webhooks/stripe',
[],
[],
[],
$eventJson
);
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$subscription = $this->em
->getRepository('AppBundle:Subscription')
->find($subscription->getId());
$this->assertFalse($subscription->isActive());
}
... lines 48 - 126
}

This subscription will have fresh data.

Try the test!

./vendor/bin/phpunit --filter testStripeCustomerSubscriptionDeleted

And we are green!

I know, that was kind of hard! But if you want to have automated webhook tests, this is the way to do it. To make matters worse, for other webhooks, you may need to fake additional API calls that you're making to Stripe.

But, there are also a couple of other, manual, but easy ways to test. Let's check 'em out!

Leave a comment!

  • 2020-01-23 Kiuega A

    Perfect man! I was just writing a comment on using 0 and 1 instead of true and false!
    And yes it worked!

    I'll take care of putting everything back in order and I'll tell you about some things to worry about (but safe it would seem)!

    EDIT : Perfect man ! We are done ! Thanks ! So, the final output : https://zupimages.net/up/20...

    There are some errors, but which do not seem to bother. One of which concerns Doctrine and the method -> flush (). But in the test, at no time did I give an argument, so it's weird. (And by the way, very annoying that this way of doing things is deprecated)

    Next, I have the answer, so perfect it confirms that the test is a success compared to our expectations.

    Now, the final files, but which certainly contain a few lines that I could have done better:

    WebhookController.php : https://pastebin.com/VgSMHg8z
    WebhookControllerTest.php : https://pastebin.com/8icisnj8

    .env : VERIFY_STRIPE_EVENT=1
    .env.test : VERIFY_STRIPE_EVENT=0

    services.yaml:

    services:
    # default configuration for services in *this* file
    _defaults:
    autowire: true # Automatically injects dependencies in your services.
    autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
    bind:
    $shouldVerifyStripeEvent: '%env(VERIFY_STRIPE_EVENT)%'

    I think we are not bad and I hope that it will help the next who pass by! If you have something to hesitate to complete do not hesitate it is with pleasure and thank you very much for your help (2 days I'm on it anyway ahah)

  • 2020-01-23 weaverryan

    Hey Kiuega A!

    > Next, for the bind, great! However, I did not understand how I can retrieve the variable from my controller?

    You can add it as an argument to any controller method in your system:


    /**
    * . @Route("/any-page")
    */
    public function anyPage($shouldVerifyStripeEvent)
    {

    }

    That's it! The variable must be called $shouldVerifyStripeEvent because that's what you set up in your bind.

    > In this case, if I do a simple var_dump () to see what it should actually return, I get this: string(5) "false"

    Ah, actually, I think there is a problem with how you're setting the env var - and I missed it before. In .env and .env.test use the values "0" and "1" instead of true and false. Environment variables are always strings - that's just the nature of how they work. So using true/false will be the strings true/false, which both should actually look "true".

    > PS: Could you tell me how to format my code on this site when I paste code to make it clearer? Thank you !

    Yea, it's super ugly :p. For inline code like this, surround the code with <code> and </code>. For multiline code, you need to *almost* do the same thing, but you need to use both <pre><code> in the beginning and </code></pre> at the end... which is ugly (but then the code looks great!).

    Cheers!

  • 2020-01-23 Kiuega A

    Okay ! So, the good line, I think; was :$logger->info(var_export(getenv("VERIFY_STRIPE_EVENT"), true));
    It's okay, I've now the good thing in my log !

    Next, for the bind, great! However, I did not understand how I can retrieve the variable from my controller?

    With this code

    : 
    $logger->info(var_export(getenv("VERIFY_STRIPE_EVENT"), true));

    $verifyStripeEvent = var_export(getenv("VERIFY_STRIPE_EVENT"), true);

    if($verifyStripeEvent)
    {
    $logger->info("env : dev");
    $stripeEvent = $stripeClient->findEvent($eventId);
    }
    else
    {
    // test environment
    $logger->info("env : test");
    $stripeEvent = json_decode($request->getContent());
    }

    In this case, if I do a simple var_dump () to see what it should actually return, I get this: string(5) "false"

    PS: Could you tell me how to format my code on this site when I paste code to make it clearer? Thank you !

    EDIT : I tried something like if(boolval($verifyStripeEvent)) but same. However the variable seems to be false.

    On the other hand, I forced the passage in the else() to see what it will produce when we will have fixed the problem.

    When it goes into the else, it seems that everything is going as planned compared to the video! We are not very far!

  • 2020-01-23 weaverryan

    Hey Kiuega A !

    You did perfectly with setting up the environment variable. Nice job!

    So, I would expect it to work. And... it *might* be working - the problem with logging is that the "true" and "false" values will both print as empty strings. You could try this:


    $logger->info(var_export(getenv("VERIFY_STRIPE_EVENT")), true);

    That should print "true" or "false". There is one other detail. I don't think this is causing the issue, but it is something I want to mention. You typically don't reference environment variables in your code with getenv. It's not a huge deal, but you're supposed to inject these as normal variables. The easiest way to do this is with a "bind":


    # config/services.yaml
    services:
    _defaults:
    # ... the other stuff

    bind:
    $shouldVerifyStripeEvent = '%env(VERIFY_STRIPE_EVENT)%'

    With this, you can now have a $shouldVerifyStripeEvent argument to any controller or as an argument to any service constructor and Symfony will pass you the environment variable.

    Let us know if this helps!

    Cheers!

  • 2020-01-23 Kiuega A

    Thank you for your reply ! I have made good progress on debugging. I still have a few concerns, well, especially a big one.

    Since I'm on Symfony 4, the way to handle environment variables is not the same.

    To be able to use the variable "VERIFY_STRIPE_EVENT", I therefore did simply like this:

    In my .env, I set it to true.
    In my .env.test, I set it to false.

    That's all i did. And in my controller, if I do a $logger->info(getenv("VERIFY_STRIPE_EVENT")); I've always an empty result.

    PS: Sorry for my bad english, I'm French

  • 2020-01-23 weaverryan

    Hey Kiuega A!

    > Is my way to get the manager correct in my setUp () function?

    The way you're doing it should be correct. However, you shouldn't need to have the public static $container on your class. This property is already implemented on a parent class as a protected static property - so the code should work the same without it. And setting the $em on a property *might* be ok... but it's a bit safer to get this property (from self::$container->get('doctrine')->getManager()) whenever you needed it. In some cases, after making a request with the Client, you'll need to ask for the entity manager again. Otherwise, you'll (kind of) still be using the "old" one from the "previous request" and you can get some odd results.

    > Besides, I noticed that when I dump after the self :: bootKernel (), it doesn't display. But if I do it before, it works. Similarly in the other functions of the class, my dumps do not pass

    I'm familiar with this too :). When Symfony boots, it does some magic with dump(). This is usually cool magic, but in the case of tests, I find that it often means that the dump is hidden, which I don't actually want. I often use var_dump() in tests to make sure that I see my dumped output. That should help you debug.

    Let us know how it's going!

    Cheers!

  • 2020-01-23 Kiuega A

    Okay, I have analyzed the problems I encounter.

    https://pastebin.com/j0k8KPTC

    [2020-01-23 15:16:16] request.CRITICAL: Uncaught PHP Exception Exception: "There seems to be no subscription ID sub_00000000000000"

    On the one hand, it seems that I can't get the manager, because he couldn't flush the subscription if I can't get it back.

    Is my way to get the manager correct in my setUp () function? Besides, I noticed that when I dump after the self :: bootKernel (), it doesn't display. But if I do it before, it works. Similarly in the other functions of the class, my dumps do not pass

  • 2020-01-23 Diego Aguiar

    Hey Kiuega A

    Your problem is at line 20. You are trying to re-define the $container property or your test class. Instead of using a property use a local variable and it should work

    Cheers!

  • 2020-01-22 Kiuega A

    Hello, I try to make the test on Symfony 4.

    My WebhookControllerTest seems https://pastebin.com/kGTXLXPG

    But when I run test with php bin/phpunit --filter testStripeCustomerSubscriptionDeleted

    I've this error :

    Fatal error: Cannot redeclare static Symfony\Bundle\FrameworkBundle\Test\WebTestCase::$container as non static App\Tests\Controller\WebhookControllerTest::$container in C:\Users\user\Desktop\StripeFormation2\tests\Controller\WebhookControllerTest.php on
    line 11

  • 2019-06-11 Diego Aguiar

    Ok, so it's hitting Stripe's API the thing is such event does not exist on your Stripe account. Check your test code, probably the subscription id doesn't exist

  • 2019-06-11 Diaconescu Petrisor

    First 2 lines were already there. i added the last 1.
    The complete result of tail -f var/log/test.log is:
    [2019-06-11 17:41:20] doctrine.DEBUG: "START TRANSACTION" [] []
    [2019-06-11 17:41:20] doctrine.DEBUG: INSERT INTO user (stripe_customer_id, email, roles, password, card_brand, card_last4) VALUES (?, ?, ?, ?, ?, ?) {"1":null,"2":"fluffy1878526223@sheep.com","3":[],"4":"$2y$13$7nBZfmwsPQNjSImNJQS [...]","5":null,"6":null} []
    [2019-06-11 17:41:20] doctrine.DEBUG: INSERT INTO subscription (stripe_subscription_id, stripe_plan_id, ends_at, billing_period_ends_at, user_id) VALUES (?, ?, ?, ?, ?) {"1":"sub_STRIPE_TEST_XYZ1183136485","2":"plan_STRIPE_TEST_ABC499083006","3":null,"4":"2019-07-11 17:41:20","5":22} []
    [2019-06-11 17:41:20] doctrine.DEBUG: "COMMIT" [] []
    [2019-06-11 17:41:21] request.INFO: Matched route "webhook_stripe". {"route":"webhook_stripe","route_parameters":{"_route":"webhook_stripe","_controller":"App\\Controller\\WebhookController::stripeWebhookAction"},"request_uri":"http://localhost/webhooks/stripe","method":"POST"} []
    [2019-06-11 17:41:21] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
    [2019-06-11 17:41:21] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginAuthenticator"} []
    [2019-06-11 17:41:21] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginAuthenticator"} []
    [2019-06-11 17:41:21] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
    [2019-06-11 17:41:22] request.CRITICAL: Uncaught PHP Exception Stripe\Error\InvalidRequest: "No such event: evt_00000000000000" at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/stripe/stripe-php/lib/ApiRequestor.php line 210 {"exception":"[object] (Stripe\\Error\\InvalidRequest(code: 0): No such event: evt_00000000000000 at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/stripe/stripe-php/lib/ApiRequestor.php:210)"} []

  • 2019-06-11 Diego Aguiar

    Hmm, that's weird, is there a stack trace? I would like to see it
    Anyway, try adding this env vars to your .env.test file


    KERNEL_CLASS='App\Kernel' # Your Kernel namespace
    APP_SECRET='s$cretf0rt3st'
    SHELL_VERBOSITY=-1
  • 2019-06-08 Diaconescu Petrisor

    You was right about duplicating env. vars in env.test file. Errors about STRIPE_SECRET_KEY disappeared, but I can't get rid of 'No such event: evt_00000000000000' critical error.
    I have in .env.test:
    VERIFY_STRIPE_EVENT=false
    STRIPE_PUBLIC_KEY=********************************
    STRIPE_SECRET_KEY=********************************
    I have in .env.local:
    STRIPE_PUBLIC_KEY=***************************************
    STRIPE_SECRET_KEY=***************************************
    VERIFY_STRIPE_EVENT=true
    I have in services.yaml:
    parameters:
    locale: 'en'
    stripe_public_key: '%env(STRIPE_PUBLIC_KEY)%'
    stripe_secret_key: '%env(STRIPE_SECRET_KEY)%'
    verify_stripe_event: '%env(VERIFY_STRIPE_EVENT)%'
    In WebhookController I have:
    if($this->getParameter('verify_stripe_event')){
    $stripeEvent = $this->stripeClient->findEvent($eventId);
    } else {
    //fake the stripe event in the test environment
    $stripeEvent = json_decode($request->getContent());
    }

  • 2019-06-06 Diego Aguiar

    Hey Diaconescu Petrisor

    This happens only when running your tests, right? That's because env vars are isolated from environmets. You will have to create a new .env.test file and duplicate your env vars there

  • 2019-06-06 Diaconescu Petrisor

    tail -f var/log/test.log produce something like this:
    request.CRITICAL: Uncaught PHP Exception Symfony\Component\DependencyInjection\Exception\EnvNotFoundException: "Environment variable not found: "STRIPE_SECRET_KEY"." at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/symfony/dependency-injection/EnvVarProcessor.php line 96 {"exception":"[object] (Symfony\\Component\\DependencyInjection\\Exception\\EnvNotFoundException(code: 0): Environment variable not found: \"STRIPE_SECRET_KEY\". at /home/petrero/www/Stripe_Subscriptions_Discounts_Webhooks/vendor/symfony/dependency-injection/EnvVarProcessor.php:96)"} []

    I have in .env.local something like so:
    STRIPE_PUBLIC_KEY=.*****************
    STRIPE_SECRET_KEY=*****************

    In services.yaml:
    # This file is the entry point to configure your own services.
    # Files in the packages/ subdirectory configure your dependencies.

    # Put parameters here that don't need to change on each machine where the app is deployed
    # https://symfony.com/doc/cur...
    parameters:
    locale: 'en'
    stripe_public_key: '%env(STRIPE_PUBLIC_KEY)%'
    stripe_secret_key: '%env(STRIPE_SECRET_KEY)%'
    verify_stripe_event: '%env(VERIFY_STRIPE_EVENT)%'
    In StripeClient.php I have:
    class StripeClient {
    /**
    * @var EntityManagerInterface
    */
    private $em;

    public function __construct($secretKey, EntityManagerInterface $em){
    $this->em = $em;
    \Stripe\Stripe::setApiKey($secretKey);
    }

    public function createCustomer(User $user, $paymentToken){
    $customer = \Stripe\Customer::create([
    "email" => $user->getEmail(),
    "source" => $paymentToken // obtained with Stripe.js
    ]);

    Acordingly to tail -f var/log/test.log you seem to be right. But why this settings doesn't present any problem in web interface? Because in web interface I could charge the credit card, I could update the credit card, I could cancel a subscription, I could reactivate a subscription, I could apply for a subscription. Why all of this work? They are dependent on STRIPE_SECRET_KEY too

  • 2019-06-06 Victor Bocharsky

    Hey Diaconescu,

    Look at logs! You can tail them with:
    $ tail -f var/log/test.log

    Most probably you just set up Stripe credentials wrong or forget to set up them at all :)

    Cheers!

  • 2019-06-05 Diaconescu Petrisor

    I try to rework this in Flex and Symfony 4.
    At the end of this leg when I run ./vendor/bin/simple-phpunit --filter testStripeCustomerSubscriptionDeleted I should have green colors. But test doesn't pass. I still have 500 status code instead of 200 status code failure.
    I have the repo at address https://github.com/petre-sy.... As far as I can see ./vendor/bin/simple-phpunit --filter testStripeCustomerSubscriptionDeleted doesn't hit https://localhost:8000. I think that's the problem. Probably I must set some parameter base-url or something like that somewhere but I can't really grasp where this setting must be put.

    Cheers