getdkan / mock-chain
一个帮助创建模拟对象链的库。
Requires
- php: >=7.4 <9.0
- ext-json: *
- phpunit/phpunit: >=8.3 <8.5 || >8.5.14 <10
Requires (Dev)
- rector/rector: ^0.15.17
- squizlabs/php_codesniffer: ^3.7.2
- symfony/phpunit-bridge: ^7.0
This package is auto-updated.
Last update: 2024-09-12 18:00:28 UTC
README
轻松创建复杂的模拟/双重对象。
示例
想象一个类似的方法和对象链
$body->getSystem('nervous')->getOrgan('brain')->getName();
仅使用phpunit创建body对象的模拟可能看起来像这样
$organ = $this->getMockBuilder(Organ::class) ->disableOriginalConstructor() ->onlyMethods(['getName']) ->getMock(); $organ->method('getName')->willReturn('brain'); $system = $this->getMockBuilder(System::class) ->disableOriginalConstructor() ->onlyMethods(['getOrgan']) ->getMock(); $system->method('getOrgan')->willReturn($organ); $body = $this->getMockBuilder(Body::class) ->disableOriginalConstructor() ->onlyMethods(['getSystem']) ->getMock(); $body->method('getSystem')->willReturn($system);
简单的模拟链实现可能非常冗长。这个库的目的是使这个过程更简单。以下是用mock-chain实现的相同模拟对象
$body = (new Chain($this)) ->add(Body::class, 'getSystem', System::class) ->add(System::class, 'getOrgan', Organ::class) ->add(Organ::class, 'getName', 'brain') ->getMock();
文档
使用这个库可以完成的大部分工作都通过一个类来完成:Chain类。
通过探索这个类公开的少量方法,我们应该能够理解库的全部功能。
模拟一个对象和单个方法
使用mock-chain,我们可以在一行代码中模拟一个对象及其方法。
$mock = (new Chain($this)) ->add(Organ::class, "getName", "heart") ->getMock();
让我们看看这里发生了什么。
(new Chain($this))
在这里,我们调用Chain类的构造函数来创建一个Chain对象。调用构造函数时额外的括号允许我们立即开始调用方法,而不需要保留Chain对象的引用。
Chain类是围绕phpunit提供的模拟功能的一个更好的接口,但所有的模拟功能都来自phpunit。这就是为什么Chain类的构造函数接受PHPUnit\Framework\TestCase对象的原因。
->add(Organ::class, "getName", "heart")
add方法用于通知Chain对象我们希望创建的模拟或模拟的结构。
add的第一个参数是我们想要创建模拟对象的类的完整名称。在我们的例子中,我们想要创建一个Organ对象。
类名是add方法中唯一的必需参数,但通常我们想要模拟对象的某个方法。额外的可选参数允许做到这一点。
第二个参数是Organ类中的方法名称:getName。
第三个参数是我们希望在getName被调用时模拟对象返回的内容。在我们的例子中,我们想要返回字符串"heart"。
最后,
->getMock()
返回Chain类构建的模拟对象。
我们可以在测试中轻松检查我们的模拟对象是否按预期工作。
$this->assertEquals("heart", $mock->getName());
模拟一个对象和多个方法
要模拟多个方法,我们只需多次调用add。
$mock = (new Chain($this)) ->add(Organ::class, "getName", "heart") ->add(Organ::class, "shoutName", "HEART") ->getMock();
Chain假设每个类名都用于生成该类的单个模拟对象。因此,这个链不会创建两个Organ模拟对象,而是一个同时模拟了getName和shoutName的Organ对象。
由于通常需要对单个对象模拟多个方法,Chain类提供了一个方法来简化此操作:addd(带三个D)。
使用addd方法,我们可以将我们的示例简化如下
$mock = (new Chain($this)) ->add(Organ::class, "getName", "heart") ->addd("shoutName", "HEART") ->getMock();
当使用addd时,Chain假设该方法是addd调用之前的最后一个命名的类的模拟。在我们的情况下,它是Organ类。
影响非常微妙,但我们发现,在复杂的模拟中,使用addd
也能提供一个视觉上的间隔,以便更容易地看到被模拟的不同类型的对象。
返回模拟对象
add
方法的第三个参数可以给出任何内容,以供模拟方法返回:字符串、数组、对象、布尔值等。
我们甚至可以返回另一个模拟对象。处理这种情况是本库存在的主要原因,也是为什么它被称为mock-chain
:我们希望能够轻松地定义一系列模拟对象和方法。
为了实现我们的目标,我们只需返回我们想要返回的模拟对象的类名。
$mock = (new Chain($this)) ->add(System::class, "getOrgan", Organ::class) ->add(Organ::class, "getName", "heart") ->addd("shoutName", "HEART") ->getMock();
重要的是要注意,在这个新示例中,由getMock
返回的main
模拟对象是System
类。无论哪个名为第一个的类与Chain
注册,都成为链的root
。其他模拟对象只能通过与root
对象的交互来访问。
还定义了一个名为Organ
的第二个模拟对象,它可以通过模拟的System
对象的getOrgan
方法访问。
有了这个结构,我们可以在模拟对象之间进行断言
$this->assertEquals("heart", $mock->getOrgan("blah")->getName());
使用序列进行不同的返回模拟
通过我们的代码的一些路径,我们可能需要在不同的环境下对同一个模拟对象有不同的响应。使用mock-chain
有多种方法可以完成这个任务,但最简单的方法是使用Sequence
类。
Sequence
允许我们定义每次方法被调用时应该按顺序返回的一组内容。
$organNames = (new Sequence()) ->add("heart") ->add("lungs"); $mock = (new Chain($this)) ->add(Organ::class, "getName", $organNames) ->getMock(); $this->assertEquals("heart", $mock->getName()); $this->assertEquals("lungs", $mock->getName());
在这个例子中,我们创建了一个器官名称的Sequence
,并告诉链,当我们的Organ
模拟的getName
方法被调用时,应该返回这个事物序列。
我们的断言通过显示当第一次调用getName
时返回"heart",当第二次调用getName
时返回"lungs"来确认预期的行为。如果第三次或第四次调用getName
,则再次返回"lungs"。
类似于我们可以从模拟方法返回任何内容,包括其他模拟,我们也可以用序列做同样的事情。
$organs = (new Sequence()) ->add(Organ::class) ->add("lungs"); $mock = (new Chain($this)) ->add(System::class, "getOrgan", $organs) ->add(Organ::class, "getName", "heart") ->getMock(); $this->assertEquals("heart", $mock->getOrgan("blah")->getName()); $this->assertEquals("lungs", $mock->getOrgan("blah"));
在这里,我们将Organ
的模拟作为序列的第一个元素返回,将字符串作为第二个元素,没有任何问题。
使用选项进行不同的返回模拟
Options
比Sequence
提供更多的功能,因为它允许我们在决定应该返回什么内容时考虑模拟方法的输入。
$organs = (new Options()) ->add("heart", Organ::class) ->add("lungs", "yep, the lungs"); $mock = (new Chain($this)) ->add(System::class, "getOrgan", $organs) ->add(Organ::class, "getName", "heart") ->getMock(); $this->assertEquals("yep, the lungs", $mock->getOrgan("lungs")); $this->assertEquals("heart", $mock->getOrgan("heart")->getName());
在这个Options
对象中,我们定义了当输入为"hearts"时调用getOrgan
应该返回我们的Organ
模拟,而当输入为"lungs"时调用getOrgan
应该返回字符串"yep, the lungs"。注意,在断言中,选项的顺序并不重要。
如果我们处理的是更复杂的方法,它们需要多个输入/参数,Options
有两种机制来处理这些场景:索引和JSON字符串。
索引
$organs = (new Options()) ->add("heart",Organ::class) ->add("lung", "yep, the left lung") ->index(0); $mock = (new Chain($this)) ->add(System::class, "getOrganByNameAndIndex", $organs) ->add(Organ::class, "getName", "heart") ->getMock(); $this->assertEquals("yep, the left lung", $mock->getOrganByNameAndIndex("lung", 0)); $this->assertEquals("heart", $mock->getOrganByNameAndIndex("heart", 0)->getName());
在这个例子中,我们有一个更复杂的方法getOrganByNameAndIndex
,它接受两个参数:一个organ name
和一个index
。如果在模拟过程中我们确定我们只关心方法的一个参数,我们可以通过使用Options
类的index
方法来建模。在这个例子中,我们描述说,在确定返回什么时,我们只关心第一个参数,即organ name
。
JSON字符串
$organs = (new Options()) ->add(json_encode(["lung", 0]),"yep, the left lung") ->add(json_encode(["lung", 1]), "yep, the right lung"); $mock = (new Chain($this)) ->add(System::class, "getOrganByNameAndIndex", $organs) ->add(Organ::class, "getName", "heart") ->getMock(); $this->assertEquals("yep, the left lung", $mock->getOrganByNameAndIndex("lung", 0)); $this->assertEquals("yep, the right lung", $mock->getOrganByNameAndIndex("lung", 1));
当我们的方法有多个参数,并且我们在决定返回什么时需要考虑这些参数时,我们总是可以创建一个表示方法输入的数组JSON字符串。
在我们的例子中,当getOrganByNameAndIndex
的输入是"lung"和0时,我们希望返回"yep, the left lung"。但是,如果我们的方法的输入是"lung"和1,我们希望返回"yep, the right lung"。