Approval-based snapshot testing library for Go (mirror)
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 }