如何使用 PHPUnit 測試 private 與 protected method?

   2016-11-22 0
核心提示:剛開始學習寫測試時,最多人的疑問就是該如何測試 private 與 protected method?,理論上不該去測試 private 與 protected ,本文會介紹一個 PHP 邪惡的技巧來完成測試,但建議除非萬不得已,不要使用此方法。 Motivation 在 PHPConf 第二天的 workshop,我曾

剛開始學習寫測試時,最多人的疑問就是該如何測試 privateprotected method?,理論上不該去測試 privateprotected ,本文會介紹一個 PHP 邪惡的技巧來完成測試,但建議除非萬不得已,不要使用此方法。

Motivation

在 PHPConf 第二天的 workshop,我曾經舉手問 PHPUnit 的原作者 Sabastian Bergmann: How to test private and protected method? ,Sabastian 的回答也很鏗鏘有力: You can't ,實務上我也真的沒測試過 privateprotected method,不過藉此機會理解為什麼不該測試 privateprotected 也是不錯的。

Version

PHP 7.0.8

Laravel 5.3.10

PHPUnit 5.5.5

為什麼不該測試 priavate 與 protected?

  1. 測試案例是來自於需求, public 才是來自於需求,而 privateprotected 則是來自於 重構 ,所以不應該特別去測試,而應該由 public 的測試案例自然去測試 privateprotected

  2. 若特別去測試 privateprotected ,則 coverage 將沒有意義,可以特別只針對 privateprotected 寫測試,而達成 coverage 為 100% ,正確方法應該只測試 public ,若有些 privateprotected 因而沒測試到,則有兩種可能:一個是測試案例不足,導致 privateprotected 沒測到,另一個則是目前根本無此需求, privateprotected 為 over design。

  3. 根據物件導向的 封裝 特性, public 會根據 interface 而穩定,但 privateprotected 則會隨著日後 重構 而變化,若直接針對 privateprotected 寫測試,則日後只要一重構,就必須修改測試,則將影響測試程式的 健狀性 ,測試應該隨著 需求 改變而修改,不該隨著程式碼 重構 而修改。

實際案例

若真的萬不得已,需要對 privateprotected 做測試時,以下介紹一個簡單的方式。

ShippingService.php 1 1 GitHub Commit : 新增 ShippingService

Services/ShippingService.php
declare(strict_types = 1);

namespace App\Services;

class ShippingService
{
    /**
     * 計算運費
     * @param int $weight
     * @return int
     */
    private function calculateFee(int $weight) : int
    {
        return 100 * $weight + 10;
    }
}

calculateFee()private ,我們該怎麼為這段程式補上測試呢?

單元測試 ShippingServiceTest.php 2 2 GitHub Commit : 單元測試 : ShippingService 使用 Closure::call()

tests/Services/ShippingServiceTest.php
declare(strict_types = 1);

use App\Services\BlackCat;
use App\Services\LogisticsInterface;
use App\Services\ShippingService;

class ShippingServiceTest extends TestCase
{
    /** @test */
    public function 當重量為1kg時費用為110元()
    {
        /** arrange */
        $target = App::make(ShippingService::class);

        $__calculateFee = function (int $weight) {
            return $this->calculateFee($weight);
        };

        /** act */
        $weight = 1;
        $actual = $__calculateFee->call($target, $weight);

        /** assert */
        $expected = 110;
        $this->assertEquals($expected, $actual);
    }
}

13 行

$target = App::make(ShippingService::class);

建立 $target 測試物件,在本範例寫 $target = new ShippingService() 亦可,不過若在 class 內有使用到依賴注入,則必須使用 App::make() , 此時 Laravel 的 service container 會自動幫你啟動依賴注入,注入相對應的物件,實務上在寫測試時,建議使用 App::make() 取代 new

15 行

$__calculateFee = function (int $weight) {
    return $this->calculateFee($weight);
};

因為 calculateFee()private ,我們無法測試,因此特別建立一個 $__calculateFee closure,由 closure 內部去呼叫 privatecalculateFee()

20 行

$weight = 1;
$actual = $__calculateFee->call($target, $weight);

在 PHP 7, Closure 物件新提供了 call() ,可以讓我們直接將自己建立的 closure 動態綁定到一個物件上。 3 3 關於 PHP 7 的 Closure::call() ,詳細請參考 Closure::call

closure 內的 $this ,就會如同 JavaScriptthis 一樣,自動指向被綁定的物件,如此我們就可以由自己建立的 closure,透過 this 去存取 private method。

call() 的第一個參數為要綁定的物件,之後的參數為要傳給 closure 的參數。 4 4 事實上這就是 PHP 5.4 所提供的 bindTo() ,只是 PHP 7 的 Closure::bind() 可讀性更高,若想了解 bindTo() ,詳細請參考深入探討 bindTo()

24 行

$expected = 110;
$this->assertEquals($expected, $actual);

測試結果是否如預期。

如何使用 PHPUnit 測試 private 與 protected method?

實際跑單元測試,會得到 綠燈

Conclusion

  • 非到萬不得已,不應該直接測試 privateprotected
  • 本文以 private 為範例,也可以套用在 protected ,一樣使用 Closure::call() 的方式。
  • Closure::call() 違反物件導向封裝原則,實務上不建議使用,除非真的萬不得已。

Sample Code

完整的範例可以在我的 GitHub 上找到。

Reference

PHP, Closure::call

 
标签: PHPUnit
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • PHPUnit 5.7.9 发布,PHP 单元测试框架
    PHPUnit 5.7.9 发布,PHP 单元测试框架
    PHPUnit 5.7.9 发布,PHP 单元测试框架王练 发布于2017年02月03日 收藏 0 MongoDB漏洞是什么?应该如何防护? PHPUnit 5.7.9发布了,PHPUnit是面向 PHP 的测试框架,是单元测试框架xUnit 的一个实例。 更新内容:修复:Reverted backwards incompatible chang
  • PHPUnit 6.0.3 和 5.7.10 发布,PHP 单元测试框架
    PHPUnit 6.0.3 和 5.7.10 发布,PHP 单元测试框
    PHPUnit 6.0.3 和 5.7.10 发布,PHP 单元测试框架王练 发布于2017年02月05日 收藏 0 MongoDB漏洞是什么?应该如何防护? PHPUnit 6.0.3和 5.7.10 发布了,PHPUnit是面向 PHP 的测试框架,是单元测试框架xUnit 的一个实例。 6.0.3 更新内容: Fixed #2460 : 更
  • PHPUnit入门基础教程
    PHPUnit入门基础教程
    PHPUnit是PHP语言的单元测试框架、工具,xunit单元测试工具系列成员之一,可以单独运行在Linux或windows系统下面,也可以集成到zend studio等IDE工具中。 工具下载: https://phpunit.de/index.html在线手册: https://phpunit.de/manual/4.6/zh_cn/index.htm
    02-05 PHPUnit
  • 组合使用Laravel和vfsStream测试文件上传
    核心要点 在应用开发中,测试是很重要的,在诸多的开发工具中,测试驱动开发是很伟大的一项; 测试文件上传并不像人们想象的那么简单; 目前,有很多很棒,但不为大家所熟知的测试工具; Larval能够让请求的校验更容易; 测试并不需要实际的文件系统,因为如
  • 如何使用 PHPUnit mock Closure?
    如何使用 PHPUnit mock Closure?
    若有需求需要抽換,物件導向編程教我們的是開 interface 達成解耦合,然後使用依賴注入,最後達成依賴反轉目標,隨著函數式編程越來越流行,函數式編程教我們將 closure 當成參數傳進函式,一樣可以解耦合與依賴反轉,尤其對於只使用一次的需求特別有效,不用
    11-22 PHPUnit
  • Phpstorm配置phpunit对php进行单元测试
    Phpstorm配置phpunit对php进行单元测试
    在 phpstorm 中配置 php 项目的单元测试,项目使用 Composer 进行管理,为了避免在项目中直接引入 phpunit 相关代码包,使项目的 vendor 目录变得臃肿,这里采用全局安装方式安装了 phpunit 代码包。 composer global require phpunit/phpunit=5.0.* 安装完成
  • PhpStorm 10 发布,PHP 7 和 PHPUnit 5 支持
    PhpStorm 10 发布,PHP 7 和 PHPUnit 5 支持
    PhpStorm 10 正式版发布 下载 ,此版本最主要的是 PHP 语言支持,编辑体验改进,调试器改进,代码分析改进和其他强大的新特性。 改进列表:PHP 语言和编辑体验:PHP 7支持 (including PHP 7 compatibility inspections),改进代码完成功能 新调试体验:PHP 交
  • PHP单元测试利器 PHPUNIT初探第1/2页
    PHP单元测试利器 PHPUNIT初探第1/2页
    你是否在程序开发的过程中遇到以下的情况:当你花了很长的时间开发一个应用后,你认为应该是大功告成了,可惜在调试的时候,老是不断的发现bug,而且最可怕的是,这些bug是重复出现的,你可能发现这些bug之间会有关联,但却老是找不到问题的所在。
    11-15 PHPUNIT
  • PHP单元测试利器 PHPUNIT深入用法(二)第1/2页
    PHP单元测试利器 PHPUNIT深入用法(二)第1/2页
    在上一篇PHP单元测试利器:PHPUNIT初探文章中,我们对phpunit有了一个初步的认识,在本文中将继续深入讲解下phpunit中的一些用法。
  • PHPUnit PHP测试框架安装方法
    PHPUnit是一个轻量级的PHP测试框架。它是在PHP5下面对JUnit3系列版本的完整移植,是xUnit测试框架家族的一员(它们都基于模式先锋Kent Beck的设计)。
点击排行