rockylars / faker
类似于 Mockery 和 Prophecy 的简单类/方法模拟工具,一次使测试专注于一个服务
Requires
- php: >=8.0
- thecodingmachine/safe: >=2.5
Requires (Dev)
- codeception/codeception: >=5.1
- codeception/module-asserts: >=3.0
- codeception/module-db: >=3.1
- codeception/module-phpbrowser: >=3.0
- friendsofphp/php-cs-fixer: >=3.49
- phpstan/phpstan: >=1.10
- phpstan/phpstan-deprecation-rules: >=1.1
- psr/clock: >=1.0
- psr/log: >=3.0
- rockylars/package-files: >=1.0
- spaze/phpstan-disallowed-calls: >=3.1
- thecodingmachine/phpstan-safe-rule: >=1.2
README
简介
欢迎来到单服务测试的世界,这是我最喜欢的项目 Faker。它很简单,但能满足您的需求,只需创建一个类并让它继承 Faker 并实现服务接口。我建议在测试文件夹中创建一个名为 "fake" 的单独文件夹,以防止它们在测试文件中丢失。我不建议在功能测试中使用它们,只用于集成和单元测试。
历史
Faker 是我在 2023 年 3 月左右在 Future500 公司为前公司 FinanceMatters/BondCenter 项目工作时,通过类方法制作的。在 Future500 和 FinanceMatters/BondCenter 都有一些非常好的家伙。接下来的一个月,添加了更多功能,由于一些类的大小,方法被复制以简化程序。随着 PHP 8 将抛出转换为语句,代码非常紧凑,但更多的测试表明代码需要稍微少一些压缩。几个月过去了,由于我无法控制的意外情况,我与这些公司的合作关系结束了长达 5 年的关系。幸运的是,那里的人们真的很友好,也很遗憾他们对我在 Faker 上的乐趣并不那么感兴趣,所以他们允许我保留它并将其作为包分发。
所以,这就是我构建的框架,因为我非常讨厌使用 prophecy 和发现 mocking 太复杂,不够简单。这个简单的抽象类允许您随心所欲地构建假类,而 PHPStan 从不会在您耳边大喊大叫。它们被设计得尽可能简单,后来添加了一些 getAllCalls.. 方法,这样就不需要在添加新方法时进行技术上的新检查。
用法
对于单元测试,我建议模拟一切,但有些类不应该被模拟,而应该保持半活跃状态。例如,一个带有 updateTime() 方法的假时钟就是一个半活跃类。
对于集成测试,只需模拟您需要模拟的内容,例如在测试存储库时不要模拟连接,但您可以使用像 Guzzle 客户端这样的假对象。关于这一点,实际上不要使用 Faker 进行日志记录,您将对日志有很多调用,但您已经在测试输出,而不太关心重复的输入。对于日志记录器,最好的做法是只收集数组并将其“模拟”成这样,其他类似的不属于您更深层的类也应该得到相同的数组收集处理。然而,尽管如此,您做您想做的。
未来
我将添加一个方法,在设置的反应数量和执行的调用数量之间添加一个计数检查,类似于 Mockery 提供的功能。当然,如果您有很多反应或设置数据但错过了实际上没有看到任何内容,这在您为单个服务设置了大量的假类时会发生。
示例
final class FakeUserRepository extends Faker implements UserRepositoryInterface { public const FUNCTION_GET_USER_BY_ID = 'getUserById'; public const FUNCTION_GET_USERS = 'getUsers'; public const FUNCTION_UPDATE_LAST_LOGIN = 'updateLastLogin'; public const FUNCTION_DELETE_USER = 'deleteUser'; public function getUserById(int $userId): User { return $this->fakeCall(__FUNCTION__, [ 'userId' => $userId, ]); } /** @return array<int, User> */ public function getUsers(): array { return $this->fakeCall(__FUNCTION__, [ 'a call was made', ]); } public function updateLastLogin(int $userId): void { $this->fakeCall(__FUNCTION__, [ 'userId' => $userId, ]); } public function deleteUser(int $userId): void { $this->fakeCall(__FUNCTION__, [ 'userId' => $userId, ]); } }
final class DeleteUserServiceCest { private DeleteUserService $deleteUserService; private FakeLogger $fakeLogger; private FakeUserRepository $fakeUserRepository; public function _before(UnitTester $tester): void { $this->deleteUserService = new DeleteUserService( $this->fakeLogger = new FakeLogger(), $this->fakeUserRepository = new FakeUserRepository() ); } public function deleteUserWillCheckAndDeleteUserByIdIfNothingIsLinked(UnitTester $tester): void { $this->fakeUserRepository->setResponsesFor(FakeUserRepository::FUNCTION_GET_USER_BY_ID, [ [Faker::ACTION_RETURN => new User( id: 1, name: 'Rocky', isAdmin: false, lastLogin: DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', '2023-02-17 12:13:14', new \DateTimeZone('Europe/Amsterdam') ), )], ]); $this->fakeUserRepository->setResponsesFor(FakeUserRepository::FUNCTION_DELETE_USER, [ [Faker::ACTION_VOID => null], ]); $this->deleteUserService->deleteUser(1); $tester->assertSame( [ [ 'level' => 'debug', 'message' => 'User 1 was deleted', 'context' => [], ], ], $this->fakeLogger->getLogs(), ); $tester->assertSame( [ FakeUserRepository::FUNCTION_DELETE_USER => [ [ 'userId' => 1, ], ], FakeUserRepository::FUNCTION_GET_USER => [ [ 'userId' => 1, ], ], ], $this->fakeUserRepository->getAllCallsInStyleSorted() ); } }
请注意,如果您使用 expectException 和 expectExceptionMessage 而不是 expectThrowable,您应该使用以下代码来包装您的调用,否则它们将静默忽略其下方的断言。
// Using expectException and expectExceptionMessage will stop the test at the error, so a try catch is used instead. $exceptionWasCaught = false; try { // Call that will throw an exception } catch (\Throwable $throwable) { // Do not assert like this on natural exceptions as those generate traces and such you can't just replicate. self::assertEquals( new \RuntimeException( "something happened", 301 ), $throwable ); $exceptionWasCaught = true; } self::assertTrue($exceptionWasCaught);
在 Linux 上设置项目以进行提交
- 确保 Docker 运行正常,您不需要为此创建账户。
- 创建一个 GitHub 账户(显然)以进行提交。
- 获取并设置一个 SSH token(最好是 id_ed25519),并将其连接到您的 GitHub 账户。
- 如果没有,您将无法正确地拉取/推送任何内容。
- 下载项目并
cd到该文件夹。- 如果您计划提交任何 PR 且没有权限,请先创建一个分支,获取它,然后尝试合并该分支的 PR。
- 确保运行
git config --global --list和git config --list都会显示user.email=YOUR_GITHUB_EMAIL和user.name=YOUR_GITHUB_USER_NAME。- 如果没有,以下是修复步骤
- 为项目设置值并取消本地设置的值,否则仅设置本地值。
- 如果这样做,您的提交将不会链接到账户。
- 确保运行
groups会显示其中的docker。- 如果没有,以下是修复步骤
- 运行
sudo usermod -aG docker $USER然后重新启动您的电脑。 - 如果没有这样做,您将无法运行所需的 Docker 命令。
- 确保运行
ls -la ~/.composer会显示您的用户而不是root。- 如果没有,以下是修复步骤
- 运行
sudo chown -R $USER:$USER ~/.composer。 - 如果没有这样做,您将无法存储库身份验证和 Composer 缓存。
- 确保已安装
make扩展。 - 运行
make setup,然后您就完成了。
[可选] 获取对 GitHub 上您有权限访问的私有仓库的访问权限
- 在 GitHub 上生成一个只有仓库权限的访问令牌。
- 运行
make composer并添加config --global github-oauth.github.com YOUR_GENERATED_TOKEN。