如何仅在 phpunit 测试中更改硬编码的 Eloquent $connection?
Posted
技术标签:
【中文标题】如何仅在 phpunit 测试中更改硬编码的 Eloquent $connection?【英文标题】:How to change hard coded Eloquent $connection only in phpunit tests? 【发布时间】:2020-06-05 21:33:54 【问题描述】:我有一个像这样的 Eloquent 模型:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SomeModel extends Model
protected $connection = 'global_connection';
......................
问题是这个 $connection 必须是硬编码的,因为我有一个多租户 Web 平台,所有租户都应该从这个数据库中读取。
但是现在在测试中,我正在访问控制器路由 store()
,但我无法访问模型!
我只是这样做:
public function store()
SomeModel::create($request->validated());
return response()->json(['msg' => 'Success']);
当通过浏览器作为用户使用时效果很好......
但现在我想以某种方式强制该模型不使用硬编码的 $connection 并将其设置为测试数据库连接...
这是我的测试
/** @test */
public function user_can_create_some_model(): void
$attributes = [
'name' => 'Some Name',
'title' => 'Some Title',
];
$response = $this->postJson($this->route, $attributes)->assertSuccessful();
有没有办法通过一些 Laravel 魔法来实现这一点:)?
【问题讨论】:
【参考方案1】:因为你要求使用 Laravel 魔法……就这样吧。可能是一种矫枉过正和过度设计的方式。
让我们首先创建一个接口,其唯一目的是定义一个返回连接字符串的函数。
app/Connection.php
namespace App;
interface Connection
public function getConnection();
然后让我们创建一个可以在现实世界(生产)中使用的具体实现。
app/GlobalConnection.php
namespace App;
class GlobalConnection implements Connection
public function getConnection()
return 'global-connection';
还有另一个我们可以在测试中使用的实现。
app/TestingConnection.php (你也可以把它放在你的tests
目录下,但一定要把命名空间改成合适的)
namespace App;
class TestingConnection implements Connection
public function getConnection()
return 'testing-connection';
现在让我们继续告诉 Laravel 我们要默认使用哪个具体实现。这可以通过转到app/Providers/AppServiceProvider.php
文件并在register
方法中添加此位来完成。
app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Connection;
use App\GlobalConnection;
// ...
public function register()
// ...
$this->app->bind(Connection::class, GlobalConnection::class);
// ...
让我们在模型中使用它。
app/SomeModel.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SomeModel extends Model
public function __construct(Connection $connection, $attributes = [])
parent::__construct($attributes);
$this->connection = $connection->getConnection();
// ...
差不多了。现在在我们的测试中,我们可以用TestingConnection
实现替换GlobalConnection
实现。方法如下。
测试/功能/ExampleTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Connection;
use App\TestingConnection;
class ExampleTest extends TestCase
public function setUp(): void
parent::setUp();
$this->app->instance(Connection::class, TestingConnection::class);
/** @test */
public function your_test()
// $connection is 'testing-connection' in here
代码未经测试,但应该可以工作。您还可以创建一个外观来静态访问该方法,然后使用 Mockery 模拟方法调用并在测试时返回所需的连接字符串。
【讨论】:
非常感谢。我发现您的解决方案正在寻找一种在包中使用不同连接的方法,而测试应该在本地完成。很好的解释。 实际上,我使用从 Eloquent\Model 扩展而来的 BaseModel 扩展了您的解决方案,因为在我的解决方案中,我只希望少数模型使用另一个连接。因此,此 BaseModel 会覆盖调用setConnection
-方法的 Eloquent 类的 newInstance
-方法。在那里我们可以调用服务容器来接收连接实现的正确实例。这种方式还允许在模型上使用静态方法,例如 ::all()
。【参考方案2】:
对我来说不幸的是,由于我针对多租户的特定数据库设置,这些答案都没有起到作用。我得到了一点帮助,这是解决这个问题的正确方法:
在 laravel 的 tests/ 目录下的某处创建一个自定义类 ConnectionResolver
<?php
namespace Tests;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\ConnectionResolver as IlluminateConnectionResolver;
class ConnectionResolver extends IlluminateConnectionResolver
protected $original;
protected $name;
public function __construct(ConnectionResolverInterface $original, string $name)
$this->original = $original;
$this->name = $name;
public function connection($name = null)
return $this->original->connection($this->name);
public function getDefaultConnection()
return $this->name;
在测试中这样使用它
在 tests/TestCase.php 中创建一个名为 create() 的方法
protected function create($attributes = [], $model = '', $route = '')
$this->withoutExceptionHandling();
$original = $model::getConnectionResolver();
$model::setConnectionResolver(new ConnectionResolver($original, 'testing'));
$response = $this->postJson($route, $attributes)->assertSuccessful();
$model = new $model;
$this->assertDatabaseHas('testing_db.'.$model->getTable(), $attributes);
$model::setConnectionResolver($original);
return $response;
在实际测试中,您可以简单地这样做:
/** @test */
public function user_can_create_model(): void
$attributes = [
'name' => 'Test Name',
'title' => 'Test Title',
'description' => 'Test Description',
];
$model = Model::class;
$route = 'model_store_route';
$this->create($attributes, $model, $route);
注意:使用 setUp() 方法和$this->
表示法时,测试方法只能有一行
就是这样。这样做是强制自定义连接名称(应该写在 config/database.php 中),并且无论您在模型中指定什么,该调用期间的模型都将使用该连接,因此它将数据存储到 DB 中你在$model::setConnectionResolver(new ConnectionResolver($original, 'HERE'));
中指定了
【讨论】:
我的回答基于这篇博文:datashaman.com/2020/02/21/testing-multi-connection-models【参考方案3】:在 Eloquent 模型中,您有以下方法。
/**
* Set the connection associated with the model.
*
* @param string|null $name
* @return $this
*/
public function setConnection($name)
$this->connection = $name;
return $this;
所以你可以这样做
$user = new User();
$user->setConnection('connectionName')
【讨论】:
如何在测试中做到这一点?后来在那个 store() 方法中,我调用了 Model::create() 我不确定你的意思,你能链接你的测试代码吗? 用实际测试更新了我的问题【参考方案4】:一种选择是创建一个仅用于测试的新环境文件,这样您就可以只为您的测试覆盖连接凭据,而不必接触您的模型:
tests/CreatesApplication.php
public function createApplication()
$app = require __DIR__ . '/../bootstrap/app.php';
$app->loadEnvironmentFrom('.env.testing'); // add this
$app->make(Kernel::class)->bootstrap();
return $app;
将您的.env
文件复制到.env.testing
,并将用于连接global_connection
的数据库凭据更改为您的测试数据库凭据。
我不确定您是如何配置连接的,但它可能类似于以下内容。
database.php
'global_connection' => [
'database' => env('DB_GLOBAL_DATABASE', ''),
'username' => env('DB_GLOBAL_USERNAME', ''),
'password' => env('DB_GLOBAL_PASSWORD', ''),
],
.env.testing:
DB_GLOBAL_DATABASE=database
DB_GLOBAL_USERNAME=username
DB_GLOBAL_PASSWORD=secret
现在您可以使用global_connection
连接,但它将使用您的测试数据库。
此外,您还可以从 phpunit.xml
文件中删除所有环境值,并将它们移动到 .env.testing
文件中,这样您就可以将测试的所有环境值放在一个位置。
如果您不想创建新的环境文件,当然可以只更新 phpunit.xml
文件中的值:
<php>
<server name="DB_GLOBAL_DATABASE" value="database"/>
<server name="DB_GLOBAL_USERNAME" value="username"/>
<server name="DB_GLOBAL_PASSWORD" value="password"/>
</php>
【讨论】:
【参考方案5】:我建议你可以做的最“神奇”的事情是只专注于测试,并尽量不修改模型:
/** @test */
public function user_can_create_some_model(): void
config([ "database.connections.global_connection" => [
'driver' => 'mysql', 'host' => x // basically override everything that is in config/database.php
]);
$attributes = [
'name' => 'Some Name',
'title' => 'Some Title',
];
$response = $this->postJson($this->route, $attributes)->assertSuccessful();
希望在需要读取配置时使用新配置。
如果您的 global_connection
配置是从 .env 文件中读取的,您还可以覆盖测试运行器配置中的 env 变量(例如 phpunit.xml)
【讨论】:
【参考方案6】:这是针对 Laravel 8 和 Super Simple 测试的。
这是一个在测试时切换连接的示例。 在你的模型中 ->
class YourModel extends Model
protected $connection = 'remote';
public function __construct(array $attributes = [])
if(config('app.env') === 'testing')
$this->connection = 'sqlite';
parent::__construct($attributes);
【讨论】:
以上是关于如何仅在 phpunit 测试中更改硬编码的 Eloquent $connection?的主要内容,如果未能解决你的问题,请参考以下文章
Laravel 路由仅在 phpunit 测试期间返回 302 而不是 404 响应
如何使用 phpunit 在 Laravel 4 中测试命名空间对象