首页  >  文章  >  后端开发  >  了解 PHPUnit 测试中的模拟对象

了解 PHPUnit 测试中的模拟对象

DDD
DDD原创
2024-09-22 16:17:02533浏览

Understanding Mock Objects in PHPUnit Testing

编写单元测试时,一个关键挑战是确保您的测试专注于被测代码,而不受外部系统或依赖项的干扰。这就是模拟对象在PHPUnit中发挥作用的地方。它们允许您以受控方式模拟真实对象的行为,使您的测试更可靠且更易于维护。在本文中,我们将探讨什么是模拟对象、它们为何有用以及如何在 PHPUnit 中有效地使用它们。

什么是模拟对象?

模拟对象是在单元测试中使用的真实对象的模拟版本。它们允许您:

  • 隔离被测代码:模拟对象模拟依赖关系的行为,确保测试结果不受这些依赖关系实际实现的影响。
  • 控制依赖行为:您可以指定调用某些方法时模拟应如何表现,使您能够测试不同的场景。
  • 验证交互:模拟跟踪方法调用及其参数,确保被测试的代码与其依赖项正确交互。

为什么使用模拟对象?

模拟在以下场景中特别有用:

  • 复杂的依赖关系:如果您的代码依赖于数据库、API 或第三方服务等外部系统,模拟对象可以通过消除与这些系统交互的需要来简化测试。
  • 交互测试:模拟允许您验证是否使用正确的参数调用特定方法,确保您的代码按预期运行。
  • 更快的测试执行:数据库查询或 API 请求等实际操作可能会减慢测试速度。模拟这些依赖项可确保更快的测试执行。

存根与模拟:有什么区别?

使用模拟对象时,您会遇到两个术语:stubbingmocking:

  • 存根:指在模拟对象上定义方法的行为,例如指示方法返回特定值。
  • 模拟:涉及设置对如何调用方法的期望,例如验证方法调用的数量及其参数。

如何在 PHPUnit 中创建和使用模拟对象

PHPUnit 通过 createMock() 方法可以轻松创建和使用模拟对象。下面是一些示例,演示了如何有效地使用模拟对象。

示例 1:基本模拟对象用法

在此示例中,我们为类依赖项创建一个模拟对象并指定其行为。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testMockExample()
    {
        // Create a mock for the SomeClass dependency
        $mock = $this->createMock(SomeClass::class);

        // Specify that when the someMethod method is called, it returns 'mocked value'
        $mock->method('someMethod')
             ->willReturn('mocked value');

        // Pass the mock object to the class under test
        $unitUnderTest = new ClassUnderTest($mock);

        // Perform the action and assert that the result matches the expected value
        $result = $unitUnderTest->performAction();
        $this->assertEquals('expected result', $result);
    }
}

说明

  • createMock(SomeClass::class) 为 SomeClass 创建一个模拟对象。
  • method('someMethod')->willReturn('mocked value') 定义模拟的行为。
  • 模拟对象被传递给正在测试的类,确保不使用真正的 SomeClass 实现。

示例 2:验证方法调用

有时,您需要验证是否使用正确的参数调用了方法。具体方法如下:

public function testMethodCallVerification()
{
    // Create a mock object
    $mock = $this->createMock(SomeClass::class);

    // Expect the someMethod to be called once with 'expected argument'
    $mock->expects($this->once())
         ->method('someMethod')
         ->with($this->equalTo('expected argument'))
         ->willReturn('mocked value');

    // Pass the mock to the class under test
    $unitUnderTest = new ClassUnderTest($mock);

    // Perform an action that calls the mock's method
    $unitUnderTest->performAction();
}

要点

  • Expects($this->once()) 确保 someMethod 被调用一次。
  • with($this->equalTo('expected argument')) 验证是否使用正确的参数调用该方法。

示例:使用 PaymentProcessor 进行测试

为了演示模拟对象的实际应用,我们以依赖于外部 PaymentGateway 接口的 PaymentProcessor 类为例。我们想要测试 PaymentProcessor 的 processPayment 方法,而不依赖于 PaymentGateway 的实际实现。

这是 PaymentProcessor 类:

class PaymentProcessor
{
    private $gateway;

    public function __construct(PaymentGateway $gateway)
    {
        $this->gateway = $gateway;
    }

    public function processPayment(float $amount): bool
    {
        return $this->gateway->charge($amount);
    }
}

现在,我们可以为 PaymentGateway 创建一个模拟来测试 processPayment 方法,而无需与实际的支付网关交互。

使用模拟对象测试 PaymentProcessor

use PHPUnit\Framework\TestCase;

class PaymentProcessorTest extends TestCase
{
    public function testProcessPayment()
    {
        // Create a mock object for the PaymentGateway interface
        $gatewayMock = $this->createMock(PaymentGateway::class);

        // Define the expected behavior of the mock
        $gatewayMock->method('charge')
                    ->with(100.0)
                    ->willReturn(true);

        // Inject the mock into the PaymentProcessor
        $paymentProcessor = new PaymentProcessor($gatewayMock);

        // Assert that processPayment returns true
        $this->assertTrue($paymentProcessor->processPayment(100.0));
    }
}

测试分解

  • createMock(PaymentGateway::class) creates a mock object simulating the PaymentGateway interface.
  • method('charge')->with(100.0)->willReturn(true) specifies that when the charge method is called with 100.0 as an argument, it should return true.
  • The mock object is passed to the PaymentProcessor class, allowing you to test processPayment without relying on a real payment gateway.

Verifying Interactions

You can also verify that the charge method is called exactly once when processing a payment:

public function testProcessPaymentCallsCharge()
{
    $gatewayMock = $this->createMock(PaymentGateway::class);

    // Expect the charge method to be called once with the argument 100.0
    $gatewayMock->expects($this->once())
                ->method('charge')
                ->with(100.0)
                ->willReturn(true);

    $paymentProcessor = new PaymentProcessor($gatewayMock);
    $paymentProcessor->processPayment(100.0);
}

In this example, expects($this->once()) ensures that the charge method is called exactly once. If the method is not called, or called more than once, the test will fail.

Example: Testing with a Repository

Let’s assume you have a UserService class that depends on a UserRepository to fetch user data. To test UserService in isolation, you can mock the UserRepository.

class UserService
{
    private $repository;

    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    public function getUserName($id)
    {
        $user = $this->repository->find($id);
        return $user->name;
    }
}

To test this class, we can mock the repository:

use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
    public function testGetUserName()
    {
        // Create a mock for the UserRepository
        $mockRepo = $this->createMock(UserRepository::class);

        // Define that the find method should return a user object with a predefined name
        $mockRepo->method('find')
                 ->willReturn((object) ['name' => 'John Doe']);

        // Instantiate the UserService with the mock repository
        $service = new UserService($mockRepo);

        // Assert that the getUserName method returns 'John Doe'
        $this->assertEquals('John Doe', $service->getUserName(1));
    }
}

Best Practices for Using Mocks

  1. Use Mocks Only When Necessary: Mocks are useful for isolating code, but overuse can make tests hard to understand. Only mock dependencies that are necessary for the test.
  2. Focus on Behavior, Not Implementation: Mocks should help test the behavior of your code, not the specific implementation details of dependencies.
  3. Avoid Mocking Too Many Dependencies: If a class requires many mocked dependencies, it might be a sign that the class has too many responsibilities. Refactor if needed.
  4. Verify Interactions Sparingly: Avoid over-verifying method calls unless essential to the test.

Conclusion

Mock objects are invaluable tools for writing unit tests in PHPUnit. They allow you to isolate your code from external dependencies, ensuring that your tests are faster, more reliable, and easier to maintain. Mock objects also help verify interactions between the code under test and its dependencies, ensuring that your code behaves correctly in various scenarios

以上是了解 PHPUnit 测试中的模拟对象的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn