如何仅在 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-&gt; 表示法时,测试方法只能有一行

就是这样。这样做是强制自定义连接名称(应该写在 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?的主要内容,如果未能解决你的问题,请参考以下文章

如何仅在 Gitlab 中更改的文件上运行 pylint?

Laravel 路由仅在 phpunit 测试期间返回 302 而不是 404 响应

如何在 XSLT 条件下更改硬编码元素的位置?

如何使用 phpunit 在 Laravel 4 中测试命名空间对象

尽量不在 SQL 查询中硬编码日期范围(Python、SQL 服务器)

Laravel 5.3 - 避免在 phpunit 测试中发送松弛通知