connecteev's avatar

Seeing puzzling and inconsistent results with unit test

Method 1: directly call Laravel Cashier's API to create a new subscription

$user->newSubscription(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'), env('STRIPE_ID_PLAN_1'))->create($paymentMethodId);

$isUserSubscribed = $user->subscribed(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'));

This returns $isUserSubscribed = TRUE.

Method 2: My unit test makes an API call to my API endpoint at /api/v1/me/payments/createOrUpdateSubscription:

        $response = $this->actingAs($user, 'api')->json('PUT', '/api/v1/me/payments/createOrUpdateSubscription', [
            'stripePlanId' => env('STRIPE_ID_PLAN_1'),
            'paymentMethodId' => $paymentMethodId
        ]);

The API endpoint does this (which is exactly the same as what method #1 does directly)

        $loggedinUser = auth()->user();
        $loggedinUser->newSubscription(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'), $stripePlanId)->create($paymentMethodId);

$isUserSubscribed = $user->subscribed(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'));

This returns $isUserSubscribed = FALSE.

Whyy??

0 likes
7 replies
connecteev's avatar

Update: In case you're curious, the env variables are accessible from the unit test. I'm not sure why there's this inconsistency or how to fix this. Any ideas?

Talinon's avatar

@connecteev

Shouldn't your endpoint being using $loggedInUser and not $user?

 $loggedinUser = auth()->user();
        $loggedinUser->newSubscription(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'), $stripePlanId)->create($paymentMethodId);

// below you have $user, not $loggedInUser
$isUserSubscribed = $user->subscribed(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'));

Also, FYI, it is best practice to use config() over env() within your code. That way, you can cache your configuration - otherwise, everytime your application calls env() it's going to read to the disk, which is very slow.

connecteev's avatar

@talinon thank you, I will use config() instead of env() as much as possible going forward.

Shouldn't your endpoint be using $loggedInUser and not $user? No, because in Method 1, the endpoint is being called by my front-end (vuejs). In Method 2, I need to call it from my unit test.

Other API calls from my unit test seem to work fine...for example:

        // update the default Stripe payment method for the user
        $response = $this->actingAs($user, 'api')->json('POST', '/api/v1/me/payments/updateDefaultPaymentMethod', [
            'paymentMethodId' => $paymentMethod['id']
        ]);

I just can't figure out why this one doesn't work.

Talinon's avatar

@connecteev Oh I see, I was just a bit confused by the way you had the code snippet.

Hmm.. have you tried refreshing the model?

$isUserSubscribed = $user->refresh()->subscribed(env('STRIPE_SUBSCRIPTION_PRODUCT_NAME'));
connecteev's avatar

@talinon whoa, that worked. I'm really puzzled as to why?

From my debugging, I put this in Laravel\Cashier\Concerns\ManagesSubscriptions::subscribed()

print_r($this->subscriptions);
  • With Method 1 (direct call), $this->subscriptions was an array with 1 entry, subsequently calling subscribed() on the user object returned TRUE.
  • With Method 2 (API call), $this->subscriptions was an empty array, subsequently calling subscribed() on the user object returned FALSE.
  • With Method 2 (API call, using refresh()), $this->subscriptions was an array with 1 entry, subsequently calling subscribed() on the user object returned TRUE.

I have so many questions: What does the refresh() method do? I did find Illuminate\Database\Eloquent\Model::refresh() whose comment says:

    /**
     * Reload the current model instance with fresh attributes from the database.
     *
     * @return $this
     */
    public function refresh()
    {
...

But I am not sure I have the right function.

Why is refresh() necessary? Is it a symptom that my unit test is not well-written, or is it really necessary?

Thank you!

Talinon's avatar
Talinon
Best Answer
Level 51

@connecteev I think your other methods worked because you were creating a model then asserting against it. With the update method, you already had an instance of a model, then applied an update to a related piece of data, then asserted against the original instance without the changes. Using refresh() will reload the model, along with it's relationships (which would include the changes you made via the PUT request)

It's not necessarily any bad practice on your behalf, it's just something to be aware of. You will require to refresh your model from time to time with unit testing to make a test pass. It's one of the reasons why the refresh() method was included within the framework. Alternatively, if you don't want to refresh the model, you could always assert against the database.

connecteev's avatar

@talinon Very interesting, thanks so much for the explanation! I have been breaking my head on this for hours on end.

Just curious, why would this work, then, without a refresh?

        $response = $this->actingAs($user, 'api')->json('POST', '/api/v1/me/payments/updateDefaultPaymentMethod', [
            'paymentMethodId' => $paymentMethod['id']
        ]);

In the API call, updateDefaultPaymentMethod did this:

    public function updateDefaultPaymentMethod(Request $request)
    {
            $loggedinUser = auth()->user();
            $paymentMethodId = $request->get('paymentMethodId');
            if ($loggedinUser->stripe_id == null) {
                $loggedinUser->createAsStripeCustomer();
            }

            // Accepts a Stripe payment method identifier and will assign the new payment method
            // as the default billing payment method
            $loggedinUser->updateDefaultPaymentMethod($paymentMethodId);

            return response([
                "success" => true,
            ], Response::HTTP_OK);
    }

Then getting the default payment method returned the correct result:

            $stripeCustomer = $user->asStripeCustomer();
            $defaultPaymentMethodId = ($stripeCustomer && $stripeCustomer->invoice_settings) ? $stripeCustomer->invoice_settings->default_payment_method : null;

Maybe a refresh() wasn't needed because I was accessing the $user model directly?

Please or to participate in this conversation.