Approval-based snapshot testing library for Go (mirror)
at main 632 lines 13 kB view raw
1package shutter_test 2 3import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/ptdewey/shutter" 13) 14 15func TestSnapMultiple(t *testing.T) { 16 shutter.SnapMany(t, "Multiple Values Test", []any{"value1", "value2", 42, "foo", "bar", "baz", "wibble", "wobble", "tock", nil}) 17} 18 19type CustomStruct struct { 20 Name string 21 Age int 22} 23 24func (c CustomStruct) Format() string { 25 return fmt.Sprintf("CustomStruct{Name: %s, Age: %d}", c.Name, c.Age) 26} 27 28func TestSnapCustomType(t *testing.T) { 29 cs := CustomStruct{ 30 Name: "Alice", 31 Age: 30, 32 } 33 shutter.Snap(t, "Custom Type Test", cs) 34} 35 36func contains(s, substr string) bool { 37 return strings.Contains(s, substr) 38} 39 40func cleanupTestSnapshots(t *testing.T) { 41 t.Helper() 42 43 cwd, err := os.Getwd() 44 if err != nil { 45 t.Logf("failed to get cwd: %v", err) 46 return 47 } 48 49 snapshotDir := filepath.Join(cwd, "__snapshots__") 50 _ = os.RemoveAll(snapshotDir) 51} 52 53// ============================================================================ 54// COMPLEX GO STRUCTURES TESTS 55// ============================================================================ 56 57type User struct { 58 ID int 59 Username string 60 Email string 61 Active bool 62 CreatedAt time.Time 63 Roles []string 64 Metadata map[string]any 65} 66 67type Post struct { 68 ID int 69 Title string 70 Content string 71 Author User 72 Tags []string 73 Comments []Comment 74 Likes int 75 Published bool 76 CreatedAt time.Time 77} 78 79type Comment struct { 80 ID int 81 Author string 82 Content string 83 CreatedAt time.Time 84 Replies []Comment 85} 86 87func TestComplexNestedStructure(t *testing.T) { 88 user := User{ 89 ID: 1, 90 Username: "john_doe", 91 Email: "john@example.com", 92 Active: true, 93 CreatedAt: time.Date(2023, 1, 15, 10, 30, 0, 0, time.UTC), 94 Roles: []string{"admin", "moderator", "user"}, 95 Metadata: map[string]any{ 96 "theme": "dark", 97 "notifications": true, 98 "language": "en", 99 "preferences": map[string]any{ 100 "email_frequency": "weekly", 101 "notifications": true, 102 }, 103 }, 104 } 105 106 comments := []Comment{ 107 { 108 ID: 1, 109 Author: "alice", 110 Content: "Great post!", 111 CreatedAt: time.Date(2023, 2, 1, 14, 22, 0, 0, time.UTC), 112 Replies: []Comment{ 113 { 114 ID: 2, 115 Author: "bob", 116 Content: "I agree!", 117 CreatedAt: time.Date(2023, 2, 1, 15, 45, 0, 0, time.UTC), 118 Replies: []Comment{}, 119 }, 120 }, 121 }, 122 { 123 ID: 3, 124 Author: "charlie", 125 Content: "Thanks for sharing!", 126 CreatedAt: time.Date(2023, 2, 2, 9, 30, 0, 0, time.UTC), 127 Replies: []Comment{}, 128 }, 129 } 130 131 post := Post{ 132 ID: 100, 133 Title: "Introduction to Go Snapshot Testing", 134 Content: "This is a comprehensive guide to snapshot testing in Go...", 135 Author: user, 136 Tags: []string{"go", "testing", "snapshots", "best-practices"}, 137 Comments: comments, 138 Likes: 42, 139 Published: true, 140 CreatedAt: time.Date(2023, 1, 20, 9, 0, 0, 0, time.UTC), 141 } 142 143 shutter.Snap(t, "Complex Nested Structure", post) 144} 145 146func TestMultipleComplexStructures(t *testing.T) { 147 users := []User{ 148 { 149 ID: 1, 150 Username: "alice", 151 Email: "alice@example.com", 152 Active: true, 153 Roles: []string{"user", "moderator"}, 154 Metadata: map[string]any{ 155 "verified": true, 156 "badge": "verified", 157 }, 158 }, 159 { 160 ID: 2, 161 Username: "bob", 162 Email: "bob@example.com", 163 Active: false, 164 Roles: []string{"user"}, 165 Metadata: map[string]any{ 166 "verified": false, 167 "avatar": "https://example.com/bob.jpg", 168 }, 169 }, 170 { 171 ID: 3, 172 Username: "charlie", 173 Email: "charlie@example.com", 174 Active: true, 175 Roles: []string{"user", "admin"}, 176 Metadata: map[string]any{ 177 "verified": true, 178 "account_age_days": 365, 179 }, 180 }, 181 } 182 183 shutter.Snap(t, "Multiple Complex Structures", users) 184} 185 186func TestStructureWithInterface(t *testing.T) { 187 type Response struct { 188 Status string 189 Message string 190 Data any 191 Meta map[string]any 192 } 193 194 responses := []Response{ 195 { 196 Status: "success", 197 Message: "User retrieved", 198 Data: User{ 199 ID: 1, 200 Username: "john", 201 Email: "john@example.com", 202 Active: true, 203 }, 204 Meta: map[string]any{ 205 "request_id": "req-123", 206 "timestamp": "2023-01-20T10:30:00Z", 207 }, 208 }, 209 { 210 Status: "error", 211 Message: "User not found", 212 Data: nil, 213 Meta: map[string]any{ 214 "error_code": 404, 215 "error_type": "NOT_FOUND", 216 }, 217 }, 218 { 219 Status: "success", 220 Message: "Posts retrieved", 221 Data: []Post{ 222 { 223 ID: 1, 224 Title: "First Post", 225 Published: true, 226 }, 227 }, 228 Meta: map[string]any{ 229 "total_count": 10, 230 "page": 1, 231 "per_page": 20, 232 }, 233 }, 234 } 235 236 shutter.Snap(t, "Structure with Interface Fields", responses) 237} 238 239func TestNestedMapsAndSlices(t *testing.T) { 240 complexData := map[string]any{ 241 "users": map[string]any{ 242 "active": []map[string]any{ 243 { 244 "id": 1, 245 "name": "Alice", 246 "verified": true, 247 }, 248 { 249 "id": 2, 250 "name": "Bob", 251 "verified": false, 252 }, 253 }, 254 "inactive": []map[string]any{ 255 { 256 "id": 3, 257 "name": "Charlie", 258 }, 259 }, 260 }, 261 "posts": map[string]any{ 262 "published": 42, 263 "drafts": 5, 264 "categories": []string{"tech", "lifestyle", "news"}, 265 }, 266 "stats": map[string]any{ 267 "daily": map[string]any{ 268 "views": 1500, 269 "clicks": 320, 270 "conversions": map[string]any{ 271 "total": 45, 272 "by_source": map[string]int{ 273 "organic": 25, 274 "paid": 15, 275 "referral": 5, 276 }, 277 }, 278 }, 279 }, 280 } 281 282 shutter.Snap(t, "Nested Maps and Slices", complexData) 283} 284 285func TestStructureWithPointers(t *testing.T) { 286 type Address struct { 287 Street string 288 City string 289 Zip string 290 } 291 292 type Person struct { 293 Name string 294 Age int 295 Address *Address 296 Manager *Person 297 Friends []*Person 298 Email *string 299 } 300 301 addr1 := &Address{ 302 Street: "123 Main St", 303 City: "Boston", 304 Zip: "02101", 305 } 306 307 email := "jane@example.com" 308 309 person1 := Person{ 310 Name: "Jane", 311 Age: 30, 312 Address: addr1, 313 Email: &email, 314 } 315 316 person2 := Person{ 317 Name: "John", 318 Age: 35, 319 Address: addr1, 320 Manager: &person1, 321 Friends: []*Person{&person1}, 322 } 323 324 shutter.Snap(t, "Structure with Pointers", person2) 325} 326 327func TestStructureWithEmptyValues(t *testing.T) { 328 type Container struct { 329 Items []string 330 Tags map[string]string 331 OptionalID *int 332 Count int 333 Active bool 334 } 335 336 containers := []Container{ 337 { 338 Items: []string{}, 339 Tags: map[string]string{}, 340 OptionalID: nil, 341 Count: 0, 342 Active: false, 343 }, 344 { 345 Items: nil, 346 Tags: nil, 347 OptionalID: nil, 348 Count: 0, 349 Active: true, 350 }, 351 { 352 Items: []string{"a", "b", "c"}, 353 Tags: map[string]string{"type": "test", "env": "dev"}, 354 OptionalID: ptr(42), 355 Count: 3, 356 Active: true, 357 }, 358 } 359 360 shutter.Snap(t, "Structure with Empty Values", containers) 361} 362 363// ============================================================================ 364// JSON TESTS - Focus on edge cases and special handling 365// ============================================================================ 366 367func TestJsonWithSpecialCharacters(t *testing.T) { 368 jsonStr := `{ 369 "english": "Hello, World!", 370 "unicode": "こんにちは 世界 🌍", 371 "emoji": "😀 😃 😄 😁 😆", 372 "special_chars": "!@#$%^&*()_+-=[]{}|;:,.<>?", 373 "escaped": "quotes: \"double\" and 'single'", 374 "newlines": "line1\nline2\rline3\r\nline4", 375 "tabs": "col1\tcol2\tcol3", 376 "backslash": "path\\to\\file" 377 }` 378 379 var data any 380 if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { 381 t.Fatalf("failed to unmarshal json: %v", err) 382 } 383 384 shutter.Snap(t, "JSON with Special Characters", data) 385} 386 387func TestLargeJson(t *testing.T) { 388 type Product struct { 389 ID int `json:"id"` 390 Name string `json:"name"` 391 Description string `json:"description"` 392 Price float64 `json:"price"` 393 Stock int `json:"stock"` 394 InStock bool `json:"in_stock"` 395 Tags []string `json:"tags"` 396 } 397 398 type Order struct { 399 ID int `json:"id"` 400 CustomerID int `json:"customer_id"` 401 Products []Product `json:"products"` 402 Total float64 `json:"total"` 403 Status string `json:"status"` 404 CreatedAt time.Time `json:"created_at"` 405 ShippedAt *time.Time `json:"shipped_at"` 406 DeliveredAt *time.Time `json:"delivered_at"` 407 } 408 409 shippedTime := time.Date(2023, 2, 1, 10, 0, 0, 0, time.UTC) 410 411 order := Order{ 412 ID: 1001, 413 CustomerID: 42, 414 Products: []Product{ 415 { 416 ID: 1, 417 Name: "Laptop", 418 Description: "High-performance laptop", 419 Price: 999.99, 420 Stock: 5, 421 InStock: true, 422 Tags: []string{"electronics", "computers", "laptops"}, 423 }, 424 { 425 ID: 2, 426 Name: "Mouse", 427 Description: "Wireless mouse", 428 Price: 29.99, 429 Stock: 50, 430 InStock: true, 431 Tags: []string{"electronics", "accessories"}, 432 }, 433 { 434 ID: 3, 435 Name: "Keyboard", 436 Description: "Mechanical keyboard", 437 Price: 149.99, 438 Stock: 0, 439 InStock: false, 440 Tags: []string{"electronics", "accessories"}, 441 }, 442 }, 443 Total: 1179.97, 444 Status: "shipped", 445 CreatedAt: time.Date(2023, 1, 28, 14, 30, 0, 0, time.UTC), 446 ShippedAt: &shippedTime, 447 DeliveredAt: nil, 448 } 449 450 jsonBytes, err := json.MarshalIndent(order, "", " ") 451 if err != nil { 452 t.Fatalf("failed to marshal json: %v", err) 453 } 454 455 var data any 456 if err := json.Unmarshal(jsonBytes, &data); err != nil { 457 t.Fatalf("failed to unmarshal json: %v", err) 458 } 459 460 shutter.Snap(t, "Large JSON Structure", data) 461} 462 463// ============================================================================ 464// SNAPJSON FUNCTION TESTS - Focus on edge cases and real-world examples 465// ============================================================================ 466 467func TestSnapJsonWithNestedObjects(t *testing.T) { 468 jsonStr := `{ 469 "user": { 470 "id": 42, 471 "profile": { 472 "username": "jane_smith", 473 "avatar": "https://example.com/avatar.jpg", 474 "settings": { 475 "theme": "dark", 476 "notifications": true, 477 "language": "en" 478 } 479 }, 480 "permissions": ["read", "write", "admin"] 481 }, 482 "created_at": "2023-06-15T10:30:00Z" 483 }` 484 485 shutter.SnapJSON(t, "SnapJSON Nested Objects", jsonStr) 486} 487 488func TestSnapJsonComplexAPI(t *testing.T) { 489 jsonStr := `{ 490 "status": "success", 491 "code": 200, 492 "data": { 493 "users": [ 494 { 495 "id": 1, 496 "name": "Alice", 497 "role": "admin", 498 "department": "Engineering", 499 "active": true 500 }, 501 { 502 "id": 2, 503 "name": "Bob", 504 "role": "user", 505 "department": "Sales", 506 "active": true 507 }, 508 { 509 "id": 3, 510 "name": "Charlie", 511 "role": "user", 512 "department": "Marketing", 513 "active": false 514 } 515 ], 516 "pagination": { 517 "page": 1, 518 "per_page": 10, 519 "total": 3, 520 "total_pages": 1 521 } 522 }, 523 "timestamp": "2023-11-18T21:45:30Z" 524 }` 525 526 shutter.SnapJSON(t, "SnapJSON Complex API Response", jsonStr) 527} 528 529func TestSnapJsonMixedTypes(t *testing.T) { 530 jsonStr := `{ 531 "mixed_array": [ 532 "string", 533 123, 534 45.67, 535 true, 536 false, 537 null, 538 {"nested": "object"}, 539 [1, 2, 3] 540 ], 541 "complex": [ 542 {"type": "user", "id": 1}, 543 {"type": "post", "id": 100}, 544 [1, 2, 3], 545 "string", 546 null 547 ] 548 }` 549 550 shutter.SnapJSON(t, "SnapJSON Mixed Types", jsonStr) 551} 552 553func TestSnapJsonRealWorldExample(t *testing.T) { 554 jsonStr := `{ 555 "success": true, 556 "data": { 557 "product": { 558 "id": "prod_12345", 559 "name": "Premium Wireless Headphones", 560 "sku": "PWH-001", 561 "description": "High-quality wireless headphones with noise cancellation", 562 "price": { 563 "amount": 199.99, 564 "currency": "USD", 565 "discount": 10, 566 "final_price": 179.99 567 }, 568 "inventory": { 569 "total": 500, 570 "available": 425, 571 "reserved": 50, 572 "damaged": 25 573 }, 574 "specifications": { 575 "battery_life": "30 hours", 576 "weight": "250g", 577 "colors": ["black", "white", "blue"], 578 "warranty_months": 24 579 }, 580 "ratings": { 581 "average": 4.5, 582 "count": 1250, 583 "breakdown": { 584 "5": 750, 585 "4": 350, 586 "3": 100, 587 "2": 30, 588 "1": 20 589 } 590 }, 591 "reviews": [ 592 { 593 "id": "rev_001", 594 "user": "john_doe", 595 "rating": 5, 596 "title": "Excellent product!", 597 "content": "Great sound quality and comfortable to wear.", 598 "helpful": 25, 599 "created_at": "2023-11-15T10:30:00Z" 600 }, 601 { 602 "id": "rev_002", 603 "user": "jane_smith", 604 "rating": 4, 605 "title": "Good but pricey", 606 "content": "Works well, could be cheaper.", 607 "helpful": 12, 608 "created_at": "2023-11-10T14:20:00Z" 609 } 610 ] 611 }, 612 "related_products": [ 613 { 614 "id": "prod_12346", 615 "name": "Headphone Case", 616 "price": 29.99 617 }, 618 { 619 "id": "prod_12347", 620 "name": "Audio Cable", 621 "price": 14.99 622 } 623 ] 624 }, 625 "request_id": "req_abc123def456", 626 "timestamp": "2023-11-18T22:00:00Z" 627 }` 628 629 shutter.SnapJSON(t, "SnapJSON Real World Example", jsonStr) 630} 631 632func ptr[T any](t T) *T { return &t }