我经常听到开发人员对他们为什么不编写测试的不同论点是合理的。
其中最受欢迎的是:
- 我确定我的代码是没有错误的
- 写作测试很困难
- 企业不想花钱在写作测试上
- 我不明白如何写它们
- 这可以是您的选择
只是忘记它!测试是代码的强制性部分,也是您作为软件工程师的责任。
在本文中,我想描述我们如何在金融科技启动中测试我们的代码,这使我们能够确保任何回归对我们安全的信心。
让我们从一些基本概念和原理开始
为什么需要单位测试?
-
测试有助于使您的生产更稳定
- 回归测试
- 合同修复
-
测试提高了开发的可用性
- 更快的反馈
- 更简单的重构
-
测试组织不良代码
- 让您考虑测试用例
- 让您考虑代码质量
质量单位测试是什么样的?
- 易于理解
- 错误失败
- 抵抗重构
- 快速
- 环境独立
正常测试是什么样的?
- 很难理解
- bugs不会失败
- 任何重构都迫使您修复测试
- 慢
- 环境依赖
那么您如何编写高质量的单位测试?
使用合同。单位合同是单位对您的期望,也是您对该单位的期望。因此,如果您想通过单位测试进行测试,则必须合同。
您需要测试的合同条款是:
- 有用的工作(例如数据库呼叫或其他第三方服务)
- 预期结果
- 在边界条件上结果
- 例外情况
尝试测试您的班级合同,而不是其实施。
您单位测试的功能必须干净,否则单位测试将测试您的功能的实现。
干净的函数又称确定性函数 - 始终给出输入(x)的相同输出(y)。非确定功能会产生可变输出。确定性意味着随机性的相反,每次都给出相同的结果,而不会以任何方式与环境互动,例如:
Input: [1, 2, 3, 4] > array_reverse > Output: [4, 3, 2, 1]
这是一个很好的单位测试的示例,该单位测试测试了一个干净的功能:
final class ItemGrouperTest extends TestCase
{
public function testGroupReturnsGroupedItemsGroupedByNameAndCurrency(): void
{
$itemGrouper = new ItemGrouper();
$items = [
$item1 = new Item(1, 'Nike AF 1', 'GBP', 13000),
$item2 = new Item(1, 'Nike AF 2', 'GBP', 13000),
$item3 = new Item(1, 'Nike AF 3', 'EUR', 14000),
$item4 = new Item(2, 'Nike AF 1', 'GBP', 13000),
];
$groupedItems = $itemGrouper->group(
ItemGrouper::GROUP_BY_NAME & ItemGrouper::GROUP_BY_CURRENCY,
...$items
);
$this->assertCount(3, $groupedItems);
$this->assertCount(2, $groupedItems[0]->getItems());
$this->assertContains($item1, $groupedItems[0]->getItems());
$this->assertContains($item2, $groupedItems[0]->getItems());
$this->assertCount(1, $groupedItems[1]->getItems());
$this->assertContains($item3, $groupedItems[1]->getItems());
$this->assertCount(1, $groupedItems[2]->getItems());
$this->assertContains($item4, $groupedItems[2]->getItems());
}
}
我们知道方法的行为,提供输入数据,并检查预期的结果,完美!这是我一直希望在确定性函数中看到的测试。
但是,不干净的功能呢?
这些功能通常在服务类中或命名为“班级经理”中调用。他们执行更复杂的操作:致电一项或多项外部服务,这些外部服务的汇总结果,通过链电话使用结果等。
这是这样类的描述:
一个调用外部服务用户列表以获取用户列表的控制器类,然后通过用户formatter类传递此用户列表以准备响应。
如果我们遵循这些类的基本规则,我们可以轻松编写此类测试。
基本规则是:
- 必须嘲笑所有外部依赖。
- 我们仅通过合同测试外部依赖性:我们检查呼叫,预期结果和例外情况。
让我们写上述给定类的测试:
final class UserControllerTest extends TestCase
{
public function testListActionReturnsSuccessfulJsonResponseAndCallsAllRelatedDependencies(): void
{
$userController = new UserController(
$userRepository = $this->createMock(UserRepository::class),
$usersFormatter = $this->createMock(UsersFormatter::class),
);
$userController->setContainer($this->createMock(ContainerInterface::class));
$request = $this->createMock(Request::class);
$userRepository->expects($this->once())
->method('getList')
->willReturn($users = [
$this->createMock(User::class),
$this->createMock(User::class),
]);
$usersFormatter->expects($this->once())
->method('format')
->with(...$users)
->willReturn($formatterResponse = [
'key' => 'any response here',
'why' => 'cuz we dont case about result of 3-rd party services here, just any expected results'
]);
$response = $userController->listAction($request);
$expectedJson = json_encode($formatterResponse, JsonResponse::DEFAULT_ENCODING_OPTIONS);
$this->assertSame($expectedJson, $response->getContent());
}
}
结论
尝试编写更确定性的方法,这些方法很容易被测试覆盖。
不确定性的方法不应未经测试,对预期行为的简单检查可以从意外的错误和错误中保存您的代码。
请记住,您的代码越好被测试所涵盖的范围,而不是保护未来的错误和重构。