getdkan / mock-chain

一个帮助创建模拟对象链的库。

1.3.7 2024-06-12 17:24 UTC

README

CircleCI Maintainability Test Coverage GPLv3 license

轻松创建复杂的模拟/双重对象。

示例

想象一个类似的方法和对象链

$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的模拟作为序列的第一个元素返回,将字符串作为第二个元素,没有任何问题。

使用选项进行不同的返回模拟

OptionsSequence提供更多的功能,因为它允许我们在决定应该返回什么内容时考虑模拟方法的输入。

$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"