$npx -y skills add Aaronontheweb/dotnet-skills --skill snapshot-testingUse Verify for snapshot testing in .NET. Approve API surfaces, HTTP responses, rendered emails, and serialized outputs. Detect unintended changes through human-reviewed baseline files.
| 1 | # Snapshot Testing with Verify |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use snapshot testing when: |
| 6 | - Verifying rendered output (HTML emails, reports, generated code) |
| 7 | - Approving public API surfaces for breaking change detection |
| 8 | - Testing HTTP response bodies and headers |
| 9 | - Validating serialization output |
| 10 | - Catching unintended changes in complex objects |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## What is Snapshot Testing? |
| 15 | |
| 16 | Snapshot testing captures output and compares it against a human-approved baseline: |
| 17 | |
| 18 | 1. **First run**: Test generates a `.received.` file with actual output |
| 19 | 2. **Human review**: Developer approves it, creating a `.verified.` file |
| 20 | 3. **Subsequent runs**: Test compares output against `.verified.` file |
| 21 | 4. **Changes detected**: Test fails, diff tool shows differences for review |
| 22 | |
| 23 | This catches **unintended changes** while allowing **intentional changes** through explicit approval. |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Installation |
| 28 | |
| 29 | ### Add Verify Package |
| 30 | |
| 31 | ```bash |
| 32 | dotnet add package Verify.Xunit |
| 33 | # or for other test frameworks: |
| 34 | dotnet add package Verify.NUnit |
| 35 | dotnet add package Verify.MSTest |
| 36 | ``` |
| 37 | |
| 38 | ### Configure ModuleInitializer |
| 39 | |
| 40 | Create a `ModuleInitializer.cs` in your test project: |
| 41 | |
| 42 | ```csharp |
| 43 | using System.Runtime.CompilerServices; |
| 44 | |
| 45 | public static class ModuleInitializer |
| 46 | { |
| 47 | [ModuleInitializer] |
| 48 | public static void Init() |
| 49 | { |
| 50 | // Use source-file-relative paths for verified files |
| 51 | VerifyBase.UseProjectRelativeDirectory("Snapshots"); |
| 52 | |
| 53 | // Configure diff tool (optional - auto-detected) |
| 54 | // DiffTools.UseOrder(DiffTool.Rider, DiffTool.VisualStudioCode); |
| 55 | } |
| 56 | } |
| 57 | ``` |
| 58 | |
| 59 | --- |
| 60 | |
| 61 | ## Basic Usage |
| 62 | |
| 63 | ### Simple Object Verification |
| 64 | |
| 65 | ```csharp |
| 66 | [Fact] |
| 67 | public Task VerifyUserDto() |
| 68 | { |
| 69 | var user = new UserDto( |
| 70 | Id: "user-123", |
| 71 | Name: "John Doe", |
| 72 | Email: "john@example.com", |
| 73 | CreatedAt: new DateTime(2025, 1, 15)); |
| 74 | |
| 75 | return Verify(user); |
| 76 | } |
| 77 | ``` |
| 78 | |
| 79 | Creates `VerifyUserDto.verified.txt`: |
| 80 | ```json |
| 81 | { |
| 82 | Id: user-123, |
| 83 | Name: John Doe, |
| 84 | Email: john@example.com, |
| 85 | CreatedAt: 2025-01-15T00:00:00 |
| 86 | } |
| 87 | ``` |
| 88 | |
| 89 | ### String/HTML Verification |
| 90 | |
| 91 | ```csharp |
| 92 | [Fact] |
| 93 | public async Task VerifyRenderedEmail() |
| 94 | { |
| 95 | var html = await _emailRenderer.RenderAsync("Welcome", new { Name = "John" }); |
| 96 | |
| 97 | // Use extension parameter for proper file naming |
| 98 | await Verify(html, extension: "html"); |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | Creates `VerifyRenderedEmail.verified.html` - viewable in browser. |
| 103 | |
| 104 | --- |
| 105 | |
| 106 | ## Email Template Testing |
| 107 | |
| 108 | Use Verify to catch unintended changes in rendered email templates: |
| 109 | |
| 110 | ```csharp |
| 111 | [Fact] |
| 112 | public async Task UserSignupInvitation_RendersCorrectly() |
| 113 | { |
| 114 | var renderer = _services.GetRequiredService<IMjmlTemplateRenderer>(); |
| 115 | |
| 116 | var variables = new Dictionary<string, string> |
| 117 | { |
| 118 | { "OrganizationName", "Acme Corporation" }, |
| 119 | { "InviteeName", "John Doe" }, |
| 120 | { "InviterName", "Jane Admin" }, |
| 121 | { "InvitationLink", "https://example.com/invite/abc123" }, |
| 122 | { "ExpirationDate", "December 31, 2025" } |
| 123 | }; |
| 124 | |
| 125 | var html = await renderer.RenderTemplateAsync( |
| 126 | "UserInvitations/UserSignupInvitation", |
| 127 | variables); |
| 128 | |
| 129 | await Verify(html, extension: "html"); |
| 130 | } |
| 131 | ``` |
| 132 | |
| 133 | **Benefits for email testing:** |
| 134 | - Catches CSS/layout regressions |
| 135 | - Detects broken template variables |
| 136 | - Visual review in diff tool |
| 137 | - Version control tracks email changes |
| 138 | |
| 139 | --- |
| 140 | |
| 141 | ## API Surface Approval |
| 142 | |
| 143 | Prevent accidental breaking changes to public APIs: |
| 144 | |
| 145 | ```csharp |
| 146 | [Fact] |
| 147 | public Task ApprovePublicApi() |
| 148 | { |
| 149 | var assembly = typeof(MyLibrary.PublicClass).Assembly; |
| 150 | |
| 151 | var publicApi = assembly.GetExportedTypes() |
| 152 | .OrderBy(t => t.FullName) |
| 153 | .Select(t => new |
| 154 | { |
| 155 | Type = t.FullName, |
| 156 | Members = t.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) |
| 157 | .Where(m => m.DeclaringType == t) |
| 158 | .OrderBy(m => m.Name) |
| 159 | .Select(m => m.ToString()) |
| 160 | }); |
| 161 | |
| 162 | return Verify(publicApi); |
| 163 | } |
| 164 | ``` |
| 165 | |
| 166 | Or use the dedicated ApiApprover package: |
| 167 | |
| 168 | ```bash |
| 169 | dotnet add package PublicApiGenerator |
| 170 | dotnet add package Verify.Xunit |
| 171 | ``` |
| 172 | |
| 173 | ```csharp |
| 174 | [Fact] |
| 175 | public Task ApproveApi() |
| 176 | { |
| 177 | var api = typeof(MyPublicClass).Assembly.GeneratePublicApi(); |
| 178 | return Verify(api); |
| 179 | } |
| 180 | ``` |
| 181 | |
| 182 | Creates `.verified.txt` with full API surface - any change requires explicit approval. |
| 183 | |
| 184 | --- |
| 185 | |
| 186 | ## HTTP Response Testing |
| 187 | |
| 188 | ```csharp |
| 189 | [Fact] |
| 190 | public async Task GetUser_ReturnsExpectedResponse() |
| 191 | { |
| 192 | var client = _factory.CreateClient(); |
| 193 | |
| 194 | var response = await client.GetAsync("/api/users/123"); |
| 195 | |
| 196 | // Verify status, headers, and body together |
| 197 | await Verify(new |
| 198 | { |
| 199 | StatusCode = response.StatusCode, |
| 200 | Headers = response.Headers |
| 201 | .Where(h => h.Key.StartsWith("X-")) // Custom headers only |
| 202 | .ToDictionary(h => h.Key, h => h.Value.First()), |
| 203 | Body = await response.Conten |