Testbench incorrectly setting application namespace to "App\"
DigitalMachinist opened this issue · 4 comments
- Testbench Version: v7.23.0
- Laravel Version: v9.52.5
- PHP Version: v8.1.17
- Database Driver & Version: mysql Ver 8.0.32-0ubuntu0.20.04.2 for Linux on x86_64 ((Ubuntu))
Description:
I'm not sure that this is a bug, but I can't find any other useful sources about this, so here I am. I'm a long-time PHP/Laravel dev but this is my first time using testbench and doing package development.
I am developing a Laravel package that does not exist inside a full Laravel app skeleton -- its just requiring orchestral/testbench
. I have configured my composer.json
to map my package namespace to my package root src/
:
"autoload": {
"psr-4": {
"MyCompany\\MyPackage\\Data\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MyCompany\\MyPackage\\Data\\Tests\\": "tests/"
}
},
"require": {
"php": "^8.1",
"illuminate/contracts": "^9",
"ramsey/uuid": "^4.6",
"spatie/laravel-binary-uuid": "1.3.3",
"spatie/laravel-package-tools": "^1.14.0"
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^6",
"nunomaduro/larastan": "^2.0.1",
"orchestra/testbench": "^7",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9",
"spatie/laravel-ray": "^1.26"
},
However, when I run ./vendor/bin/phpunit
, I get the following error when using a factory to create any model:
Error: Class "App\ModelName" not found
Obviously, this doesn't agree with the namespace I have given. However, my static analysis tools respect the namespace I've provided in composer.json
. It seems like within my package, the namespace I have given is being handled correctly, but not by the Laravel Application
instance.
I've traced this back through how this namespace gets set, and it turns out that the Application
instance is checking the composer.json
file of laravel/laravel
for this value -- not the namespace I've given in the root of my package. I dumped out the contents of the composer.json
that Application
is reading when I try to create a model:
array:10 [ // /mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:1416
"name" => "laravel/laravel"
"description" => "The Laravel Framework."
"keywords" => array:2 [
0 => "framework"
1 => "laravel"
]
"license" => "MIT"
"type" => "project"
"require" => array:1 [
"laravel/framework" => "~5.0"
]
"require-dev" => array:1 [
"phpunit/phpunit" => "~4.0"
]
"autoload" => array:2 [
"classmap" => array:2 [
0 => "database"
1 => "tests/TestCase.php"
]
"psr-4" => array:1 [
"App\" => "app/"
]
]
"extra" => array:1 [
"laravel" => array:1 [
"dont-discover" => []
]
]
"minimum-stability" => "dev"
]
Which makes sense if you're using laravel/laravel
as a project skeleton, but not for package development.
As far as I'm aware, I'm following all of the normal practices for testing a Laravel package using testbench, and I can't find any other cases on google for this issue I'm having. And yet... I can't see how this error shouldn't happen to everyone.
What am I doing wrong here?
Steps To Reproduce:
- Make a new composer package and set up
composer.json
with the package namespace as above. - Add database models, factories and tests to run against those models.
composer require --dev orchestral/testbench:^7 phpunit/phpunit:^9
composer install
./vendor/bin/phpunit
Add actual reproducing code.
Sure. Does this give you enough to work with?
I can run ./vendor/bin/phpunit --filter=testOperatorHasManySessionsRelationship
and reproduce the issue with the setup described above.
The exact phpunit output is:
❯ ./vendor/bin/phpunit --filter=testOperatorHasManySessionsRelationship
PHPUnit 9.6.6 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 00:05.740, Memory: 28.00 MB
There was 1 error:
1) MyCompany\MyPackage\Data\Tests\Models\Operator\OperatorTest::testOperatorHasManySessionsRelationship
Error: Class "App\Operator" not found
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:767
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:412
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:155
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:418
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:385
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:279
/mnt/c/Users/jrose/Workspace/git/my-package-name/tests/Unit/Models/Operator/OperatorTest.php:18
tests/Unit/Models/Operator/Operator.php
namespace MyCompany\MyPackage\Data\Tests\Models\Operator;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Testing\RefreshDatabase;
use MyCompany\MyPackage\Data\Models\Operator\Operator;
use MyCompany\MyPackage\Data\Models\Session\Session;
use MyCompany\MyPackage\Data\Tests\TestCase;
class OperatorTest extends TestCase
{
use RefreshDatabase;
public function testOperatorHasManySessionsRelationship(): void
{
$count = 2;
$operator = Operator::factory()->create();
Session::factory()->count($count)->create([
'operator_uuid' => $operator->uuid,
]);
$this->assertInstanceOf(HasMany::class, $operator->sessions());
$this->assertCount($count, $operator->sessions()->get());
}
}
tests/TestCase.php
namespace MyCompany\MyPackage\Data\Tests;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Orchestra\Testbench\TestCase as Orchestra;
class TestCase extends Orchestra
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
Factory::guessFactoryNamesUsing(
fn (string $modelName) => 'MyCompany\\MyPackage\\Data\\Database\\Factories\\'.class_basename($modelName).'\\'.class_basename($modelName).'Factory'
);
}
protected function getPackageProviders($app)
{
return [
\MyCompany\MyPackage\Data\GamesServiceCoreDataModelServiceProvider::class,
\MyCompany\MyPackage\Data\Services\ApiKey\ApiKeyServiceProvider::class,
\MyCompany\MyPackage\Data\Services\DatabaseConnection\DatabaseConnectionServiceProvider::class,
\MyCompany\MyPackage\Data\Services\Lock\LockServiceProvider::class,
\MyCompany\MyPackage\Data\Services\OperatorModel\OperatorModelServiceProvider::class,
];
}
public function getEnvironmentSetUp($app)
{
// Make sure our .env file is loaded.
$app->useEnvironmentPath(__DIR__.'/..');
$app->bootstrapWith([LoadEnvironmentVariables::class]);
parent::getEnvironmentSetUp($app);
// Use mysql as our database connection.
config()->set('database.default', 'mysql');
config()->set('database.connections.mysql', [
'driver' => 'mysql',
'database' => env('DATABASE_NAME', 'testing'),
'host' => env('DATABASE_HOST', '127.0.0.1'),
'username' => env('DATABASE_USERNAME', ''),
'password' => env('DATABASE_PASSWORD', ''),
]);
}
}
src/Models/Operator/Operator.php
namespace MyCompany\MyPackage\Data\Models\Operator;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
use MyCompany\MyPackage\Data\Models\Session\Session;
use MyCompany\MyPackage\Data\Models\TransactionRequest\TransactionRequest;
use MyCompany\MyPackage\Data\Traits\SerializeBinaryUuids;
use Serializable;
use Spatie\BinaryUuid\HasBinaryUuid;
final class Operator extends Model implements Serializable
{
use HasBinaryUuid;
use HasFactory;
use Notifiable;
use SerializeBinaryUuids;
public const DEMO_OPERATOR_NAME = 'Demo';
//
// Settings
//
protected $primaryKey = 'uuid';
protected $keyType = 'string';
protected $dateFormat = 'c';
public $timestamps = true;
/**
* @var array<int, string>|null
*/
protected $binaryUuidFields = [
'uuid',
];
/**
* @var array<int, string>
*/
protected $fillable = [
'name',
'contact_email',
'base_url',
];
/**
* @var array<int, string>
*/
protected $hidden = [
'uuid',
'secret',
];
/**
* @var array<int, string>
*/
protected $dates = [
'started_at',
'ended_at',
'created_at',
'updated_at',
];
/**
* @var array<string, string>
*/
protected $casts = [
'status' => OperatorStatus::class,
];
//
// Boot
//
public static function boot(): void
{
parent::boot();
self::observe(new OperatorObserver());
}
//
// Custom Builder & Collection
//
/**
* @param \Illuminate\Database\Query\Builder $query
* @return OperatorBuilder
*/
public function newEloquentBuilder($query): OperatorBuilder
{
return new OperatorBuilder($query);
}
/**
* @param array<int, Operator> $models
* @return OperatorCollection
*/
public function newCollection(array $models = []): OperatorCollection
{
return new OperatorCollection($models);
}
//
// Relationships
//
/**
* @return HasMany<TransactionRequest>
*/
public function transaction_requests(): HasMany
{
return $this->hasMany(TransactionRequest::class);
}
/**
* @return HasMany<Session>
*/
public function sessions(): HasMany
{
return $this->hasMany(Session::class);
}
database/factories/Operator/OperatorFactory.php
namespace MyCompany\MyPackage\Data\Database\Factories\Operator;
use MyCompany\MyPackage\Data\Models\Operator\Operator;
use MyCompany\MyPackage\Data\Models\Operator\OperatorStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\MyCompany\MyPackage\Data\Models\Operator\Operator>
*/
class OperatorFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => Str::limit(fake()->company(), 16, ''),
'status' => OperatorStatus::ENABLED,
'contact_email' => fake()->email(),
'base_url' => url('api'),
'secret' => app(\MyCompany\MyPackage\Data\Services\ApiKey\ApiKeyServiceContract::class)->generate(64),
];
}
/**
* Indicate that the model's status should be set to 'disabled'.
*
* @return static
*/
public function disabled()
{
return $this->state(fn (array $attributes) => [
'status' => OperatorStatus::DISABLED,
]);
}
/**
* Indicate that the model's status should be set to 'disabled'.
*
* @return static
*/
public function enabled()
{
return $this->state(fn (array $attributes) => [
'status' => OperatorStatus::ENABLED,
]);
}
/**
* Indicate that the model's status should be set to 'disabled'.
*
* @return static
*/
public function demo()
{
return $this->state(fn (array $attributes) => [
'name' => Operator::DEMO_OPERATOR_NAME,
'contact_email' => 'gamesdemo@mycompany.com',
'base_url' => 'http://localhost',
'secret' => '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
]);
}
}
Error: Class "App\Operator" not found
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:767
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:412
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:155
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:418
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:385
/mnt/c/Users/jrose/Workspace/git/my-package-name/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factories/Factory.php:279
Clearly, from above it was caused by Eloquent Factory and not Testbench. Testbench doesn't mess with any of the factory and would recommend using UserFactory::new()->create()
instead of User::factory()->create()
.
See https://github.com/orchestral/testbench-core/blob/8.x/src/Factories/UserFactory.php
As it turns out, it makes no difference whether I use Operator::factory()
or OperatorFactory::new()
but what does make a difference is explicitly defining the model type in the factory as you've done in UserFactory there: https://github.com/orchestral/testbench-core/blob/8.x/src/Factories/UserFactory.php#L20
Good enough to unblock me for now! Thank you for linking that. This might be a helpful thing to mention in docs as its something of a gotcha if you use model factories at all for testing.