Skip to content

Commit 62fe5a4

Browse files
Add a skill with tips for claude to write good RT tests
1 parent 66a5ccf commit 62fe5a4

File tree

1 file changed

+393
-0
lines changed

1 file changed

+393
-0
lines changed

.claude/skills/rt-testing/SKILL.md

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
---
2+
name: rt-testing
3+
description: Use when writing or reviewing RT tests - provides guidance on precise validation, proper test structure, and RT-specific testing patterns
4+
---
5+
6+
# RT Testing Standards and Practices
7+
8+
This skill provides comprehensive guidance for writing high-quality, precise tests for RT (Request Tracker).
9+
10+
## Core Testing Philosophy
11+
12+
**Tests must validate EXACTLY what you expect, not approximately what you hope to find.**
13+
14+
### The Golden Rule
15+
> If you're writing a test, you should know precisely what the code produces. The test should validate that exact output, not fuzzy-match that something similar appears somewhere.
16+
17+
## Validation Precision
18+
19+
### ❌ WRONG: Loose Text Matching
20+
21+
```perl
22+
# BAD: Could match anywhere on page
23+
$m->content_contains('50', 'Priority displays');
24+
25+
# BAD: Too permissive regex
26+
ok($page_html =~ /Subject/, 'Has subject');
27+
```
28+
29+
**Problems:**
30+
- Could match in wrong context (sidebar, footer, JavaScript, etc.)
31+
- Doesn't validate structure
32+
- Will pass even if feature is broken but text appears elsewhere
33+
- Hides regressions
34+
35+
### ✅ RIGHT: Precise Structure Validation
36+
37+
```perl
38+
# GOOD: Validates exact location and structure
39+
my $priority_cell = $first_row->find('td')->[5];
40+
is($priority_cell->text, 'Medium', 'Priority cell shows Medium for priority 50');
41+
42+
# GOOD: Validates specific HTML structure
43+
ok($page_html =~ /<td[^>]*>.*?Medium.*?<\/td>/is,
44+
'Priority "Medium" appears in table cell');
45+
```
46+
47+
## DOM vs Content Validation
48+
49+
### When to Use DOM Selectors
50+
51+
Use `$m->dom` when you need:
52+
- **Precise element location**: "The 3rd cell in the 2nd row"
53+
- **Structure validation**: "A link inside a table header"
54+
- **Attribute checking**: Link href, CSS classes, data attributes
55+
- **Navigation**: Finding specific elements in known structure
56+
57+
```perl
58+
my $dom = $m->dom;
59+
my $table = $dom->at('table.collection-as-table');
60+
my $first_row = $table->at('tbody tr:first-child');
61+
my @cells = $first_row->find('td')->each;
62+
63+
# Validate specific cell
64+
my $subject_cell = $cells[1];
65+
my $subject_link = $subject_cell->at('a');
66+
ok($subject_link, 'Subject cell contains a link');
67+
like($subject_link->attr('href'), qr/Ticket\/Display\.html/,
68+
'Subject link points to ticket display');
69+
```
70+
71+
### When to Use Content Regex
72+
73+
Use `$m->content` with regex when:
74+
- **Checking presence in context**: "Does this appear in a `<td>`?"
75+
- **Nested content**: Text might be in various nested elements
76+
- **Multiple matches**: Need to find all occurrences
77+
- **HTML attributes**: Checking for specific attributes in elements
78+
79+
```perl
80+
my $page_html = $m->content;
81+
82+
# Validate text appears in specific HTML context
83+
ok($page_html =~ /<td[^>]*>.*?open.*?<\/td>/is,
84+
'Status "open" appears in table cell');
85+
86+
# Validate attribute in specific element type
87+
ok($page_html =~ /<th[^>]*>.*?<a[^>]*href="[^"]*OrderBy=Subject"[^>]*>Subject<\/a>.*?<\/th>/is,
88+
'Subject header is sortable link');
89+
```
90+
91+
### ❌ NEVER: Loose Content Matching
92+
93+
```perl
94+
# NEVER DO THIS - too imprecise
95+
$m->content_contains('Medium'); # Where? In what context?
96+
ok($page_html =~ /Subject/); # Could be anywhere!
97+
```
98+
99+
## RT-Specific Selectors
100+
101+
### Finding the Right Table
102+
103+
RT has multiple tables on many pages. Always use specific selectors:
104+
105+
```perl
106+
# ❌ WRONG: Too generic
107+
my $table = $dom->at('table.table');
108+
109+
# ✅ RIGHT: RT-specific class
110+
my $table = $dom->at('table.collection-as-table');
111+
112+
# ✅ EVEN BETTER: Ticket-specific
113+
my $table = $dom->at('table.ticket-list');
114+
```
115+
116+
**RT table classes:**
117+
- `collection-as-table` - All search results/collection displays
118+
- `ticket-list` - Ticket search results specifically
119+
- `collection` - Non-ticket collections (assets, etc.)
120+
121+
### Understanding Feature Output and Structure
122+
123+
When testing any feature, know exactly what it produces:
124+
125+
```perl
126+
# Example: Testing a feature that adds title attributes
127+
# Specification: Field with /TITLE: modifier produces title attribute
128+
# Expected: <div title="#">1</div>
129+
130+
# Test should validate BOTH the display and the attribute
131+
my $id_cell = $first_row->find('td')->[0];
132+
ok($id_cell->at('[title="#"]'), 'Cell has title attribute as specified');
133+
```
134+
135+
## Test Data Setup
136+
137+
### Keep It Simple
138+
139+
```perl
140+
# ✅ GOOD: Minimal setup
141+
my $ticket = RT::Test->create_ticket(
142+
Queue => 'General',
143+
Subject => 'Test ticket',
144+
Status => 'open',
145+
);
146+
147+
# ❌ WRONG: Unnecessary complexity
148+
my $user = RT::Test->load_or_create_user(...);
149+
$user->PrincipalObj->GrantRight(...); # Not needed if testing display/rendering!
150+
```
151+
152+
**Remember:**
153+
- Root user has all rights - use it when permissions aren't what you're testing
154+
- Use General queue (created automatically)
155+
- Only create what you're actually testing
156+
- Add complexity only when it's required for the specific test
157+
158+
### Test File Organization
159+
160+
**When to combine tests in one file:**
161+
- Tests share similar setup requirements
162+
- Tests don't need special configuration
163+
- Related functionality that benefits from shared test data
164+
- **Performance**: Each test file has some overhead with test database setup/teardown, so each new test file adds time. We want tests to run as fast as possible, so we don't want unnecessary overhead.
165+
166+
**When to split into separate files:**
167+
- Tests require different RT configurations like specific RT configuration values, or special setups for users, tickets, queues, rights, groups, etc.
168+
- Tests are testing completely different subsystems
169+
- File is becoming too large to maintain (>1000 lines)
170+
171+
```perl
172+
# ✅ GOOD: Combine related tests in the same file
173+
# - Tests can use the same test objects, like users tickets, etc.
174+
# - Tests are all related to the same features or parts of RT so a failure immediately indicates what part of RT has an issue.
175+
# - The test won't get so long that it will create a performance issue for running the test.
176+
# - All share similar test database needs or easy updates, like adding a new ticket, can be made to accommodate related tests.
177+
```
178+
179+
**Key principle**: Balance test organization clarity with test execution speed. Database overhead is significant in RT tests.
180+
181+
## Test Structure
182+
183+
### Organize with `diag`
184+
185+
```perl
186+
diag "Validate table structure exists";
187+
# ... structure tests ...
188+
189+
diag "Validate header content";
190+
# ... header tests ...
191+
192+
diag "Validate data cells contain correct values";
193+
# ... data cell tests ...
194+
```
195+
196+
### Be Deterministic - Avoid Conditionals
197+
198+
**Tests must be deterministic.** If you're writing a test, you know what the code should produce. Don't use conditionals to handle "maybe this exists, maybe it doesn't" - assert that the expected structure exists.
199+
200+
```perl
201+
# ❌ WRONG: Conditional makes test non-deterministic
202+
my @rows = $table->find('thead tr')->each;
203+
if (@rows > 1) {
204+
# Test second row...
205+
ok($rows[1]->at('th'), 'Second row has headers');
206+
}
207+
# If second row doesn't exist, test silently passes - bug hidden!
208+
209+
# ✅ RIGHT: Explicit expectation
210+
my @rows = $table->find('thead tr')->each;
211+
is(scalar @rows, 2, 'Table has exactly 2 header rows (Row 1 + NEWLINE Row 2)');
212+
my $second_row = $rows[1];
213+
ok($second_row->at('th'), 'Second row has headers');
214+
# If second row doesn't exist, test fails immediately - bug detected!
215+
```
216+
217+
**Why this matters:**
218+
- If structure changes unexpectedly, test should **fail**, not skip
219+
- Tests document expected behavior - conditionals hide expectations
220+
- Skipped tests give false confidence that everything works
221+
222+
**When testing any feature:**
223+
1. Read the specification or code to understand what it produces
224+
2. Know exactly what structure or output it creates
225+
3. Assert that exact structure exists
226+
4. No "if it exists, check it" - instead "it MUST exist, so check it"
227+
228+
```perl
229+
# Example: Feature creates 2-row table header
230+
# Specification says: Header has 2 rows (main headers + additional info)
231+
232+
# Test should validate BOTH rows explicitly
233+
my @header_rows = $table->find('thead tr')->each;
234+
is(scalar @header_rows, 2, 'Table has 2 header rows as specified');
235+
236+
# Test Row 1
237+
my @row1_headers = $header_rows[0]->find('th')->each;
238+
ok(@row1_headers >= 6, 'Row 1 has at least 6 headers');
239+
240+
# Test Row 2 - no conditional needed!
241+
my @row2_headers = $header_rows[1]->find('th')->each;
242+
ok(@row2_headers >= 5, 'Row 2 has at least 5 headers');
243+
```
244+
245+
### Test the Specification
246+
247+
Always test against the specification or documented behavior:
248+
249+
```perl
250+
# Example: Testing display specification
251+
# Spec: ID field displays as bold link with title="#"
252+
# Expected HTML: <td><b><a href="/Ticket/Display.html?id=1" title="#">1</a></b></td>
253+
254+
# Test should validate all specified elements:
255+
# 1. Cell contains a bold element
256+
# 2. Bold element contains a link
257+
# 3. Link href points to correct URL
258+
# 4. Link text is correct
259+
# 5. Element has correct title attribute
260+
261+
my $id_cell = $first_row->find('td')->[0];
262+
my $link = $id_cell->at('b a');
263+
ok($link, 'ID cell has bold link as specified');
264+
is($link->text, $ticket->id, 'Link text is ticket id');
265+
like($link->attr('href'), qr/Ticket\/Display\.html\?id=\d+/,
266+
'Link points to ticket display');
267+
ok($id_cell->at('[title="#"]'), 'Cell has title="#" as specified');
268+
```
269+
270+
## Common Patterns
271+
272+
### Validating Links
273+
274+
```perl
275+
# Check link exists and points to right place
276+
my $link = $m->find_link(text => $ticket->id);
277+
ok($link, 'Found ticket id link');
278+
like($link->url, qr/Ticket\/Display\.html\?id=\d+/,
279+
'Link URL matches expected pattern');
280+
281+
# Or use DOM
282+
my $subject_link = $table->at('tbody tr:first-child td:nth-child(2) a');
283+
ok($subject_link, 'Subject cell contains link');
284+
is($subject_link->text, 'Test ticket', 'Link text is ticket subject');
285+
```
286+
287+
### Validating Table Structure
288+
289+
```perl
290+
# Get the table
291+
my $table = $dom->at('table.collection-as-table');
292+
ok($table, 'Found collection table');
293+
294+
# Validate structure
295+
ok($table->at('thead'), 'Table has thead');
296+
ok($table->at('tbody'), 'Table has tbody');
297+
298+
my @rows = $table->find('tbody tr')->each;
299+
is(scalar @rows, 2, 'Table has exactly 2 data rows');
300+
301+
# Validate specific row
302+
my @cells = $rows[0]->find('td')->each;
303+
ok(@cells >= 6, 'First row has at least 6 cells');
304+
```
305+
306+
### Validating Multiple Tickets
307+
308+
```perl
309+
# When testing multiple tickets, validate they maintain structure
310+
my @all_rows = $table->find('tbody tr')->each;
311+
is(scalar @all_rows, 2, 'Table has 2 rows for 2 tickets');
312+
313+
# Check each row has correct structure
314+
for my $row (@all_rows) {
315+
my @cells = $row->find('td')->each;
316+
ok(@cells >= 6, 'Row has expected number of columns');
317+
ok($cells[0]->at('a'), 'ID cell has link');
318+
ok($cells[1]->at('a'), 'Subject cell has link');
319+
}
320+
```
321+
322+
## Test Scope - Stay Focused
323+
324+
Each test should focus on **one specific area** and avoid testing unrelated functionality:
325+
326+
**Example - Display/Rendering Tests:**
327+
- ✅ Test: Does the feature render HTML correctly?
328+
- ❌ Don't test: Permissions, data validation, query parsing
329+
- Those belong in their own focused tests (API tests, security tests, etc.)
330+
331+
**Example - API Tests:**
332+
- ✅ Test: Does the API method return correct data?
333+
- ❌ Don't test: How that data renders in HTML
334+
- That belongs in web/display tests
335+
336+
**Key principle**: Each test validates one thing well. If a test needs to validate permissions AND rendering AND data correctness, split it into multiple focused tests.
337+
338+
## Red Flags in Tests
339+
340+
Watch out for these anti-patterns:
341+
342+
```perl
343+
# 🚩 RED FLAG: Where does this appear?
344+
$m->content_contains('50');
345+
346+
# 🚩 RED FLAG: What structure validates this?
347+
ok($page_html =~ /Subject/);
348+
349+
# 🚩 RED FLAG: Why create unnecessary test data?
350+
my $alice = RT::Test->load_or_create_user(...); # Not needed for this test!
351+
352+
# 🚩 RED FLAG: Too generic selector
353+
my $table = $dom->at('table');
354+
355+
# 🚩 RED FLAG: Imprecise cell access
356+
my $cells = $dom->find('td')->each; # Which row? Which table?
357+
$cells[5] # Could be any table's 6th cell!
358+
359+
# 🚩 RED FLAG: Conditional test logic
360+
if (@rows > 1) {
361+
# Test second row... # Might silently skip if structure wrong!
362+
}
363+
```
364+
365+
## Running Tests
366+
367+
```bash
368+
# Single test
369+
prove -lv t/web/ticket_display.t
370+
371+
# With verbose output (same as -lv)
372+
prove -lv t/api/ticket.t
373+
374+
# Run all tests in a directory
375+
prove -l t/web/
376+
377+
# Keep temp files on failure for inspection
378+
# (automatically done by RT::Test on failure)
379+
```
380+
381+
## Summary Checklist
382+
383+
When writing a test, ask yourself:
384+
385+
- ✅ Do I know exactly what the code produces?
386+
- ✅ Am I validating that specific structure?
387+
- ✅ Would this test fail if the structure changed?
388+
- ✅ Am I using precise selectors?
389+
- ✅ Is my test data minimal and focused?
390+
- ✅ Is my test deterministic (no conditionals that skip validation)?
391+
- ✅ Would another developer understand what I'm validating?
392+
393+
**Remember: Tests are documentation of expected behavior. Be precise and deterministic.**

0 commit comments

Comments
 (0)