测试助手
- 作者仓库星标 21
- 作者更新于 实时读取
- 作者仓库 craft-pest-core
- 领域
- 写作
- 兼容 Agent
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- 信任分
- 88 / 100 · 社区维护
- 作者 / 版本 / 许可
- @markhuot · 未声明 license
- Token 消耗评级
- 中等消耗
- 接入复杂程度
- 需简单配置
- 是否需要外部 API Key
- 不需要
- 兼容的系统
- 未声明(默认跨平台)
- 底层运行要求
- 无特殊要求
- 文件与系统权限
-
- 只读
- 允许写入 / 修改
- Shell 执行
- 网络行为
- 允许外网请求
- 安装命令数
- 26 条
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
需要注意: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: Pest Testing Framework
description: Integration with the Pest PHP testing framework for writing and running tests Pest is an elegant…
category: 写作
runtime: 无特殊运行时
---
# Pest Testing Framework 输出预览
## PART A: 任务判断
- 适用问题:文章、文案、发言稿、润色或结构化表达。
- 输入要求:目标材料、限制条件、期望输出和验收方式。
- 证据边界:围绕“Overview / Running Tests / Basic Commands”读取原文规则,不把推断写成作者承诺。
## PART B: 执行结果
- **01** 任务判断:确认你的需求是否属于文章、文案、发言稿、润色或结构化表达,并标出输入、限制和预期结果。
- **02** 执行计划:优先按“Overview / Running Tests / Basic Commands”拆成步骤,说明每一步会读取什么、修改什么、产出什么。
- **03** 交付结果:给出可复制的命令、文件改动、检查清单或内容草稿,并说明如何继续迭代。
- **04** 风险边界:结合 读取文件、写入/修改文件、执行终端命令、会按任务需要访问外部网络、通常不需要额外 API Key 给出执行前确认项。
## Running Rules
- 读取文件、写入/修改文件、执行终端命令;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先小样例验证,再放大到真实任务。
- 交付时同时给结果、检查口径和下一步迭代建议。 原文没有稳定的斜杠命令要求。安装验证后通常全局生效,直接在对话里点名这个 Skill 并描述任务即可。
告诉 Agent 目标文件或材料、期望结果、不可改范围、是否允许联网或执行命令。本 Skill 的权限画像是:读取文件、写入/修改文件、执行终端命令。
先用一个小任务确认它会围绕“Overview / Running Tests / Basic Commands”工作;涉及文件或命令时,先看 diff、日志、预览或测试结果。
检查最终产物是否包含明确结果、必要证据和下一步动作;如果输出泛泛而谈,就补充输入、边界和验收标准后重跑。
---
name: Pest Testing Framework
description: Integration with the Pest PHP testing framework for writing and running tests Pest is an elegant…
category: 写作
source: markhuot/craft-pest-core
---
# Pest Testing Framework
## 什么时候使用
- Pest Testing Framework 是写作方向的技能,帮你打磨文章、稿子、内容表达 适合处理文章、文案、润色、翻译、总结和结构化表达,核心价值是把输入、判断、执行、验证和交付边界固定下来,避免 Agent 泛泛回答。 把任务拆…
- 面向文章、文案、发言稿、润色或结构化表达,优先处理能明确输入、步骤和验收标准的工作。
## 需要提供什么
- 目标材料、目录范围、期望结果和不可改动内容。
- 是否允许联网、执行命令、读写文件或调用外部服务。
## 执行规则
- 围绕「Overview / Running Tests / Basic Commands」组织步骤,不把推断写成作者事实。
- 读取文件、写入/修改文件、执行终端命令;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先跑小样例,确认结果可检查后再扩大任务范围。
## 输出要求
- 给出最终产物、关键证据、验证方式和下一步动作。
- 信息不足时标记 unknown,不编造命令、平台或依赖。 作者原文负责流程事实;仓库文件负责来源和命令;流狐只补充适用场景、限制和质量判断。
skill "Pest Testing Framework" {
输入层 -> 用户目标 + 目标文件 + 禁止范围 + 验收标准
上下文层 -> Overview / Running Tests / Basic Commands
规则层 -> SKILL.md 触发条件 / 执行顺序 / 输出格式
运行层 -> 无特殊运行时 | 读取文件、写入/修改文件、执行终端命令 | 会按任务需要访问外部网络
安全层 -> 通常不需要额外 API Key + 小任务验证 + diff / 日志复核
输出层 -> 可复制结果 + 检查清单 + 下一步迭代
} - Running tests:
php ./vendor/bin/pest - Running specific tests:
php ./vendor/bin/pest tests/MyTest.php
Overview
Pest is an elegant PHP testing framework with a focus on simplicity. It's built on top of PHPUnit but provides a more expressive and developer-friendly syntax.
Running Tests
Basic Commands
# Run all tests
php ./vendor/bin/pest
# Run tests in a specific directory
php ./vendor/bin/pest tests/Unit
# Run a specific test file
php ./vendor/bin/pest tests/HeroComponentsTest.php
# Run tests with coverage
php ./vendor/bin/pest --coverage
# Run tests with coverage and minimum threshold
php ./vendor/bin/pest --coverage --min=80
# Run tests in parallel (faster execution)
php ./vendor/bin/pest --parallel
# Run tests with verbose output
php ./vendor/bin/pest -v
# Run tests and stop on first failure
php ./vendor/bin/pest --stop-on-failure
# Run tests matching a filter
php ./vendor/bin/pest --filter="HeroComponents"
Test Structure
Basic Test File
<?php
// tests/ExampleTest.php
test('example test', function () {
expect(true)->toBeTrue();
});
it('can perform assertions', function () {
$result = 2 + 2;
expect($result)->toBe(4);
});
Organized Tests with describe()
<?php
describe('HeroComponents', function () {
it('renders correctly', function () {
// Test implementation
});
it('handles empty state', function () {
// Test implementation
});
});
Using beforeEach() and afterEach()
<?php
beforeEach(function () {
// Setup code that runs before each test
$this->user = createUser();
});
afterEach(function () {
// Cleanup code that runs after each test
$this->user = null;
});
test('user can login', function () {
expect($this->user)->not->toBeNull();
});
Common Expectations
Value Assertions
expect($value)->toBe($expected); // Strict equality (===)
expect($value)->toEqual($expected); // Loose equality (==)
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($value)->toBeNull();
expect($value)->toBeEmpty();
expect($value)->toBeGreaterThan(5);
expect($value)->toBeLessThan(10);
Type Assertions
expect($value)->toBeInt();
expect($value)->toBeString();
expect($value)->toBeArray();
expect($value)->toBeObject();
expect($value)->toBeInstanceOf(MyClass::class);
Array/Collection Assertions
expect($array)->toHaveCount(3);
expect($array)->toContain('value');
expect($array)->toHaveKey('key');
expect($array)->each->toBeString();
String Assertions
expect($string)->toStartWith('Hello');
expect($string)->toEndWith('World');
expect($string)->toContain('test');
expect($string)->toMatch('/regex/');
Negation
expect($value)->not->toBe('wrong');
expect($array)->not->toBeEmpty();
Datasets
Use datasets to run the same test with different inputs:
<?php
it('can add numbers', function (int $a, int $b, int $expected) {
expect($a + $b)->toBe($expected);
})->with([
[1, 2, 3],
[5, 5, 10],
[10, 20, 30],
]);
// Named datasets
it('validates email', function (string $email) {
expect(isValidEmail($email))->toBeTrue();
})->with([
'valid email' => 'test@example.com',
'another valid' => 'user@domain.co.uk',
]);
HTTP Requests
Pest provides methods to test HTTP endpoints by simulating requests and asserting on responses.
Basic GET Requests
<?php
it('loads the homepage', function () {
$this->get('/')
->assertOk();
});
// Can be chained in a fluent style
it('returns json data')
->get('/api/data')
->assertOk()
->assertJson(['status' => 'success']);
POST Requests
<?php
it('posts to an action', function () {
$this->post('/post-data', ['foo' => 'bar'])
->assertOk()
->assertSee('"foo":"bar"');
});
// Post JSON data
it('posts json to an action', function () {
$response = $this->postJson('/post-data', ['foo' => 'bar'])
->assertHeader('content-type', 'application/json')
->assertOk();
expect($response->json()->json())->foo->toBe('bar');
});
Authenticated Requests
Use actingAs() to make requests as a specific user:
<?php
use markhuot\craftpest\factories\User;
it('allows authenticated users to access protected pages', function () {
$user = User::factory()->create();
$this->actingAs($user)
->get('/admin/settings')
->assertOk();
});
// Shorthand for admin users
it('allows admins to access settings')
->actingAsAdmin()
->get('/admin/settings')
->assertOk();
Common Response Assertions
<?php
// Status codes
->assertOk() // 200
->assertCreated() // 201
->assertForbidden() // 403
->assertNotFound() // 404
// Content assertions
->assertSee('text') // Response contains text
->assertDontSee('text') // Response doesn't contain text
// JSON assertions
->assertJson(['key' => 'value']) // Contains JSON subset
->assertExactJson(['key' => 'value']) // Exact JSON match
->assertJsonPath('foo', 'bar') // Assert value at path
->assertJsonCount(2) // Assert JSON array count
->assertJsonStructure(['foo', 'bar']) // Assert JSON structure
->assertJsonFragment(['baz' => 'qux']) // Contains JSON fragment
->assertJsonMissing(['missing']) // JSON doesn't contain value
->assertJsonMissingPath('qux') // Path doesn't exist
// Header assertions
->assertHeader('x-foo') // Header exists
->assertHeader('x-foo', 'bar') // Header has value
->assertHeaderMissing('x-qux') // Header doesn't exist
// Cookie assertions
->assertCookie('cookieName') // Cookie exists
->assertCookie('cookieName', 'cookieValue') // Cookie has value
->assertCookieMissing('foo') // Cookie doesn't exist
->assertCookieExpired('cookieName') // Cookie is expired
->assertCookieNotExpired('cookieName') // Cookie is valid
// Other assertions
->assertDownload('file.jpg') // Response is a download
->assertCacheTag('foo', 'baz') // Response has cache tags
Working with Response Data
<?php
it('processes json response data', function () {
$response = $this->get('/api/users');
// Access JSON data
$data = $response->json();
expect($data)->toHaveKey('users');
// Access response content
$content = $response->content;
expect($content)->toContain('expected text');
});
Testing Forms and Links
<?php
it('clicks links and follows redirects', function () {
$this->get('/links')
->querySelector('a')
->click()
->assertOk()
->assertSee('Hello World');
});
Best Practices for HTTP Testing
Test Your Logic, Not the Framework: Focus on custom validation, rendering logic, or business rules rather than testing if Craft CMS works
Use Meaningful Assertions: Assert on the actual behavior that matters to your application
Clean URLs: Use relative URLs starting with
/for consistencyChain Assertions: Take advantage of fluent chaining for readable tests
Example of a well-focused HTTP test:
<?php
it('validates that blog posts require a title', function () {
$this->actingAsAdmin()
->post('/actions/entries/save', [
'sectionId' => 1,
'typeId' => 1,
'title' => '', // Empty title
'slug' => 'test-post',
])
->assertSessionHasErrors('title');
});
Rendering Templates Directly
Use ->renderTemplate() to render Twig templates directly without the overhead of a full HTTP request. This is faster and more focused than ->get() when you only need to test template output.
Basic Template Rendering
<?php
it('renders a template', function () {
$this->renderTemplate('pages/home')
->assertSee('Welcome');
});
// Can be chained in a fluent style
it('renders the hero component')
->renderTemplate('_components/hero')
->assertSee('Hero Content');
Passing Variables to Templates
Pass variables as the second parameter, just like you would in Twig:
<?php
it('renders a template with variables', function () {
$this->renderTemplate('_components/card', [
'title' => 'My Card Title',
'description' => 'Card description text',
])
->assertSee('My Card Title')
->assertSee('Card description text');
});
// Pass complex data
it('renders a list with entries', function () {
$entries = Entry::factory()
->section('posts')
->count(3)
->create();
$this->renderTemplate('_partials/entry-list', [
'entries' => $entries,
])
->assertSee($entries[0]->title)
->assertSee($entries[1]->title)
->assertSee($entries[2]->title);
});
All HTTP Assertions Work
Since renderTemplate() returns a TestableResponse object (same as ->get() and ->post()), all the same assertions are available:
<?php
it('uses various assertions on rendered templates', function () {
$response = $this->renderTemplate('_components/hero', [
'heading' => 'Welcome',
'showCta' => true,
]);
// Content assertions
$response->assertSee('Welcome');
$response->assertDontSee('Hidden Content');
// Access the raw content
expect($response->content)->toContain('<h1>');
});
DOM Selection and Testing
Use ->querySelector() to select and test specific elements. Because Pest's browser testing uses Playwright under the hood we should use data-testid attributes for reliable selection in complex templates. This also allows Playwright to find the same elements during browser tests. For example, in your Twig template:
<button data-testid="submit-button" class="btn-primary">Submit</button>
Then in your Pest test:
<?php
it('selects and tests specific elements using data-testid', function () {
$this->renderTemplate('_components/button', ['type' => 'primary'])
->querySelector('[data-testid="submit-button"]')
->assertSee('Submit')
->assertAttribute('class', 'btn-primary');
});
You can also query any standard CSS selector, but be cautious with this because CSS classes may change as the site evolves or dynamically as JavaScript modifies the DOM. Using data-testid attributes provides a stable way to select elements specifically for testing purposes.
<?php
it('selects and tests specific elements', function () {
$this->renderTemplate('_components/navigation')
->querySelector('nav ul')
->assertSee('Home')
->assertSee('About');
});
it('tests element attributes', function () {
$this->renderTemplate('_components/button', ['type' => 'primary'])
->querySelector('button')
->assertAttribute('class', 'btn-primary');
});
it('tests nested elements')
->renderTemplate('_layouts/page')
->querySelector('header h1')
->assertSee('Page Title');
Snapshot Testing
Templates work great with snapshot testing:
<?php
it('matches template snapshot', function () {
$this->renderTemplate('_components/card', [
'title' => 'Test Card',
'content' => 'Test content',
])
->assertMatchesSnapshot();
});
it('matches element snapshot', function () {
$this->renderTemplate('_partials/footer')
->querySelector('.copyright')
->assertMatchesSnapshot();
});
When to Use renderTemplate() vs get()
Use ->renderTemplate() when:
- ✅ Testing template logic in isolation
- ✅ Testing components or partials that don't have their own routes
- ✅ You need faster tests (no HTTP overhead)
- ✅ Testing pure template rendering without controllers or routing
Use ->get() when:
- ✅ Testing full request/response cycles
- ✅ Testing routing, controllers, or middleware
- ✅ Testing with authentication or session state
- ✅ The route performs logic before rendering
Use browser testing (visit() or visitTemplate()) when:
- ✅ Testing JavaScript interactions
- ✅ Testing visual properties (CSS, layout, spacing)
- ✅ Testing dynamic behavior that requires a browser
See Browser Testing Documentation for comprehensive information on browser testing with visit() and visitTemplate().
Best Practices
Test Template Logic, Not Framework Features: Focus on your custom Twig logic, conditionals, loops, and output
<?php // ✅ Good - Testing custom conditional logic it('shows CTA when flag is enabled', function () { $this->renderTemplate('_components/hero', ['showCta' => true]) ->assertSee('Call to Action'); $this->renderTemplate('_components/hero', ['showCta' => false]) ->assertDontSee('Call to Action'); }); // ❌ Bad - Just testing that Twig works it('renders a template', function () { $this->renderTemplate('_components/hero') ->assertSee('Hero'); });Use for Component Testing: Perfect for testing reusable components in isolation
<?php it('renders alert component with different types', function ($type, $expected) { $this->renderTemplate('_components/alert', ['type' => $type]) ->assertSee($expected); })->with([ ['success', 'alert-success'], ['error', 'alert-error'], ['warning', 'alert-warning'], ]);Combine with Factories: Use factories to create realistic test data
<?php it('renders entry card with all fields', function () { $entry = Entry::factory() ->section('articles') ->title('Test Article') ->description('Article description') ->featuredImage($imageId) ->create(); $this->renderTemplate('_components/entry-card', ['entry' => $entry]) ->assertSee('Test Article') ->assertSee('Article description') ->querySelector('img') ->assertAttribute('src'); });Template Paths: Use relative paths from your templates directory
<?php // Correct paths $this->renderTemplate('pages/home') // templates/pages/home.twig $this->renderTemplate('_components/hero') // templates/_components/hero.twig $this->renderTemplate('_partials/nav') // templates/_partials/nav.twig
Factories
See Factories Documentation for comprehensive information on creating test data with factories, including:
- Basic entry creation with
Entry::factory() - Setting field values and working with different field types
- Advanced techniques like sequences and custom authors
- Complete examples combining multiple field types
Test Organization
Pest.php Configuration
The tests/Pest.php file is used for global configuration:
<?php
uses()->in('Feature');
uses()->in('Unit');
// Set up global expectations
expect()->extend('toBeWithinRange', function (int $min, int $max) {
return $this->toBeGreaterThanOrEqual($min)
->toBeLessThanOrEqual($max);
});
Working with Existing Tests
When modifying or creating tests:
Read existing tests first: Always use the Read tool to examine current test files to understand patterns and conventions used in the project
Match existing style: Follow the same structure, naming conventions, and assertion patterns used in the codebase
Run tests after changes: Always run the test suite after making modifications to ensure nothing breaks
Check for test configuration: Look for
phpunit.xmlorpest.xmlfiles that may contain important configuration
Debugging Tests
Dumping Values
test('debugging example', function () {
$value = ['key' => 'value'];
\markhuot\craftpest\helpers\test\dump($value); // Output value and continue
\markhuot\craftpest\helpers\test\dd($value); // Output value and die
expect($value)->toHaveKey('key');
});
Using --filter
# Run only tests with "HeroComponents" in the name
php ./vendor/bin/pest --filter="HeroComponents"
# Run tests in a specific file
php ./vendor/bin/pest tests/HeroComponentsTest.php
Verbose Output
# Show more details about test execution
php ./vendor/bin/pest -v
php ./vendor/bin/pest -vv
php ./vendor/bin/pest -vvv
Test Coverage
Generating Coverage Reports
# HTML coverage report (opens in browser)
php ./vendor/bin/pest --coverage --coverage-html=coverage
# Terminal coverage report
php ./vendor/bin/pest --coverage
# Enforce minimum coverage
php ./vendor/bin/pest --coverage --min=80
Best Practices
Focus on User Logic, Not Framework Functionality: Do not write tests that only verify core Craft CMS or Pest functionality. Tests should focus on your custom logic, business rules, or validation.
❌ Bad - Testing framework functionality:
it('can create a hero component entry', function () { $entry = Entry::factory() ->section('heroComponents') ->heading('Test Hero Heading') ->description('<p>Test hero description content</p>') ->create(); // These only test that Craft and Pest work, not your code expect($entry)->toBeInstanceOf(Entry::class); expect($entry->section->handle)->toBe('heroComponents'); expect((string)$entry->heading)->toBe('Test Hero Heading'); expect((string)$entry->description)->toContain('Test hero description'); });✅ Good - Testing custom validation or business logic:
it('validates that heading cannot exceed 100 characters', function () { $entry = Entry::factory() ->section('heroComponents') ->heading(str_repeat('a', 101)) ->create(); expect($entry->getErrors('heading'))->not->toBeEmpty(); }); it('renders hero component with formatted output', function () { $entry = Entry::factory() ->section('heroComponents') ->heading('Test Heading') ->create(); $output = renderHeroComponent($entry); expect($output)->toContain('<h1>Test Heading</h1>'); expect($output)->toContain('hero-component-wrapper'); });Only create entries with factories as setup for testing your actual logic. If you're not testing custom validation, rendering logic, or business rules, you probably don't need the test.
Test Naming: Use descriptive test names that explain what is being tested
it('renders hero component with correct title and description', function () { // ... });Single Assertion Per Test: When possible, test one thing at a time
it('has a title', function () { expect($component->title)->not->toBeNull(); }); it('has a description', function () { expect($component->description)->not->toBeNull(); });Use Datasets for Similar Tests: Reduce code duplication with datasets
it('validates input', function ($input, $expected) { expect(validate($input))->toBe($expected); })->with([ ['valid', true], ['invalid', false], ]);Arrange-Act-Assert Pattern: Structure tests clearly
it('creates a user', function () { // Arrange $data = ['name' => 'John', 'email' => 'john@example.com']; // Act $user = createUser($data); // Assert expect($user->name)->toBe('John'); expect($user->email)->toBe('john@example.com'); });Clean Up After Tests: Use
afterEach()orafterAll()to clean up resourcesSkip Tests When Needed: Use
skip()to temporarily disable testsit('has a pending feature', function () { // Test implementation })->skip('Waiting for API changes');
Common Issues
Memory Limit
For large test suites, you may need to increase PHP memory limit:
php -d memory_limit=512M ./vendor/bin/pest
Xdebug
If tests are slow, check if Xdebug is enabled. Disable it for faster execution:
php -d xdebug.mode=off ./vendor/bin/pest
Additional Resources
- Official Pest Documentation: https://pestphp.com
- Craft specific Documentation: https://craft-pest.com
- Expectation API: https://pestphp.com/docs/expectations
先判断是否适合
作者设计意图
作者的方法与取舍
边界和复核