ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SQL Server Samples 仓库中的 Laravel 示例:Mockery PHP 模拟对象框架使用指南

SQL Server Samples 仓库中的 Laravel 示例:Mockery PHP 模拟对象框架使用指南 SQL Server Samples 仓库中的 Laravel 示例Mockery PHP 模拟对象框架使用指南【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samplesMockery 是一个简洁而灵活的 PHP 模拟对象mock object框架专为 PHPUnit、PHPSpec 等单元测试框架设计。本仓库的 Laravel 示例samples/development-frameworks/laravel在composer.json的require-dev中引入了mockery/mockery0.9.*并将其完整文档随 vendor 目录一并提供docs/index.rst。阅读本文后你将掌握 Mockery 的核心概念、安装方式、Mock 对象创建语法、期望声明expectation与参数匹配、PHPUnit 集成等完整技能可直接在 Laravel 项目的单元测试中落地使用。Mockery 是什么面向单元测试的 PHP 模拟对象框架Mockery 的核心目标是以一套精炼的 API 提供测试替身test double能力用接近人类自然语言的领域特定语言DSL清晰定义对象的操作与交互。它被设计为 PHPUnit 自带phpunit-mock-objects的直接替代品既易于与 PHPUnit 集成也可以与phpunit-mock-objects共存使用互不冲突。在单元测试中模拟对象用于模拟真实对象的行为常见用途包括提供测试隔离让被测代码不依赖真实外部依赖替身那些尚不存在的对象例如依赖尚未开发的接口或服务在不实现具体代码的前提下探索式地设计类 API。模拟对象框架的价值在于能够灵活生成 Mock 对象与桩stub并以可读性极强的 API 设置预期方法调用与返回值尽可能以接近自然语言的方式捕获真实对象的每一种行为。在 Laravel 示例中的实际定位在本仓库的 Laravel 示例composer.json中Mockery 被声明为开发期依赖require-dev: { fzaninotto/faker: ~1.4, mockery/mockery: 0.9.*, phpunit/phpunit: ~4.0, phpspec/phpspec: ~2.1 }可见该项目同时使用 PHPUnit 4.x 与 PHPSpec 2.1 作为测试框架Mockery 作为两者共用的模拟框架被固定为 0.9.x 版本。由于它位于require-dev段生产环境执行composer install --no-dev时不会安装。这也与本文档所在位置吻合——Mockery 完整文档被打包在 vendor/mockery/mockery/docs 目录下分为 Getting Started、Reference、Cookbook 三大部分。安装与引入 Mockery通过 Composer 安装首先为项目安装 Composer然后在composer.json的require-dev中声明依赖。文档建议使用稳定的版本标签如stable也可以直接引用开发分支{ require-dev: { mockery/mockery: dev-master } }随后执行更新命令即可完成安装php composer.phar update安装后的 Mockery 仅作为开发依赖存在生产环境执行php composer.phar update --no-dev时不会被安装。本仓库 Laravel 示例的 composer.json 即采用该模式并锁定为0.9.*版本。通过 PEAR 安装Mockery 托管在独立的 PEAR 频道上可使用如下命令安装会同时安装 Hamcrest 依赖sudo pear channel-discover pear.survivethedeepend.com sudo pear channel-discover hamcrest.googlecode.com/svn/pear sudo pear install --alldeps deepend/Mockery通过 Git 仓库安装Mockery 的 master 分支承载开发版本。可以使用 Composer 引用dev-master版本也可以直接克隆仓库后通过 PEAR 安装。安装过程会同时安装 Mockery 与 Hamcrest——Hamcrest 并非 Mockery 运行的硬性依赖但强烈建议安装因为它为参数匹配argument matching提供更丰富的匹配器matcher能力。引入自动加载Mockery 自带自动加载器autoloader无需在测试中手动写require_once()。使用时只需确保 Mockery 位于include_path中并在测试套件的Bootstrap.php或TestHelper.php中注册require_once Mockery/Loader.php; require_once Hamcrest/Hamcrest.php; $loader new \Mockery\Loader; $loader-register();如果使用 Composer则只需引入 Composer 生成的自动加载文件即可require __DIR__ . /../vendor/autoload.php; // 假设 vendor 位于上一级目录注意Hamcrest 1.0.0 之前Hamcrest.php文件名是小写hamcrest.php。升级到 1.0.0 后请检查所有项目中该文件名是否已更新。快速上手第一个 Mockery 测试假设有一个Temperature类它通过外部服务采样某地区的温度并计算平均值。数据可能来自 Web 服务或其他数据源但当前我们并没有这样的服务类。不过基于Temperature类对服务的调用方式可以推断出服务类应具备的基本交互class Temperature { public function __construct($service) { $this-_service $service; } public function average() { $total 0; for ($i0;$i3;$i) { $total $this-_service-readTemp(); } return $total/3; } }即便没有真实的服务类也能清楚看到Temperature对它的预期readTemp()方法将被调用 3 次。编写测试时可以用 Mock 对象替代真实服务从而在无需具体服务实例的情况下验证Temperature的行为use \Mockery as m; class TemperatureTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testGetsAverageTemperatureFromThreeServiceReadings() { $service m::mock(service); $service-shouldReceive(readTemp)-times(3)-andReturn(10, 12, 14); $temperature new Temperature($service); $this-assertEquals(12, $temperature-average()); } }这段测试的核心逻辑是通过shouldReceive(readTemp)-times(3)-andReturn(10, 12, 14)声明期望——readTemp必须恰好被调用 3 次依次返回 10、12、14。Temperature::average()对 3 次读数求和后取平均得到 12与断言一致。tearDown()中的m::close()负责清理当前测试的 Mockery 容器并执行期望验证若使用 PHPUnit 集成见后文则可以省略该tearDown()。创建 Mock 对象的完整语法\Mockery::mock()的第一个参数决定 Mock 的类型与行为第二个参数可以是期望数组或期望闭包配合期望数组时可为第三参数下面是文档中列举的全部创建方式。命名 Mock最宽松的形式$mock \Mockery::mock(foo);创建一个名为 foo 的 Mock 对象。这里的 foo 是名称不一定是类名仅作为抛出异常时的简单标识。它生成\Mockery\Mock类型的对象是所有 Mock 中约束最宽松的形式。通过期望数组快速设置$mock \Mockery::mock(array(foo1,bar2));未指定名称但传入了期望数组——这是快速声明期望哪些方法、返回什么值的简便方式。期望数组可以作为第二个参数传给所有形式的mock()$mock \Mockery::mock(foo, array(foo1,bar2));通过闭包设置可复用期望$mock \Mockery::mock(foo, function($mock) { $mock-shouldReceive(method_name); });除了期望数组还可以传入包含可复用期望的闭包作为第二个参数若与期望数组并用则作为第三个参数。这是实现可复用 Mock 期望的方式之一。基于具体类创建 Mock$mock \Mockery::mock(stdClass);当名称是真实类名时Mock 对象将通过继承获得该类类型可通过类型提示type hint或instanceof校验适合要求 Mock 必须是特定类型的场景。基于接口、抽象类创建 Mock$mock \Mockery::mock(FooInterface);任何具体类、抽象类甚至接口都可以作为 Mock 基础主要目的同样是让 Mock 继承特定类型以通过类型提示。例外是被标记为final的类或含final方法的类无法被完整 Mock此时必须使用部分 Mockpartial mock。别名 MockAlias Mock模拟静态方法$mock \Mockery::mock(alias:MyNamespace\MyClass);给一个当前尚未加载的类名加上alias:前缀会生成别名 Mock为该类名创建指向stdClass的类别名通常用于模拟公共静态方法。设置在 Mock 对象上、针对静态方法的期望将作用于该类的所有静态调用。实例 MockInstance Mock / overload$mock \Mockery::mock(overload:MyNamespace\MyClass);overload:前缀与alias:类似但区别在于之后new出来的该类实例会导入在源 Mock$mock上设置的期望。源 Mock 本身永远不会被验证它仅作为新实例的期望存储故称为实例 Mockinstance mock以区别于更简单的别名 Mock。警告在同一测试中多次使用 alias/instance Mock 会因无法定义同名类而产生致命错误。解决方法是让这类测试各自运行在独立的 PHP 进程中PHPUnit 与 PHPT 均原生支持。同时实现多个接口$mock \Mockery::mock(stdClass, MyInterface1, MyInterface2);第一个参数也可以是用逗号分隔的接口列表其中最多可包含一个现有类作为基础。类名不要求位于列表首位但按惯例放在开头更易读。若给定的类不存在则必须事先定义并包含它否则会抛出\Mockery\Exception。传统部分 MockPartial Mock$mock \Mockery::mock(MyNamespace\MyClass[foo,bar]);方括号语法告诉 Mockery 对MyNamespace\MyClass进行部分 Mock仅模拟foo()与bar()两个方法其余方法不被 Mockery 覆盖。这种传统形式适用于任何类或抽象类例如模拟尚无具体实现的抽象方法。如果尝试部分 Mock 一个final方法该实例中会被忽略final 方法保持原样——因为按 PHP 定义final 方法本就无法被 Mock。传递构造函数参数$mock \Mockery::mock(MyNamespace\MyClass[foo], array($arg1, $arg2));当 Mockery 遇到索引数组作为第二或第三参数时会将其视为构造函数参数在构造 Mock 对象时传入。上面的语法创建了一个新的部分 Mock特别适合bar方法内部通过$this-foo()调用foo的场景。代理部分 MockProxied Partial Mock$mock \Mockery::mock(new Foo);向 Mockery 传入任意真实对象会创建代理部分 Mock。当真实部分 Mock 不可能实现时例如 final 类或必须覆盖 final 方法此方式非常有用既然已经能创建具体对象只需选择性覆盖现有方法的子集甚至可以添加不存在的非现有方法来满足期望。复习参数约定所有 Mock 方法都以要 Mock 的类、对象或别名作为第一个参数第二个参数可以是方法到返回值的期望数组或期望闭包与期望数组并用时闭包为第三参数。\Mockery::self()链式返回自身\Mockery::self()当期望中的方法需要返回同一个 Mock 对象时例如设计 DSL 时——Mockery 自身就是这样的例子调用\Mockery::self()会返回最近一次通过\Mockery::mock()创建的 Mock 对象$mock \Mockery::mock(BazIterator) -shouldReceive(next) -andReturn(\Mockery::self()) -mock();以迭代器为例很多场景下可以把所有迭代元素往往类型相同替换为同一个 Mock 对象让它轮流扮演各个离散元素。namedMock按自定义名称生成类$mock \Mockery::namedMock(MyClassName, DateTime);namedMock会生成一个以第一个参数为类名的类如MyClassName其余参数处理方式与mock相同。例如上述调用创建名为MyClassName、继承DateTime的类。命名 Mock 属于边缘场景但当代码依赖__CLASS__魔术常量或需要某个抽象类型的两个不同派生类时它很有用。警告命名 Mock 只能创建一次后续以不同参数再次调用namedMock很可能引发异常。行为修饰符改变 Mock 的默认行为shouldIgnoreMissing()被动 Mock\Mockery::mock(MyClass)-shouldIgnoreMissing()该修饰符将 Mock 标记为被动 MockPassive Mock调用未被期望覆盖的方法时返回null而不是通常那样抱怨没有匹配该调用的期望。若希望恢复 0.7.2 时代的行为——返回\Mockery\Undefined类型的对象即空对象可叠加\Mockery::mock(MyClass)-shouldIgnoreMissing()-asUndefined()返回的空对象只是一个占位符如果被误用在不该出现的地方通常无法通过逻辑检查。makePartial() / shouldDeferMissing()被动部分 Mock\Mockery::mock(MyClass)-makePartial()\Mockery::mock(MyClass)-shouldDeferMissing()这两种写法等价称为被动部分 MockPassive Partial Mock区别于后文讨论的真实部分 Mock 对象。它对未受期望约束的所有方法会委托给 Mock 的父类即MyClass执行。与前面shouldIgnoreMissing()返回null不同此行为会直接调用父类的对应方法。期望声明Expectation Declarations定义 Mock 的行为与调用方式创建 Mock 对象后下一步就是通过期望声明定义它的行为与调用约束。声明期望的方法shouldReceive(method_name)声明 Mock 期望收到对指定方法的调用这是所有后续期望与约束的起点。shouldReceive(method1, method2, ...)一次声明多个期望方法所有方法共享后续链式期望或约束。shouldReceive(array(method11, method22, ...))声明多个期望调用并同时指定返回值的数组形式。shouldReceive(closure)仅从部分 Mock 创建生成Mock 对象记录器recorder它是原始对象的简单代理闭包可对其执行一系列操作这些操作被记录为部分 Mock 上的期望。典型场景是在重构时基于已有用法自动录制期望。shouldNotReceive(method_name)声明 Mock 不应收到对指定方法的调用等价于shouldReceive()-never()。参数约束with(arg1, arg2, ...) / withArgs(array(arg1, arg2, ...))为该期望增加参数列表必须匹配的约束。可以借助内置匹配器大幅增强灵活性例如\Mockery::any()匹配该位置的任意参数Mockery 也允许使用 Hamcrest 匹配器如 Hamcrest 的anything()等价于\Mockery::any()。这意味着同一方法可依据不同参数设置不同期望——期望只在参数完全匹配时才生效。withAnyArgs()声明该期望匹配任何参数的方法调用未特别指定时是默认行为。withNoArgs()声明该期望只匹配零参数的方法调用。返回值设置andReturn(value)设置期望方法调用的返回值。andReturn(value1, value2, ...)设置返回值序列或闭包序列第一次调用返回 value1第二次返回 value2。注意之后对该 Mock 方法的所有调用都始终返回最后或唯一给定的值。andReturnNull() / andReturn([NULL])两种写法主要是向测试读者传达该方法返回 null 或什么都不返回。andReturnValues(array)andReturn()的替代语法接受数组而非参数列表。返回顺序由数组数字索引决定先前返回值耗尽后后续调用返回最后一个数组成员。andReturnUsing(closure, ...)设置一个闭包用传入方法的参数调用它并将闭包返回值作为结果返回。适合对参数做动态加工后得到相关结果闭包可通过额外参数排队用法同andReturn()。注意当前不能混用andReturnUsing()与andReturn()。andThrow(Exception)声明该方法被调用时抛出给定的Exception对象。andThrow(exception_name, message)也可以不传对象而传异常类名与消息由 Mock 方法抛出新异常。andSet(name, value1) / set(name, value1)配合期望使用当匹配的方法被调用时同时将 Mock 对象的公共属性设置为指定值。passthru()让期望绕过返回值队列转而调用被 Mock 类的真实方法并返回结果。本质上它允许对真实方法施加期望匹配与调用次数验证同时仍然以期望的参数调用真实类方法。调用次数约束zeroOrMoreTimes()声明期望方法可被调用零次或多次所有方法未另行指定时的默认行为。once()声明方法只能被调用一次。与所有调用次数约束一样违反时会抛出\Mockery\CountValidator\Exception并可被atLeast()与atMost()修改。twice()声明方法只能被调用两次。times(n)声明方法只能被调用 n 次。never()声明方法绝不能被调用——永远atLeast()为下一个调用次数期望添加最小值修饰atLeast()-times(3)表示调用必须至少 3 次。atMost()添加最大值修饰atMost()-times(3)表示调用不得超过 3 次也允许零次调用。between(min, max)设置调用次数的区间等价于atLeast()-times(min)-atMost()-times(max)的简写可跟一个无参的times()以保持 API 的自然语言可读性。调用顺序约束ordered()声明该方法必须在与同样标记的方法之间按特定顺序被调用顺序由设置期望时使用该修饰符的顺序决定。ordered(group)将方法声明为某个顺序组可命名或编号的成员。组内方法可任意顺序调用但组外有序调用相对该组是有序的——例如可设置 method1 在 group1 之前、group1 在 method2 之前。globally()在ordered()或ordered(group)之前调用声明该顺序约束作用于所有 Mock 对象而非仅当前 Mock从而跨多个 Mock 规定调用顺序。默认期望与链式取回byDefault()将期望标记为默认期望。默认期望在未创建非默认期望时生效后创建的非默认期望会立即替换先前定义的默认值。这样可以在setup()中设置默认 Mock再在具体测试中按需微调。getMock()从期望链中返回当前 Mock 对象便于将 Mock 设置保持为单条语句$mock \Mockery::mock(foo)-shouldReceive(foo)-andReturn(1)-getMock();参数匹配与验证Argument Validationwith()中传入的参数决定了调用如何匹配期望。可以为同一方法设置多个期望各自按参数区分。匹配采用最佳匹配best fit原则显式匹配优先于泛化匹配。显式匹配指期望参数与实际参数容易判定相等或更泛化的匹配可通过正则表达式、类提示class hinting与内置匹配器实现。Mockery 内置匹配器并未覆盖所有可能但提供对 Hamcrest 匹配器库的可选支持Hamcrest 是 Java 同名库的 PHP 移植版同时也有 Python、Erlang 等版本。由于 Hamcrest 已提供出色的自然语言 DSL 工具集Mockery 没有必要重复造轮子。内置匹配器一览with(1)匹配整数1通过全等判定也允许较宽松的等于判定此时字符串1也能匹配该参数。with(\Mockery::any()) OR with(anything())匹配任意参数anything()为 Hamcrest 等价写法。with(\Mockery::type(resource)) OR with(resourceValue()) OR with(typeOf(resource))匹配任意资源类型即is_resource()返回 true。Type 匹配器接受任意可拼成is_前缀的有效类型检查字符串例如\Mockery::type(float)对应is_float()检查\Mockery::type(callable)对应is_callable()检查。Type 匹配器也接受类名或接口名用于对实际参数做instanceof判定Hamcrest 对应anInstanceOf()。with(\Mockery::on(closure))On 匹配器接受一个闭包实际参数会被传入其中闭包返回布尔true即视为匹配。当参数期望过于复杂、超出默认匹配器能力时此方法价值极大。Hamcrest 无对应实现。with(/^foo/) OR with(matchesPattern(/^foo/))参数声明器也假定任意字符串可能是正则表达式。正则匹配仅在两种情况同时满足时启用a) 不存在或匹配b) 该字符串被验证为合法正则即preg_match()不返回 false。若不喜欢隐式正则检测Hamcrest 提供更显式的matchesPattern()函数。with(\Mockery::ducktype(foo, bar))Ducktype 匹配器是类类型匹配的替代方案匹配任何包含给定方法列表的对象即该对象可调用foo、bar方法。Hamcrest 无对应实现。with(\Mockery::mustBe(2)) OR with(identicalTo(2))MustBe 匹配器比默认匹配器更严格默认匹配器允许 PHP 类型转换字符串2可匹配整数 2而 MustBe 要求参数与期望值类型相同——同一场景下2字符串匹配整数 2 会被 MustBe 判定失败。注意该匹配器对对象不做全等比较因为 PHP 要求两个对象为同一实例才全等当对象在返回前才生成时全等匹配几乎不可能成立。with(\Mockery::not(2)) OR with(not(2))Not 匹配器匹配任何不等于或不全等于其参数的值。with(\Mockery::anyOf(1, 2)) OR with(anyOf(1,2))匹配等于给定参数中任意一个的值。with(\Mockery::notAnyOf(1, 2))匹配不等于也不全等于给定参数中任何一个的值。Hamcrest 无对应实现。with(\Mockery::subset(array(0 foo)))匹配任何包含给定数组子集的数组同时比较键名与键值。Hamcrest 无对应实现但它可以用hasEntry()或hasKeyValuePair()检查单条条目。with(\Mockery::contains(value1, value2))匹配任何包含所列值的数组忽略键名。with(\Mockery::hasKey(key));匹配任何包含给定键名的数组。with(\Mockery::hasValue(value));匹配任何包含给定值的数组。与 PHPUnit 集成Mockery 是设计为简单易用的独立模拟框架与测试框架的集成完全可选。最基本的集成方式是在测试中定义tearDown()并调用public function tearDown() { \Mockery::close(); }该静态调用会清理当前测试使用的 Mockery 容器并执行期望所需的验证任务。为书写简便可用更短的别名use \Mockery as m; class SimpleTest extends PHPUnit_Framework_TestCase { public function testSimpleMock() { $mock m::mock(simplemock); $mock-shouldReceive(foo)-with(5, m::any())-once()-andReturn(10); $this-assertEquals(10, $mock-foo(5)); } public function tearDown() { m::close(); } }自动清理TestListener若要避免手动调用 close 方法并让 Mockery 从代码覆盖率报告中移除自身可在测试套件中注册 Mockery 的 PHPUnit 监听器// 创建套件 $suite new PHPUnit_Framework_TestSuite(); // 创建结果监听器并添加 $result new PHPUnit_Framework_TestResult(); $result-addListener(new \Mockery\Adapter\Phpunit\TestListener()); // 运行测试 $suite-run($result);若使用 PHPUnit 的 XML 配置方式可在配置中加入listeners listener class\Mockery\Adapter\Phpunit\TestListener/listener /listeners务必确保 Composer 或 Mockery 的自动加载器已出现在 bootstrap 文件中否则还需为该TestListener类定义file属性指向其所在文件。进程隔离的注意事项PHPUnit 支持让测试在独立进程中运行以获得更好的隔离性。Mockery 通过Mockery::close()验证期望其 PHPUnit 监听器会在每个测试后自动调用该方法。但使用进程隔离时该监听器不会在正确的进程中执行导致期望可能未被验证且不会抛出任何Mockery\Exception。因此在使用进程隔离时不能依赖 Mockery 自带的 PHPUnitTestListener必须在tearDown()中显式调用Mockery::close()即本文前述最基础的方式。升级注意事项文档记录了 0.8 与 0.9 两个版本的关键行为变更如果你在 Laravel 示例中升级mockery/mockery版本当前锁定 0.9.*需要注意升级到 0.9Mockery 的生成器generator被完全重写任何与 Mockery 深度集成的代码都需要重新评估。升级到 0.8shouldIgnoreMissing()行为变化——0.7.2 及更早版本中未匹配已知期望的方法调用返回\Mockery\Undefined实例自 0.8.0 起改为返回null。可显式恢复旧行为$mock \Mockery::mock(stdClass)-shouldIgnoreMissing()-asUndefined();深入阅读更多参考文档与 Cookbook本仓库随包提供了完整的 Mockery 文档集docs除本文覆盖的内容外还可按需查阅快速参考reference/startup_methods.rstMock 创建与行为修饰符全解期望声明reference/expectations.rst参数匹配reference/argument_validation.rstPHPUnit 集成reference/phpunit_integration.rst部分 Mockreference/partial_mocks.rst公共属性 / 静态属性reference/public_properties.rst、reference/public_static_properties.rst引用传递行为reference/pass_by_reference_behaviours.rstDemeter 链reference/demeter_chains.rst对象录制reference/object_recording.rstfinal 类与 final 方法reference/final_methods_classes.rst魔术方法reference/magic_methods.rst配置 / 异常 / 保留方法名 / 易错点reference/mockery/configuration.rst、reference/mockery/exceptions.rst、reference/mockery/reserved_method_names.rst、reference/mockery/gotchas.rstCookbook 技巧cookbook/index.rst含默认期望、Mock 对象检测、硬依赖 Mock 等实战技巧见 cookbook 目录在 Laravel 项目的单元测试实践中建议以 PHPUnit 的tearDown()显式调用Mockery::close()作为稳健基线再按需引入 TestListener 简化写法使用shouldReceive系列声明期望、配合参数匹配器精确控制调用约束即可获得与自然语言高度一致、可读且可维护的测试代码。【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表