Kiểm tra đơn vị - Xử lý các phụ thuộc


7

Điều này có thể được coi là một hệ quả của kiểm tra gọi lại hook hook .

Vấn đề: Tôi muốn kiểm tra một lớp tạo ra một thể hiện mới của một My_Noticelớp được xác định bên ngoài plugin (hãy gọi nó là "Plugin chính").

Kiểm tra đơn vị của tôi không biết gì My_Noticevì nó được xác định trong thư viện của bên thứ ba (chính xác là một plugin khác). Do đó, tôi có các tùy chọn này (theo như tôi biết):

  1. Sơ khai My_Noticelớp: khó duy trì
  2. Bao gồm các tệp cần thiết từ thư viện bên thứ ba: điều này có thể hoạt động, nhưng tôi đang làm cho các thử nghiệm của mình ít bị cô lập hơn
  3. Tự động khai thác lớp: không chắc điều đó có khả thi hay không, nhưng nó sẽ rất giống với việc chế nhạo một lớp, ngoại trừ việc nó cũng sẽ tạo ra định nghĩa tương tự, vì vậy lớp tôi đang kiểm tra sẽ có thể khởi tạo nó.

Câu trả lời từ @gmazzap chỉ ra rằng chúng ta nên tránh tạo ra loại phụ thuộc này là điều tôi hoàn toàn đồng ý. Và ý tưởng tạo các lớp sơ khai có vẻ không tốt đối với tôi: Tôi muốn bao gồm mã của "Plugin chính", với tất cả các hậu quả).

Tuy nhiên tôi không thấy làm thế nào tôi có thể làm khác.

Đây là một ví dụ về mã tôi đang cố kiểm tra:

class My_Admin_Notices_Handler {
    public function __construct( My_Notices $admin_notices ) {
        $this->admin_notices = $admin_notices;
    }

    /**
     * This will be hooked to the `activated_plugin` action
     *
     * @param string $plugin
     * @param bool   $network_wide
     */
    public function activated_plugin( $plugin, $network_wide ) {
        $this->add_notice( 'plugin', 'activated', $plugin );
    }

    /**
     * @param string $type
     * @param string $action
     * @param string $plugin
     */
    private function add_notice( $type, $action, $plugin ) {
        $message = '';
        if ( 'activated' === $action ) {
            if ( 'plugin' === $type ) {
                $message = __( '%1s Some message for plugin(s)', 'my-test-domain' );
            }
            if ( 'theme' === $type ) {
                $message = __( '%1s Some message for the theme', 'my-test-domain' );
            }
        }
        if ( 'updated' === $action && ( 'plugin' === $type || 'theme' === $type ) ) {
            $message = __( '%1s Another message for updated theme or plugin(s)', 'my-test-domain' );
        }

        if ( $message ) {
            $notice          = new My_Notice( $plugin, 'wpml-st-string-scan' );
            $notice->text    = $message;
            $notice->actions = array(
                new My_Admin_Notice_Action( __( 'Scan now', 'my-test-domain' ), '#' ),
                new My_Admin_Notice_Action( __( 'Skip', 'my-test-domain' ), '#', true ),
            );
            $this->admin_notices->add_notice( $notice );
        }
    }
}

Về cơ bản, lớp này có một phương thức sẽ được nối vào activated_plugin. Phương thức này xây dựng một thể hiện của lớp "thông báo", sẽ được lưu trữ ở đâu đó bởi My_Noticesthể hiện được truyền cho hàm tạo.

Hàm tạo của My_Noticelớp nhận hai đối số cơ bản (UID và "nhóm") và được đặt một số thuộc tính (lưu ý rằng cùng một vấn đề với My_Admin_Notice_Actionlớp).

Làm thế nào tôi có thể làm cho My_Noticelớp một phụ thuộc được tiêm?

Tất nhiên, tôi có thể sử dụng một mảng kết hợp, gọi một số hành động, được nối bởi "Plugin chính" và dịch mảng đó trong các đối số của lớp, nhưng nó không rõ ràng đối với tôi.


Vấn đề có lẽ là cách bạn cấu trúc mã của mình chứ không phải cách triển khai phương pháp A hoặc B, nhưng các câu hỏi kiểm tra đơn vị chung chung như thế này có thể được hỏi tốt hơn tại SO. Tôi thực sự không thấy bất kỳ vấn đề nào với việc kiểm tra mã của bạn để thậm chí không thể hiểu vấn đề bạn đang gặp phải là gì. BTW chỉ vì một câu trả lời có 30 lượt upvote không có nghĩa đó là cách duy nhất hợp lệ để thực hiện
Mark Kaplun

@MarkKaplun Tôi đã thêm một số giải thích về vấn đề kiểm tra lớp này là gì. Tôi hy vọng điều này có ý nghĩa hơn. Tôi đã thêm câu hỏi ở đây, như một câu hỏi tiếp theo của câu hỏi gốc, trong cùng một mạng. Vì vấn đề rất giống với vấn đề được đặt ra trong câu hỏi ban đầu, tôi không chắc chắn 100% tôi phải chuyển nó tại SO, nhưng mọi hướng dẫn đều rất đáng hoan nghênh!
Andrea Sciamanna

1
Đó là thời điểm khác nhau trong đó các quy tắc ở đây là khác nhau, và câu trả lời chỉ là không tốt. Mặc dù mọi từ đều đúng và tôi đồng ý với nó, toàn bộ vấn đề viết một plugin cho wordpress đang tích hợp với nó, vì vậy việc kiểm tra riêng rẽ mang lại cho bạn rất ít đặc biệt là nếu mã của bạn như trong mã bạn hiển thị ở đây tương đối tầm thường. Thử nghiệm trong sự cô lập là tuyệt vời, nhưng thử nghiệm cũng phải hữu ích và không chỉ thuần túy.
Đánh dấu Kaplun

Câu trả lời:


7

Là đối tượng tiêm thực sự cần thiết?

Để có thể kiểm tra đầy đủ trong sự cô lập, mã cần tránh để khởi tạo trực tiếp các lớp và tiêm bất kỳ đối tượng nào cần thiết vào bên trong các đối tượng.

Tuy nhiên, có ít nhất hai ngoại lệ cho quy tắc này:

  1. Lớp học là một lớp ngôn ngữ, vd ArrayObject
  2. Lớp học là một "đối tượng giá trị" thích hợp .

Không cần ép buộc cho các đối tượng cốt lõi

Trường hợp đầu tiên rất dễ giải thích: đó là một cái gì đó được nhúng trong ngôn ngữ để bạn chỉ cần giả sử nó hoạt động. Nếu không, bạn cũng nên kiểm tra bất kỳ chức năng lõi PHP hoặc tuyên bố như thế return...

Không cần ép buộc đối tượng giá trị

Trường hợp thứ hai, liên quan đến bản chất của một đối tượng giá trị. Thực ra:

  • nó là bất biến
  • nó không có bất kỳ sự thay thế đa hình nào
  • theo định nghĩa, một đối tượng giá trị không thể phân biệt với một đối tượng khác có cùng các đối số hàm tạo

Nó có nghĩa là một đối tượng giá trị có thể được xem như là một loại bất biến của chính nó, giống như, ví dụ, một chuỗi.

Nếu một số mã $myEmail = 'some.email@example.com'không ai quan tâm đến việc chế nhạo chuỗi đó, và theo cách tương tự, không ai nên quan tâm đến việc chế nhạo một dòng như new Email('some_name@example.com')(giả sử đó Emaillà bất biến và có thể final).

Đối với những gì tôi có thể đoán từ mã của bạn, My_Admin_Notice_Actionlà một ứng cử viên tốt để trở thành / trở thành một đối tượng giá trị. Nhưng, không thể chắc chắn mà không nhìn thấy mã.

Tôi không thể nói như vậy My_Notice, nhưng đó chỉ là một phỏng đoán khác.

Nếu tiêm là cần thiết

Trong trường hợp lớp được khởi tạo trong một lớp khác không phải là một trong hai trường hợp trên, điều đó chắc chắn tốt hơn để tiêm nó.

Tuy nhiên, giống như ví dụ trong OP, lớp được xây dựng cần các đối số phụ thuộc vào ngữ cảnh.

Không có câu trả lời "một" trong trường hợp này, nhưng các cách tiếp cận khác nhau có thể hoàn toàn hợp lệ tùy theo trường hợp.

Khởi tạo trong mã máy khách

Một cách tiếp cận đơn giản là tách "mã đối tượng" khỏi "mã máy khách". Trong đó mã khách hàng là mã sử dụng các đối tượng.

Theo cách này, bạn có thể kiểm tra mã đối tượng bằng các kiểm tra đơn vị và để kiểm tra mã máy khách cho các kiểm tra chức năng / tích hợp, trong đó bạn không phải lo lắng về sự cô lập.

Trong trường hợp của bạn, nó sẽ là một cái gì đó như:

add_action( 'activate_plugin', function( $plugin, $network_wide ) {

   $message = My_Admin_Notices_Message( 'plugin', 'activated' );

   if ( $message->has_text() ) {

      $notice = new My_Notice( $plugin, $message, 'wpml-st-string-scan' );
      $notice->add_action( new My_Admin_Notice_Action( 'Scan now', '#' ) );
      $notice->add_action( new My_Admin_Notice_Action( 'Skip', '#', true ) );

      $notices = new My_Notices();
      $notices->add_notice( $notice );

      $handler = new My_Admin_Notices_Handler( $notices );
      $handler->handle_notices();
   }

}, 10, 2);

Tôi đã đưa ra một số dự đoán về mã của bạn và viết các phương thức và các lớp có thể không tồn tại (như My_Admin_Notices_Message), nhưng vấn đề ở đây là việc đóng ở trên có chứa tất cả mã khách hàng cần để khởi tạo và "sử dụng" các đối tượng. Sau đó, bạn có thể kiểm tra các đối tượng của mình một cách cô lập vì không ai trong số các đối tượng đó cần khởi tạo các đối tượng khác, nhưng tất cả chúng đều nhận được các thể hiện cần thiết trong hàm tạo hoặc dưới dạng tham số phương thức.

Đơn giản hóa mã khách hàng với các nhà máy

Cách tiếp cận ở trên có thể hoạt động tốt đối với các plugin nhỏ (hoặc một phần nhỏ của plugin có thể tách biệt với phần còn lại của plugin), nhưng đối với các cơ sở mã lớn hơn, chỉ sử dụng cách tiếp cận đó, bạn có thể kết thúc bằng mã máy khách lớn, trong số đó điều, rất khó để kiểm tra và duy trì.

Trong những trường hợp đó, các nhà máy có thể giúp bạn. Các nhà máy là các đối tượng với phạm vi duy nhất của các đối tượng khác. Hầu hết thời gian là tốt để có các nhà máy cụ thể cho các đối tượng cùng loại (thực hiện cùng một giao diện).

Với các nhà máy, mã ở trên có thể trông như thế này:

add_action( 'activate_plugin', function( $plugin, $network_wide ) {

   $notice = $notice_factory->create_for( 'plugin', 'activated' );

   if ( $notice instanceof My_Notice_Interface ) {
      $handler_factory = new My_Admin_Notices_Handler_Factory();
      $handler = $handler_factory->build_for_notices( [ $notice ] );
      $handler->handle_notices();
   }

}, 10, 2);

Tất cả các mã khởi tạo là trong các nhà máy. Bạn vẫn có thể kiểm tra các nhà máy một cách cô lập, bởi vì bạn cần kiểm tra các đối số phù hợp mà họ tạo ra các lớp dự kiến ​​(hoặc đưa ra các đối số sai mà họ tạo ra các lỗi dự kiến).

Và bạn vẫn có thể kiểm tra tất cả các đối tượng khác một cách cô lập vì không có đối tượng nào cần tạo phiên bản, trong thực tế, mã khởi tạo là tất cả trong các nhà máy.

Tất nhiên, hãy nhớ rằng các đối tượng giá trị không cần nhà máy ... nó sẽ giống như tạo ra các nhà máy cho chuỗi ...

Nếu tôi không thể thay đổi mã thì sao?

Sơ khai

Đôi khi không thể thay đổi mã khởi tạo các đối tượng khác, vì những lý do khác nhau. Ví dụ mã là bên thứ 3, tính tương thích ngược, v.v.

Trong các trường hợp đó, nếu có thể chạy các bài kiểm tra mà không tải các lớp đang được khởi tạo, thì bạn có thể viết một số sơ khai.

Giả sử bạn có một mã làm:

class Foo {

   public function run_something() {
     $something = new Something();
     $something->run();
   }
}

Nếu bạn có thể chạy thử nghiệm mà không cần tải Somethinglớp, bạn có thể viết một Somethinglớp tùy chỉnh chỉ với mục đích thử nghiệm (một "sơ khai").

Luôn luôn tốt hơn để giữ sơ khai rất đơn giản, ví dụ:

class Something{

   public function run() {
     return 'I ran'.
   }
}

Khi các bài kiểm tra chạy, sau đó bạn có thể tải tệp chứa sơ khai này cho Somethinglớp (ví dụ từ các bài kiểm tra setUp()) và khi lớp được kiểm tra sẽ khởi tạo một new Something, bạn sẽ kiểm tra nó một cách đơn giản, vì thiết lập rất đơn giản và bạn có thể tạo nó trong một cách mà theo thiết kế nó làm những gì bạn mong đợi.

Tất nhiên điều này không đơn giản để duy trì, nhưng xem xét rằng thông thường bạn không kiểm tra đơn vị mã bên thứ 3, hiếm khi bạn cần làm điều này.

Tuy nhiên, đôi khi, điều này hữu ích cho việc thử nghiệm các plugin / mã chủ đề cô lập để khởi tạo các đối tượng WordPress (ví dụ WP_Post).

Mockery quá tải

Sử dụng Mockery(một thư viện để cung cấp các công cụ cho các bài kiểm tra đơn vị PHP), bạn thậm chí có thể tránh để viết các sơ khai đó. Với Mockery "dụ mock" (còn gọi là "quá tải") có thể chặn việc tạo cá thể mới và thay thế bằng một giả. Bài viết này giải thích khá tốt làm thế nào để làm điều đó.

Khi lớp được tải ...

Nếu mã để kiểm tra có các phụ thuộc cứng (khởi tạo các lớp đang sử dụng new) và không có khả năng tải các kiểm tra mà không tải lớp sẽ được khởi tạo thì sẽ có rất ít cơ hội để kiểm tra nó một cách đơn lẻ mà không chạm vào mã (hoặc viết một bản tóm tắt lớp xung quanh nó).

Tuy nhiên, lưu ý rằng tệp bootstrap thử nghiệm thường được tải dưới dạng điều đầu tiên tuyệt đối. Vì vậy, ít nhất, có hai trường hợp trong bạn có thể buộc tải các cuống của bạn:

  1. Mã sử ​​dụng một trình tải tự động. Trong trường hợp này, nếu bạn tải sơ khai trước khi tải trình tải tự động, thì lớp "thực" không bao giờ được tải, bởi vì khi newđược sử dụng, lớp đã được tìm thấy và trình tải tự động không được kích hoạt.

  2. Mã kiểm tra class_existstrước khi xác định / tải lớp. Trong trường hợp đó để tải các sơ khai như điều đầu tiên, sẽ ngăn lớp "thực" được tải.

Phương án cuối cùng khó khăn

Khi mọi thứ khác thất bại, có một điều khác bạn có thể làm để kiểm tra các phụ thuộc cứng.

Các phụ thuộc rất thường được lưu trữ dưới dạng các biến riêng tư.

class Foo {

   public function __construct() {
     $this->something = new Something();
   }

   public function run_something() {
     return $this->something->run();
   }
}

trong những trường hợp như thế này, bạn có thể thay thế các phụ thuộc cứng bằng mock / stub sau khi thể hiện được tạo.

Điều này bởi vì privatecác thuộc tính thậm chí có thể dễ dàng được thay thế trong PHP. Trong nhiều hơn một cách, thực sự.

Không đào sâu vào chi tiết, tôi có thể nói rằng ràng buộc đóng có thể được sử dụng để làm điều đó. Tôi thậm chí đã viết một thư viện gọi là "Andrew" có thể được sử dụng cho phạm vi.

Sử dụng Andrew (và Mockery) để kiểm tra lớp Fooở trên, bạn có thể làm:

public function test_run_something() {

    $foo = new Foo();

    $something_mock = Mockery::mock( 'Something' );
    $something_mock
        ->shouldReceive('run')
        ->once()
        ->withNoArgs()
        ->andReturn('I ran.');

    // changing proxy properties will change private properties of proxied object
    $proxy = new Andrew\Proxy( $foo );
    $proxy->something = $something_mock;

    $this->assertSame( 'I ran.', $foo->run_something() );
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.