|
| 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