ホームページ  >  記事  >  バックエンド開発  >  テスト可能で保守可能な PHP コードを作成する

テスト可能で保守可能な PHP コードを作成する

王林
王林オリジナル
2023-08-29 22:01:08819ブラウズ

创建可测试和可维护的 PHP 代码

フレームワークは、迅速なアプリケーション開発のためのツールを提供しますが、多くの場合、機能を早く作成するほど技術的負債が増加します。開発者にとって保守性が意図的に重視されていない場合、技術的負債が発生します。単体テストと構造が不足しているため、将来の変更とデバッグにはコストがかかります。

ここでは、テスト容易性と保守性を考慮してコードの構造化を開始し、時間を節約する方法を説明します。


(大まかに)

を取り上げます
    ###ドライ###
  1. 依存性の注入
  2. ###インターフェース### ###容器###
  3. 単体テストにPHPUnitを使用する
  4. 不自然ではあるが典型的なコードから始めましょう。これは、任意のフレームワークのモデル クラスになる可能性があります。
  5. リーリー
  6. このコードは機能しますが、改善の必要があります:
これはテストできません。

$_SESSION

グローバル変数に依存します。 PHPUnit などの単体テスト フレームワークはコマンド ラインに依存しており、
    $_SESSION
  1. やその他の多くのグローバル変数は使用できません。
    • 私たちはデータベース接続に依存しています。理想的には、単体テストでは実際のデータベース接続を回避する必要があります。テストはデータではなくコードに関するものです。 このコードの保守性は理想的ではありません。たとえば、データ ソースを変更する場合、アプリケーションで使用されるすべての
    • App::db
    • インスタンスのデータベース コードを変更する必要があります。また、現在のユーザーの情報が不要な場合はどうすればよいでしょうか?
  2. 試行された単体テスト
  3. ここでは、上記の機能の単体テストを作成する試みを示します。 リーリー ### それをチェックしよう。まず、テストは失敗します。
  4. User
オブジェクトで使用される

$_SESSION

変数は、コマンド ラインから PHP を実行するため、単体テストには存在しません。

第二に、データベース接続設定がありません。つまり、これを機能させるには、アプリケーションをブートストラップして

App オブジェクトとその db オブジェクトを取得する必要があります。テストするには、動作するデータベース接続も必要です。

この単体テストが機能するには、次のものが必要です:

アプリケーションで実行される CLI (PHPUnit) の構成をセットアップします データベース接続に依存します。これを行うことは、単体テストとは別のデータ ソースに依存することを意味します。テスト データベースに期待するデータが含まれていない場合はどうすればよいでしょうか?データベース接続が遅い場合はどうすればよいでしょうか?

ブートストラップに依存するアプリケーションは、テストにオーバーヘッドが追加されるため、単体テストの速度が大幅に低下する可能性があります。理想的には、コードのほとんどは、使用されているフレームワークに関係なくテストできます。

  1. それでは、これを改善する方法について話し合いましょう。
  2. コードをドライな状態に保ちます
  3. この単純なコンテキストでは、現在のユーザーを取得する関数は不要です。これは不自然な例ですが、DRY 原則の精神に基づき、私が選択した最初の最適化は、このアプローチを一般化することでした。
リーリー

これは、アプリケーション全体で使用できるメソッドを提供します。関数をモデルに渡す代わりに、呼び出し時に現在のユーザーを渡すことができます。セッション グローバル変数などの他の機能に依存しない場合、コードはよりモジュール化され、保守しやすくなります。


ただし、これは依然として意図したとおりにテストおよび保守することができません。私たちは依然としてデータベース接続に依存しています。

依存性の注入

依存関係の注入を追加して、この状況を改善しましょう。データベース接続をクラスに渡すと、モデルは次のようになります。

リーリー

これで、

User
モデルの依存関係が提供されました。私たちのクラスは、特定のデータベース接続を想定しなくなり、グローバル オブジェクトにも依存しなくなりました。

この時点で、クラスは基本的にテストの準備が整いました。 (ほとんどの場合) 選択したデータ ソースとユーザー ID を渡し、その呼び出しの結果をテストできます。別々のデータベース接続を切り替えることもできます (両方が同じデータ取得方法を実装していると仮定します)。いいね。

単体テストがどのようなものかを見てみましょう。

リーリー この単体テストに新しいもの、Mockery を追加しました。 Mockery を使用すると、PHP オブジェクトを「モック」(偽装) できます。この例では、データベース接続をシミュレートしています。このモックを使用すると、データベース接続のテストを省略して、モデルをテストするだけです。

Mockery について詳しく知りたいですか?

この例では、SQL 接続をシミュレートしています。

select

where

limit
、および

get メソッドの呼び出しを予期するようにモック オブジェクトに指示します。 SQL 接続オブジェクトがそれ自体を返す方法 ($this) を反映するためにモック自体を返し、そのメソッド呼び出しを「チェーン可能」にします。 get メソッドの場合、データベース呼び出しの結果、つまりユーザー データが設定された stdClass オブジェクトを返すことに注意してください。 これによりいくつかの問題が解決されます:

  1. 我们仅测试我们的模型类。我们还没有测试数据库连接。
  2. 我们能够控制模拟数据库连接的输入和输出,因此可以可靠地测试数据库调用的结果。我知道由于模拟数据库调用,我将获得用户 ID“1”。
  3. 我们不需要引导我们的应用程序,也不需要提供任何配置或数据库来进行测试。

我们还可以做得更好。这就是它变得有趣的地方。


接口

为了进一步改进这一点,我们可以定义并实现一个接口。考虑以下代码。

interface UserRepositoryInterface {
    public function getUser($user_id);
}

class MysqlUserRepository implements UserRepositoryInterface {

    protected $_db;

    public function __construct($db_conn)
    {
        $this->_db = $db_conn;
    }

    public function getUser($user_id)
    {
        $user = $this->_db->select('user')
                    ->where('id', $user_id)
                    ->limit(1)
                    ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return false;
    }

}

class User {

    protected $userStore;

    public function __construct(UserRepositoryInterface $user)
    {
        $this->userStore = $user;
    }

    public function getUser($user_id)
    {
        return $this->userStore->getUser($user_id);
    }

}

这里发生了一些事情。

  1. 首先,我们为用户数据源定义一个接口。这定义了 addUser() 方法。
  2. 接下来,我们实现该接口。在本例中,我们创建一个 MySQL 实现。我们接受一个数据库连接对象,并使用它从数据库中获取用户。
  3. 最后,我们在 User 模型中强制使用实现 UserInterface 的类。这样可以保证数据源始终有一个可用的 getUser() 方法,无论使用哪个数据源来实现 UserInterface

请注意,我们的 User 对象类型提示 UserInterface 在其构造函数中。这意味着实现 UserInterface 的类必须传递到 User 对象中。这是我们所依赖的保证 - 我们需要 getUser 方法始终可用。

这样做的结果是什么?

  • 我们的代码现在完全可测试。对于User类,我们可以轻松地模拟数据源。 (测试数据源的实现将是单独的单元测试的工作)。
  • 我们的代码更加易于维护。我们可以切换不同的数据源,而无需更改整个应用程序的代码。
  • 我们可以创建任何数据源。 ArrayUser、MongoDbUser、CouchDbUser、MemoryUser 等
  • 如果需要,我们可以轻松地将任何数据源传递到我们的 User 对象。如果您决定放弃 SQL,则只需创建一个不同的实现(例如 MongoDbUser)并将其传递到您的 User 模型中。

我们还简化了单元测试!

<?php

use Mockery as m;
use Fideloper\User;

class ThirdUserTest extends PHPUnit_Framework_TestCase {

    public function testGetCurrentUserMock()
    {
        $userRepo = $this->_mockUserRepo();

        $user = new User( $userRepo );

        $result = $user->getUser( 1 );

        $expected = new StdClass();
        $expected->id = 1;
        $expected->username = 'fideloper';

        $this->assertEquals( $result->id, $expected->id, 'User ID set correctly' );
        $this->assertEquals( $result->username, $expected->username, 'Username set correctly' );
    }

    protected function _mockUserRepo()
    {
        // Mock expected result
        $result = new StdClass();
        $result->id = 1;
        $result->username = 'fideloper';

        // Mock any user repository
        $userRepo = m::mock('Fideloper\Third\Repository\UserRepositoryInterface');
        $userRepo->shouldReceive('getUser')->once()->andReturn( $result );

        return $userRepo;
    }

}

我们已经完全取消了模拟数据库连接的工作。相反,我们只是模拟数据源,并告诉它当调用 getUser 时要做什么。

但是,我们仍然可以做得更好!


容器

考虑我们当前代码的用法:

// In some controller
$user = new User( new MysqlUser( App:db->getConnection("mysql") ) );
$user->id = App::session("user->id");

$currentUser = $user->getUser($user_id);

我们的最后一步是引入容器。容器。在上面的代码中,我们需要创建并使用一堆对象来获取当前用户。此代码可能散布在您的应用程序中。如果您需要从 MySQL 切换到 MongoDB,您仍然需要编辑上述代码出现的每个位置。那几乎不是干的。容器可以解决这个问题。

容器只是“包含”一个对象或功能。它类似于应用程序中的注册表。我们可以使用容器自动实例化一个新的 User 对象以及所有需要的依赖项。下面,我使用 Pimple,一个流行的容器类。

// Somewhere in a configuration file
$container = new Pimple();
$container["user"] = function() {
    return new User( new MysqlUser( App:db->getConnection('mysql') ) );
}

// Now, in all of our controllers, we can simply write:
$currentUser = $container['user']->getUser( App::session('user_id') );

我已将 User 模型的创建移至应用程序配置中的一个位置。结果是:

  1. 我们的代码保持干燥。 User 对象和选择的数据存储在我们应用程序的一个位置定义。
  2. 我们可以将 User 模型从使用 MySQL 切换到 ONE 位置中的任何其他数据源。这更易于维护。

最终想法

在本教程的过程中,我们完成了以下任务:

  1. 保持我们的代码干燥且可重用
  2. 创建了可维护的代码 - 如果需要,我们可以在整个应用程序的一个位置切换对象的数据源
  3. 使我们的代码可测试 - 我们可以轻松模拟对象,而无需依赖引导我们的应用程序或创建测试数据库
  4. 了解如何使用依赖注入和接口来创建可测试和可维护的代码
  5. 了解容器如何帮助我们的应用程序更易于维护

我相信您已经注意到,我们以可维护性和可测试性的名义添加了更多代码。可以对这种实现提出强有力的论据:我们正在增加复杂性。事实上,这需要项目的主要作者和合作者对代码有更深入的了解。

但是,技术债务总体减少远远超过了解释和理解的成本。

  • 代码的可维护性大大提高,可以在一个位置而不是多个位置进行更改。
  • 能够(快速)进行单元测试将大幅减少代码中的错误 - 特别是在长期或社区驱动的(开源)项目中。
  • 提前做额外的工作节省时间并减少以后的麻烦。

资源

您可以使用 Composer 轻松地将 MockeryPHPUnit 包含到您的应用程序中。将这些添加到 composer.json 文件中的“require-dev”部分:

"require-dev": {
    "mockery/mockery": "0.8.*",
    "phpunit/phpunit": "3.7.*"
}

然后,您可以按照“dev”要求安装基于 Composer 的依赖项:

$ php composer.phar install --dev

在 Nettuts+ 上了解有关 Mockery、Composer 和 PHPUnit 的更多信息。

  • 嘲笑:更好的方法
  • 使用 Composer 轻松进行包管理
  • 测试驱动的 PHP

对于 PHP,请考虑使用 Laravel 4,因为它特别利用了容器和此处介绍的其他概念。

感谢您的阅读!

以上がテスト可能で保守可能な PHP コードを作成するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。