Skip to content

Conversation

@DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter DarkGhostHunter commented Oct 28, 2021

(Updated)

What?

Test traits now can be initialized automatically using setUp{trait}, created{trait} and destryoing{trait}, making traits shareable across multiple Test Classes.

  • setUp{trait} runs as soon the application is already created.
  • created{trait} runs after all traits have been set up.
  • destroying{trait} runs before the application is shut down.

How?

The main TestCase::setUpTraits() has been changed to get an array of all traits used recursively by the Test Case. While this may take the main Test Case Concerns (like InteractsWithContainer(), the difference in performance is negligible, specially if OPcache is enabled on CLI.

/**
 * Boot the testing helper traits.
 *
 * @return array
 */
protected function setUpTraits()
{
    // ...

    foreach (array_reverse($uses) as $trait => $int) {
        if (method_exists($trait, $method = 'setUp' . $trait)) {
            $this->{$method}();
        }

        if (method_exists($trait, $method = 'created' . $trait)) {
            $this->afterApplicationCreated([$this, $method]);
        }

        if (method_exists($trait, $method = 'destroying' . $trait)) {
            $this->beforeApplicationDestroyed([$this, $method]);
        }
    }

    return $uses;
}

Why?

To allow reusing the same trait across multiple Test Classes out of the box. This traits setups everything with just including it in the Test Classes the developer may need it.

<?php

namespace Tests\Feature\Concerns;

use App\Models\User;

trait WithSubscribedUser
{
    protected $user;
    
    protected $subscriptionsOpen = true; 
    
    protected function setUpWithSubscribedUser()
    {
        $this->app['config']->set('subscriptions.open', $this->subscriptionsOpen);
    }
    
    protected function createdWithSubscribedUser()
    {
        $this->user = User::factory()->subscribed()->createOne();
        
        $this->actingAs($this->user);
    }
    
    protected function destroyingWithSubscribedUser()
    {
        // Always assert the user is still subscribed whatever may happen!
        static::assertTrue($this->user->subscription->exists);

        // Free up the user from memory.
        $this->user = null;
    }
}

Later in their Test Classes:

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;
    use Concerns\WithSubscribedUser;

    protected $subscriptionsOpen = false;

    public function test_user_sees_subscription()
    {
        $this->get('user/subscription')->assertOk();
    }
}

It also works for deep nested traits. Since `

BC?

Yes, changes traits names, therefore:

  • Projects using conflicting setUp, created or destroying methods will have to change. (minor).

Caveats

By reversing the array of traits, deeply nested traits are executed first, making composite traits possible. The only caveat is that traits declared last will be executed first, but that could be fixed with a revised version of traits_uses_recursive().

TestClass/
├─ Sixth/
├─ Fifth/
│  ├─ Second
│  ├─ First
├─ Fourth
├─ Third

For example, this would be the order:

class ExampleTest
{
    use Fourth, Third, Second
}

trait Second
{
    use First;
}

Bonus:

A trait for an authenticated user could come with the default laravel/laravel scaffold.

@driesvints
Copy link
Member

Can we re-add the old methods that call the new ones to preserve BC? Otherwise this is going to have an enormous impact.

@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Oct 28, 2021 via email

$this->disableMiddlewareForAllTests();
}
foreach ($uses = class_uses_recursive(static::class) as $trait) {
if (method_exists($this, $method = 'setUp'.$trait)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do method_exists($trait, $method = 'setUp'.$trait) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it break something?

@caugner
Copy link
Contributor

caugner commented Oct 28, 2021

  1. Could we use tearDown{trait} instead of destroying{trait} to be consistent with PHPUnit terminology?
  2. Can I somehow influence the order of the traits? So far, I have a const TEST_TRAITS array for my own test traits, and these are setUp in that order and tearDown in the reverse order, because some traits interact with each other (e.g. WithUser and WithProject create a user and a project respectively, and if used together the user will have access to the project). Or should the created{trait} hook cover all those cases? 🤔

@taylorotwell
Copy link
Member

IMO this should only apply to userland traits. All built-in Laravel traits should be excluded and should be kept exactly as they were and in the same order as they were.

@taylorotwell taylorotwell marked this pull request as draft October 28, 2021 12:55
@DarkGhostHunter
Copy link
Contributor Author

DarkGhostHunter commented Oct 28, 2021 via email

@DarkGhostHunter DarkGhostHunter marked this pull request as ready for review October 28, 2021 22:19
@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If possible, please consider releasing your code as a package so that the community can still take advantage of your contributions!

If you feel absolutely certain that this code corrects a bug in the framework, please "@" mention me in a follow-up comment with further explanation so that GitHub will send me a notification of your response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants