美文网首页php单元测试进阶
php单元测试进阶(13)- 核心技术 - mock对象 - 同

php单元测试进阶(13)- 核心技术 - mock对象 - 同

作者: wanggang999 | 来源:发表于2017-07-24 00:28 被阅读0次

    php单元测试进阶(13)- 核心技术 - mock对象 - 同时使用mock和stub

    本系列文章主要代码与文字来源于《单元测试的艺术》,原作者:Roy Osherove。译者:金迎。

    本系列文章根据php的语法与使用习惯做了改编。所有代码在本机测试通过。如转载请注明出处。
    假设需求变更,更加复杂一些。
    如文件名过短,则web服务记录日志,但万一记录过程中发生异常,需发送一封邮件。
    要求测试发送邮件是成功的。

    源代码有2个接口,一个被测类。
    测试代码有2个伪对象类,一个测试类。

    源代码

    (1)\t2\application\index\controller下,错误日志接口
    IWebService.php

    <?php
    namespace app\index\controller;
    
    /**
     * 记录错误日志的接口,供mock对象和真正的对象实现
     */
    interface IWebService
    {
        /**
         * 记录错误日志
         * @param string $message
         */
        public function logError($message);
    }
    

    (2)\t2\application\index\controller下,邮件接口
    IEmailService.php

    <?php
    namespace app\index\controller;
    
    /**
     * 邮件的接口,供mock对象和真正的对象实现
     */
    interface IEmailService
    {
        /**
         * 发送邮件
         * 
         * @param string $to
         * @param string $subject
         * @param string $body
         */
        public function sendEMail ($to, $subject, $body);
    }
    

    (3)被测类,实现万一抛异常,就发邮件这个功能。\t2\application\index\controller下,
    LogAnalyzer.php

    <?php
    namespace app\index\controller;
    
    /**
     * 日志分析器类,也是被测类
     * 
     * 这是同时使用mock对象和桩件的例子。
     */
    class LogAnalyzer
    {
        /**
         * @var IWebService
         */
        private $service;
        
        /**
         * @var IEmailService
         */
        private $email;
        
        /**
         * 构造方法注入服务
         * @param IWebService $service
         * @param IEmailService $email
         */
        public function __construct(IWebService $service, IEmailService $email)
        {
            $this->service = $service;
            $this->email = $email;
        }
        
        /**
         * 分析日志,省略无关功能,检查文件名过短,记录错误日志,可能发生异常。
         * @param string $filename
         */
        public function analyze($filename)
        {
            if (strlen($filename) < 8 ) {
                try {
                    $this->service->logError("Filename too short:{$filename}");
                } catch ( \Exception $e ) {
                    $this->email->sendEMail("someone@somewhere.com", "can not log", $e->getMessage());
                }
            }
            // 做一些其他的事情。
            // ... ...
        }
    }
    

    测试代码

    (4)\t2\tests\index\controller下,实现错误日志接口的桩件类
    FakeWebService.php

    <?php
    namespace tests\index\controller;
    
    /**
     * 桩件类,要能抛异常,为了测试用
     */
    class FakeWebService implements \app\index\controller\IWebService
    {
        /**
         * @var \Exception
         */
        public $toThrow;
        
        /**
         * 记录错误日志,但是没有伪实现,只是可能抛异常
         * @param string $message 
         */
        public function logError($message)
        {
            // 字段由外部注入,注入就抛异常
            if ($this->toThrow) {
                throw $this->toThrow;
            }
        }
    }
    

    (5)\t2\tests\index\controller下,实现邮件接口的mock类,要断言的
    FakeEmailService.php

    <?php
    namespace tests\index\controller;
    
    /**
     * mock类,要能判断状态。
     */
    class FakeEmailService implements \app\index\controller\IEmailService
    {
        /**
         * @var string
         */
        public $to;
        
        /**
         * @var string
         */
        public $subject;
        
        /**
         * @var string
         */
        public $body;
        
        /**
         * 发送邮件,伪实现
         * 
         * @param string $to
         * @param string $subject
         * @param string $body
         */
        public function sendEMail ($to, $subject, $body)
        {
            $this->to = $to;
            $this->subject = $subject;
            $this->body = $body;
        }
    }
    

    (6)测试类,主要断言了抛异常时,邮件发送成功。\t2\tests\index\controller下,
    LogAnalyzerTest.php

    <?php
    namespace tests\index\controller;
    
    /**
     * 测试用的类
     */
    class LogAnalyzerTest extends \think\testing\TestCase
    {
    
        /**
         * @test
         * 使用桩件模拟web服务,并在其抛异常后 对mock对象断言
         * 注意,尽量使得测试的方法名称有意义,这非常重要,便于维护测试代码。有规律
         */
        public function analyze_WebServiceThrows_SendEmail()
        {
            //创建桩件,并配置使其能抛异常
            $stubService = new FakeWebService();
            $stubService->toThrow = new \Exception("fake exception");
            
            //创建mock对象,好断言
            $mockEmail = new FakeEmailService();
            
            // 创建被测类的对象,注入mock对象和桩件
            $analyzer = new \app\index\controller\LogAnalyzer($stubService, $mockEmail);
            $tooShortFileName= 'abc.ext';
            
            //调用被测对象
            $analyzer->analyze($tooShortFileName);
            
            // 注意是对mock对象断言!!
            $this->assertEquals($mockEmail->to, "someone@somewhere.com");
            $this->assertEquals($mockEmail->subject, "can not log");
            $this->assertEquals($mockEmail->body, "fake exception");
        }
    }
    

    cmd下测试通过。

    总结

    原作者认为:

    1. 一个测试中,应该最多只有一个mock对象,所有其他伪对象都应该是桩件。如有多个mock对象,应分成多个测试,确保每个测试只有一个mock对象。
    2. 一个测试只能断言工作单元三种最终结果中的一种。3种结果是,断言返回值,断言对象或系统状态,断言对象交互。目的要明确。如果有多个不同的测试意图,应分成多个测试。

    相关文章

      网友评论

        本文标题:php单元测试进阶(13)- 核心技术 - mock对象 - 同

        本文链接:https://www.haomeiwen.com/subject/sqdvkxtx.html