laracoft's avatar
Level 27

How do you test a trait like this?

use App\Models\Token;
use Illuminate\Database\Eloquent\Relations\HasMany;

trait HasTokens
{
    public function tokens(): HasMany
    {
        return $this->hasMany(Token::class);
    }

    public function createToken($activate = true)
    {
        $token = $this->tokens()->firstOrNew([
            'user_id' => auth()->user()?->id,
        ]);

        if ($activate) {
            $token->activateAndSave();
        } else {
            $token->save();
        }

        return $this;
    }
}

Specifically, how would you test the 2 methods createToken, and especially tokens? Or refactor so that they are testable

0 likes
7 replies
maxxd's avatar

You'll use feature tests to set up an in-memory database, run migrations, and seed it before you test the methods. Use the RefreshDatabase trait in the test to take care of all that (you'll obviously need to write the migrations and seeders, but I kinda assume that's already been done) - make sure you update the phpunit.xml file to with

        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>

This will override the .env file values so you don't inadvertently overwrite your entire database. You can specify which tables get seeded by passing either a class name or array of class names to $this->seed() in the setUp() method of your feature tests.

maxxd's avatar

@laracoft You could always use your dev database - don't use the RefreshDatabase trait and just create the token in your test data. No additional muss or fuss.

martinbean's avatar

@laracoft Personally, I just write tests for a class that implements the trait. You could even move those test cases to a trait for testing, and include that trait in multiple test classes:

namespace Tests\Feature\Concerns;

trait HasTokensTestCases
{
    // Test cases for HasTokens trait here...
}
class FooTest extends TestCase
{
    use HasTokensTestCases;
}
class BarTest extends TestCase
{
    use HasTokensTestCases;
}

So if you had two classes, Foo and Bar, that both used the HasTokens trait, you could then have a test class for each class, and your test cases would get ran for both tests:

Bar (Tests\Feature\BarTest)
 ✔ Can create token

Foo (Tests\Feature\FooTest)
 ✔ Can create token

OK (2 tests, 2 assertions)
1 like
laracoft's avatar
Level 27

@martinbean Hmm... I feel a good test should only be testing the 20+ lines of the trait and nothing else, i.e. execute those 20 lines of code + a small overhead... and only once, why would one have to test it twice?

martinbean's avatar
Level 80

@laracoft But a trait is useless on its own. A trait is used inside classes. You can’t instantiate a trait by itself, so it’s difficult to test its methods without using horrible workarounds like reflection.

Traits are also usually used to augment functionality in some other class, so even if you did use something like reflection to execute a method in a trait, that method is probably going to be relying on other properties or methods, particularly methods you add into Eloquent model classes.

Consider a Publishable trait that has a couple of methods like this:

trait Publishable
{
    public function publish(): bool
    {
        return $this->forceFill([
            'published_at' => Date::now(),
        ])->save();
    }

    public function isPublished(): bool
    {
        return $this->is_published && $this->is_published->isPast();
    }
}

This trait is useless on its own because it’s relying on the class it being used in to have a fluent forceFill method and a save method, and a published_at property, none of which exist in the trait itself. So if you use reflection to invoke the method, you’d then have to use some form of mocking or something for those methods and properties, in which case, what are you actually testing about the trait itself? Answer: extremely little. You’re not testing any actual behaviour.

So, this is why I prefer to test the trait’s functionality inside a class that actually uses the trait:

public function test_post_can_be_published(): void
{
    $post = Post::factory()->unpublished()->create();

    $this->assertFalse($post->isPublished());

    $post->publish();

    $this->assertTrue($post->fresh()->isPublished());
}

Please or to participate in this conversation.