哈喽,各位观众老爷们,今天咱们来聊聊PHPUnit里几个好玩又实用的小伙伴:Data Providers和Test Doubles。别怕,虽然名字听起来有点高大上,但其实都是能帮你写出更健壮、更易于维护的测试代码的利器。
Data Providers:让你的测试像机关枪一样扫射各种数据
想象一下,你要测试一个函数,这个函数的功能是判断一个数是不是偶数。你可能会写出这样的测试:
<?php
use PHPUnitFrameworkTestCase;
class EvenNumberTest extends TestCase
{
public function testIsEvenWithEvenNumber()
{
$this->assertTrue(isEven(2));
}
public function testIsEvenWithOddNumber()
{
$this->assertFalse(isEven(3));
}
public function testIsEvenWithZero()
{
$this->assertTrue(isEven(0));
}
}
function isEven(int $number): bool
{
return $number % 2 === 0;
}
这看起来没什么问题,但如果我们要测试更多的偶数和奇数呢?难道要复制粘贴一堆类似的测试用例?太Low了!这时候,Data Providers就派上用场了。
Data Providers允许你提供一组数据,然后PHPUnit会用这组数据多次运行同一个测试方法。就像机关枪一样,哒哒哒哒地扫射各种数据,确保你的函数在各种情况下都能正常工作。
让我们用Data Provider重写上面的测试:
<?php
use PHPUnitFrameworkTestCase;
class EvenNumberTest extends TestCase
{
/**
* @dataProvider evenNumberProvider
*/
public function testIsEven(int $number, bool $expected)
{
$this->assertEquals($expected, isEven($number));
}
public function evenNumberProvider(): array
{
return [
[2, true],
[3, false],
[0, true],
[10, true],
[11, false],
[-2, true],
[-3, false],
];
}
}
function isEven(int $number): bool
{
return $number % 2 === 0;
}
在这个例子中,evenNumberProvider
方法就是一个Data Provider。它返回一个二维数组,每一行代表一组测试数据。第一列是输入数据 $number
,第二列是期望的输出 $expected
。
@dataProvider evenNumberProvider
注解告诉PHPUnit,testIsEven
方法要使用 evenNumberProvider
提供的数据。
现在,PHPUnit会运行 testIsEven
方法七次,每次使用 evenNumberProvider
返回的一组数据。这样我们就用更少的代码,测试了更多的场景。
Data Provider 的一些小技巧:
- Data Provider 方法必须是
public
的。 - Data Provider 方法必须返回一个数组,这个数组可以是一维的,也可以是二维的。如果是二维的,每一行代表一组测试数据。
- 可以在一个测试类中使用多个 Data Provider。只需要在
@dataProvider
注解中指定不同的 Data Provider 方法名即可。 - Data Provider 可以返回 Generator 对象,这在处理大量数据时非常有用。
Test Doubles:模拟依赖,隔离测试
在单元测试中,我们希望只测试被测单元本身的功能,而不要受到它所依赖的其他组件的影响。如果被测单元依赖于一个外部服务,例如数据库或者第三方API,那么测试就会变得很复杂,而且可能会受到外部环境的影响。
Test Doubles就是用来解决这个问题的。Test Doubles 是一种模拟对象,它可以模拟被测单元所依赖的其他组件的行为,从而隔离测试环境,让我们可以专注于测试被测单元本身的功能。
PHPUnit提供了四种类型的Test Doubles:
- Dummy: 传递给被测单元,但并不真正使用。
- Stub: 提供预定义的返回值,模拟被测单元所依赖的组件的行为。
- Mock: 用于验证被测单元是否以正确的方式调用了它所依赖的组件。
- Spy: 记录被测单元对它所依赖的组件的调用,用于事后分析。
接下来,我们来详细介绍这四种类型的Test Doubles。
1. Dummy:假装存在的占位符
Dummy 对象就像一个假人,它只是用来占位的,并不会真正被使用。通常用于满足被测单元的接口要求,但我们并不关心它实际的行为。
例如,假设我们有一个 UserController
类,它的构造函数需要一个 LoggerInterface
对象,但我们在测试 UserController
的其他功能时,并不需要用到 LoggerInterface
。这时,我们可以使用 Dummy 对象来满足 UserController
的构造函数的要求。
<?php
use PHPUnitFrameworkTestCase;
use PsrLogLoggerInterface;
use AppUserController;
class UserControllerTest extends TestCase
{
public function testIndex()
{
// 创建一个 Dummy Logger 对象
$logger = $this->createMock(LoggerInterface::class);
// 创建 UserController 对象,传入 Dummy Logger
$userController = new UserController($logger);
// 调用 index 方法
$response = $userController->index();
// 断言 response 是否正确
$this->assertEquals('Index page', $response);
}
}
namespace App;
use PsrLogLoggerInterface;
class UserController
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function index(): string
{
return 'Index page';
}
}
在这个例子中,我们使用 createMock(LoggerInterface::class)
创建了一个 Dummy Logger 对象。这个对象实现了 LoggerInterface
接口,但它的所有方法都是空的,不会做任何事情。
2. Stub:提供预定义行为的演员
Stub 对象就像一个演员,它可以按照我们的剧本,提供预定义的返回值,模拟被测单元所依赖的组件的行为。
例如,假设我们有一个 OrderService
类,它依赖于一个 PaymentGatewayInterface
来处理支付。在测试 OrderService
的 processOrder
方法时,我们可以使用 Stub 对象来模拟 PaymentGatewayInterface
的行为,例如模拟支付成功或者支付失败。
<?php
use PHPUnitFrameworkTestCase;
use AppOrderService;
use AppPaymentGatewayInterface;
class OrderServiceTest extends TestCase
{
public function testProcessOrderSuccess()
{
// 创建一个 PaymentGateway Stub 对象
$paymentGateway = $this->createStub(PaymentGatewayInterface::class);
// 设置 Stub 对象的行为:processPayment 方法总是返回 true (支付成功)
$paymentGateway->method('processPayment')
->willReturn(true);
// 创建 OrderService 对象,传入 PaymentGateway Stub
$orderService = new OrderService($paymentGateway);
// 调用 processOrder 方法
$result = $orderService->processOrder(100);
// 断言 result 是否为 true (订单处理成功)
$this->assertTrue($result);
}
public function testProcessOrderFailure()
{
// 创建一个 PaymentGateway Stub 对象
$paymentGateway = $this->createStub(PaymentGatewayInterface::class);
// 设置 Stub 对象的行为:processPayment 方法总是返回 false (支付失败)
$paymentGateway->method('processPayment')
->willReturn(false);
// 创建 OrderService 对象,传入 PaymentGateway Stub
$orderService = new OrderService($paymentGateway);
// 调用 processOrder 方法
$result = $orderService->processOrder(100);
// 断言 result 是否为 false (订单处理失败)
$this->assertFalse($result);
}
}
namespace App;
interface PaymentGatewayInterface
{
public function processPayment(int $amount): bool;
}
class OrderService
{
private $paymentGateway;
public function __construct(PaymentGatewayInterface $paymentGateway)
{
$this->paymentGateway = $paymentGateway;
}
public function processOrder(int $amount): bool
{
return $this->paymentGateway->processPayment($amount);
}
}
在这个例子中,我们使用 createStub(PaymentGatewayInterface::class)
创建了一个 PaymentGateway Stub 对象。然后,我们使用 method('processPayment')->willReturn(true)
设置了 Stub 对象的行为:当 processPayment
方法被调用时,总是返回 true
。
3. Mock:验证交互的监视器
Mock 对象就像一个监视器,它可以验证被测单元是否以正确的方式调用了它所依赖的组件。Mock 对象可以验证方法是否被调用、被调用了多少次、以及传递的参数是否正确。
例如,假设我们有一个 ArticleService
类,它依赖于一个 NotificationService
来发送文章发布的通知。在测试 ArticleService
的 publishArticle
方法时,我们可以使用 Mock 对象来验证 NotificationService
的 sendNotification
方法是否被调用,以及传递的参数是否正确。
<?php
use PHPUnitFrameworkTestCase;
use AppArticleService;
use AppNotificationService;
class ArticleServiceTest extends TestCase
{
public function testPublishArticle()
{
// 创建一个 NotificationService Mock 对象
$notificationService = $this->createMock(NotificationService::class);
// 设置 Mock 对象的期望:sendNotification 方法应该被调用一次,并且传递的参数为 'Article published'
$notificationService->expects($this->once())
->method('sendNotification')
->with($this->equalTo('Article published'));
// 创建 ArticleService 对象,传入 NotificationService Mock
$articleService = new ArticleService($notificationService);
// 调用 publishArticle 方法
$articleService->publishArticle('My Article');
}
}
namespace App;
class NotificationService
{
public function sendNotification(string $message): void
{
// Send notification logic
}
}
class ArticleService
{
private $notificationService;
public function __construct(NotificationService $notificationService)
{
$this->notificationService = $notificationService;
}
public function publishArticle(string $title): void
{
// Publish article logic
$this->notificationService->sendNotification('Article published');
}
}
在这个例子中,我们使用 createMock(NotificationService::class)
创建了一个 NotificationService Mock 对象。然后,我们使用 expects($this->once())->method('sendNotification')->with($this->equalTo('Article published'))
设置了 Mock 对象的期望:sendNotification
方法应该被调用一次,并且传递的参数为 'Article published'
。
如果 ArticleService
的 publishArticle
方法没有调用 NotificationService
的 sendNotification
方法,或者调用了但传递的参数不正确,那么测试就会失败。
4. Spy:记录行为的间谍
Spy 对象就像一个间谍,它可以记录被测单元对它所依赖的组件的调用,用于事后分析。Spy 对象可以记录方法是否被调用、被调用了多少次、以及传递的参数。
Spy 和 Mock 的区别在于,Mock 用于验证行为,而 Spy 用于记录行为。Mock 会在测试过程中强制执行期望,而 Spy 只是记录行为,不会强制执行任何期望。
例如,假设我们有一个 UserService
类,它依赖于一个 UserRepository
来保存用户信息。在测试 UserService
的 createUser
方法时,我们可以使用 Spy 对象来记录 UserRepository
的 save
方法是否被调用,以及传递的参数。
<?php
use PHPUnitFrameworkTestCase;
use AppUserService;
use AppUserRepository;
use AppUser;
class UserServiceTest extends TestCase
{
public function testCreateUser()
{
// 创建一个 UserRepository Spy 对象
$userRepository = $this->createMock(UserRepository::class);
// 设置 Spy 对象的行为:当 save 方法被调用时,记录调用信息
$userRepository->expects($this->once())
->method('save')
->with($this->isInstanceOf(User::class));
// 创建 UserService 对象,传入 UserRepository Spy
$userService = new UserService($userRepository);
// 调用 createUser 方法
$userService->createUser('John Doe', '[email protected]');
}
}
namespace App;
class UserRepository
{
public function save(User $user): void
{
// Save user to database
}
}
class UserService
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function createUser(string $name, string $email): void
{
$user = new User($name, $email);
$this->userRepository->save($user);
}
}
class User
{
private $name;
private $email;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
}
}
在这个例子中,我们使用 createMock(UserRepository::class)
创建了一个 UserRepository Spy 对象。然后,我们使用 expects($this->once())->method('save')->with($this->isInstanceOf(User::class))
设置了 Spy 对象的期望:save
方法应该被调用一次,并且传递的参数是 User
类的一个实例。
Test Doubles 选择指南:
Test Double Type | 目的 | 使用场景 |
---|---|---|
Dummy | 满足接口要求,占位 | 当被测单元需要依赖对象,但不需要使用该对象的功能时。例如,构造函数需要一个 LoggerInterface 对象,但测试过程中不需要用到 Logger。 |
Stub | 提供预定义的返回值 | 当需要模拟被测单元所依赖的组件的行为,例如模拟支付成功或失败,模拟数据库查询结果。通常用于隔离外部依赖,使测试更加可控。 |
Mock | 验证交互,强制执行期望 | 当需要验证被测单元是否以正确的方式调用了它所依赖的组件,例如验证方法是否被调用、被调用了多少次、以及传递的参数是否正确。通常用于测试单元之间的协作,确保它们按照预期的方式进行交互。 |
Spy | 记录行为,事后分析 | 当需要记录被测单元对它所依赖的组件的调用,例如记录方法是否被调用、被调用了多少次、以及传递的参数。通常用于事后分析,例如分析性能瓶颈,或者调试复杂的交互过程。与 Mock 不同,Spy 不会强制执行任何期望,只是记录行为。 |
总结:
Data Providers 和 Test Doubles 都是PHPUnit中非常有用的工具。Data Providers可以让你用更少的代码,测试更多的场景。Test Doubles可以让你隔离测试环境,专注于测试被测单元本身的功能。
熟练掌握这些工具,可以帮助你写出更健壮、更易于维护的测试代码,提高软件质量。
好了,今天的讲座就到这里。希望大家有所收获,下次再见!