0

I'm trying to make a pest test file easier to read.

Currently, I've got some standard tests:

test('can get subscribers latest subscription', function () {
     $this->seed(PlansTestSeeder::class);
    $this->seed(SubscriptionsTestSeeder::class);

    $this->assertDatabaseCount('plans', 2);
    $this->assertDatabaseCount('subscriptions', 0);


    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "bronze")->first()->id
    ]);
    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "silver")->first()->id
    ]);
    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "silver")->first()->id,
        "status"  => "expired"
    ]);
    Subscription::factory()->trashed()->create();

    $this->assertDatabaseCount('subscriptions', 4);

});

test('can get subscribers active subscriptions', function () {
    $this->seed(PlansTestSeeder::class);
    $this->seed(SubscriptionsTestSeeder::class);

    $silverPlan = Plan::where("slug", "silver")->first();

    $subscription1 = Subscription::factory()->create([
        "plan_id"         => Plan::where("slug", "silver")->first()->id,
        "subscriber_id"   => 1,
        "subscriber_type" => "ApresourcingFramework\Billing\Tests\Models\Subscriber",
        "created_at"      => now()->subDays(2),
        "started_at"      => now()->subDays(2)
    ]);
    $subscription2 = Subscription::factory()->create([
        "plan_id"         => $silverPlan->id,
        "subscriber_id"   => 1,
        "subscriber_type" => "ApresourcingFramework\Billing\Tests\Models\Subscriber",
        "created_at"      => now()->subDays(1),
        "started_at"      => now()->subDays(1)
    ]);

    $user         = Subscriber::find(1);
    $subscription = $user->latestSubscription();
    expect($subscription->id)->toBe($subscription2->id);
});

But to then remind myself what tests I've written, I've got to scroll up and down the page over and over again.

What I'd like to do is change to something like the following:

test('can get subscribers latest subscription', getLatestSubscription());
test('can get subscribers active subscriptions', getActiveSubscriptions());

function getLatestSubscription() {
    /// function code here
});

function getActiveSubscriptions() {
    // function code here
});

However, the test functions include references to $this, which is available within the normal closure, but it's not available in the standard function as I've set it up here.

Edit: I'm using the laravel pest plugin - I'm not sure if that makes a difference to the use of $this

Is there any way to get around this?

Blakey UK
  • 53
  • 8
  • Are all instances of `$this` related to `assertXYZ()` methods? Which class name is returned when you echo `get_class($this)` in one of those tests? – Duroth Apr 14 '23 at 10:12
  • Is this the exact code you are using? Note that in the first example, you pass anonymous functions as second parameter, but in the second you pass the functions call, eg the result of the functions – Kaddath Apr 14 '23 at 10:15
  • I've added extra code showing the full test closures. – Blakey UK Apr 14 '23 at 10:22
  • Have your tried with this as a closure? `function(){ call_user_func([$this, 'getLatestSubscription']); }`? – Kaddath Apr 14 '23 at 10:33

3 Answers3

0

Got there thanks to some hints in the repliers. Not as tidy as I would have liked, but at least it means all the test('description of test') calls are in one place at the bottom of the php file.


$createSubscription = function () {

    $this->seed(PlansTestSeeder::class);
    $this->seed(SubscriptionsTestSeeder::class);

    $this->assertDatabaseCount('plans', 2);
    $this->assertDatabaseCount('subscriptions', 0);


    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "bronze")->first()->id
    ]);
    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "silver")->first()->id
    ]);
    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "silver")->first()->id,
        "status"  => "expired"
    ]);
    Subscription::factory()->trashed()->create();

    $this->assertDatabaseCount('subscriptions', 4);

};

$createBronzeSubscription = function () {
    $this->seed(PlansTestSeeder::class);
    $this->seed(SubscriptionsTestSeeder::class);

    Subscription::factory()->create([
        "plan_id" => Plan::where("slug", "bronze")->first()->id
    ]);

    $this->assertDatabaseCount('subscriptions', 1);
};


test('can create subscription', function () use ($createSubscription) {
    return \Closure::bind(\Closure::fromCallable($createSubscription), $this, get_class($this))($this);
});

test('can create bronze subscription', function () use ($createBronzeSubscription) {
    return \Closure::bind(\Closure::fromCallable($createBronzeSubscription), $this, get_class($this))($this);
});
Blakey UK
  • 53
  • 8
  • 1
    The `Closure::bind` call shouldn't be required. Try doing just `test('can create subscription', $createSubscription)` instead. – Duroth Apr 14 '23 at 11:01
  • You might be able to get away with `test('can create bronze subscription', fn() => \Closure::fromCallable($createBronzeSubscription)->call($this))` or similar; using the arrow function automatically `use`s all variables in scope and explicitly using `call(...)` binds to the first argument, so you don't need to bind separately. – msbit Apr 14 '23 at 11:47
  • Actually, I think it can be further reduced to `test('can create bronze subscription', Closure::fromCallable($createBronzeSubscription))`, for example ✌ – msbit Apr 14 '23 at 11:57
  • Oh blimey, I thought I'd tried that, @msbit - that works. Thank you so much! – Blakey UK Apr 14 '23 at 21:07
  • If you liked that, you might find what I've reduced it to in my answer interesting too ✌ – msbit Apr 15 '23 at 02:29
0

Practice

Consider this, which modifies your spun-out functions to each return an anonymous function, and satisfies the call site you'd like to see:

function getLatestSubscription()
{
    return function () {
        // function code here
    };  
};

function getActiveSubscriptions()
{
    return function () {
        // function code here
    };  
};

test('can get subscribers latest subscription', getLatestSubscription());
test('can get subscribers active subscriptions', getActiveSubscriptions());

If you're comfortable with having the functions as closure variables at the same scope, you can remove one level of function invocation with the following:

$getLatestSubscription = function () {
    // function code here
};

$getActiveSubscriptions = function () {
    // function code here
};

test('can get subscribers latest subscription', $getLatestSubscription);
test('can get subscribers active subscriptions', $getActiveSubscriptions);

Theory

As the Pest test function accepts a Closure for its second argument, and binds that closure to the test case internally, any of the following would behave the same:

test('closure literal', function () {
    $this->helperMethod();
    expect(true)->toBeTrue();
});
$closureVariable = function () {
    $this->helperMethod();
    expect(true)->toBeTrue();
};
test('closure variable', $closureVariable);
function closureFactory()
{
    return function () {
        $this->helperMethod();
        expect(true)->toBeTrue();
    };  
}
test('closure factory', closureFactory());

For your use case the last is likely overkill, but could be useful in situations where you have test cases that differ in just a few ways.

msbit
  • 4,152
  • 2
  • 9
  • 22
-1

Method 1: just call your function from inside a closure:

function getActiveSubscriptions($obj) {
  // use $obj instead of $this
  $obj->doSomething();
}


test('can get subscribers latest subscription', 
   function () { 
        getLatestSubscription($this); 
   }
);

Method2: rewrite your test function for it to accept function name instead of closure. E.g.:

  function test($text, $fun) {
    print $text;
    print $fun();
  }

  function getLatestSubscription() {
    return "123";
  }

  test('can get subscribers latest subscription', 'getLatestSubscription');
AterLux
  • 4,566
  • 2
  • 10
  • 13