File size: 59,323 Bytes
da2246c
3f47420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da2246c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>FluentAssertions 8 license — find replacements and license flags</title>
<meta name="description" content="Maps FluentAssertions lines in project and C# test files to Shouldly replacements and flags version 8 or later.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:break-word}#out th{white-space:nowrap}#out pre{white-space:pre-wrap;overflow-wrap:anywhere}#out code{white-space:pre-wrap;overflow-wrap:break-word}@media (max-width:640px){#out td,#out th{overflow-wrap:anywhere}#out th{white-space:normal}table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}h1,h2,h3,h4,p,li,dd,dt,blockquote,figcaption,summary,label,.res,.lead{overflow-wrap:anywhere}label{max-width:100%;box-sizing:border-box}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/shouldly-migration-check"><!--yj-trust-org-s191--><script type="application/ld+json" data-yj="yj-trust-org">{"@context": "https://schema.org", "@type": "Organization", "name": "ReadyStack", "url": "https://getreadystack.com/", "logo": "https://getreadystack.com/branding/readystack_profile.png", "email": "support@getreadystack.com", "contactPoint": {"@type": "ContactPoint", "contactType": "customer support", "email": "support@getreadystack.com", "availableLanguage": ["English", "German", "Japanese", "Spanish", "Portuguese"]}}</script><!--/yj-trust-org-s191--></head>
<body>
<div class="wrap">
<h1>FluentAssertions 8 license — find replacements and license flags</h1>
<p class="lede">Maps every FluentAssertions line in a .csproj or C# test file to its Shouldly replacement and flags FluentAssertions 8+ (commercial use needs a paid Xceed licence). Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">using System;
using System.Linq;
using FluentAssertions;
using Xunit;

namespace Shop.Tests;

public class OrderServiceTests
{
    private readonly OrderService _service = new(new FakeClock(new DateTime(2026, 10, 12)));

    [Fact]
    public void Total_includes_vat()
    {
        var order = _service.Create("C-1042", 3);
        var total = _service.Total(order);

        total.Should().Be(119.00m);
        order.Lines.Should().HaveCount(3);
        order.Customer.Should().NotBeNull();
    }

    [Fact]
    public void Paying_twice_throws()
    {
        var order = _service.Create("C-1042", 1);
        _service.Pay(order);

        Action act = () =&gt; _service.Pay(order);

        act.Should().Throw&lt;InvalidOperationException&gt;().WithMessage("Order is already paid");
    }

    [Fact]
    public void Invoice_matches_order()
    {
        var order = _service.Create("C-1042", 2);
        var expected = new Invoice("C-1042", 2, 79.33m);

        _service.Invoice(order).Should().BeEquivalentTo(expected);
    }
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Check one .csproj, Directory.Packages.props, packages.config or C# test file and see every FluentAssertions line with the exact Shouldly line that replaces it.. <strong>In the editor:</strong> Scan every project and test file in the workspace at once and export one dated Markdown migration plan, each FluentAssertions line mapped to its Shouldly line, for the licence review ticket. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var SHM_RULES = [
 {"id": "fa-v8-package", "sev": "error", "scope": "pkg", "title": "FluentAssertions 8+ package", "source": "NuGet FluentAssertions 8.0.0 (2025-01-14) nuspec: free for open-source and non-commercial use, commercial use requires a paid licence (xceed.com)"},
 {"id": "fa-floating-version", "sev": "warn", "scope": "pkg", "title": "Floating FluentAssertions version", "source": "A * or open-ended NuGet range resolves to the newest 8.x on the next restore"},
 {"id": "fa-v7-apache", "sev": "info", "scope": "pkg", "title": "FluentAssertions 7.x or older (Apache-2.0)", "source": "NuGet FluentAssertions 7.2.2 (2026-03-16) licenseExpression Apache-2.0"},
 {"id": "fa-central-version", "sev": "warn", "scope": "pkg", "title": "FluentAssertions version set elsewhere", "source": "Central package management keeps the version in Directory.Packages.props"},
 {"id": "fa-extension-package", "sev": "warn", "scope": "pkg", "title": "FluentAssertions extension package", "source": "FluentAssertions.* add-on packages extend the core library and leave with it"},
 {"id": "fa-msbuild-using", "sev": "warn", "scope": "msbuild", "title": "Global using in the project file", "source": "MSBuild \u003cUsing Include\u003e item"},
 {"id": "fa-using", "sev": "error", "scope": "cs", "title": "using FluentAssertions", "source": "Shouldly 4.3.0 BSD-3-Clause · AwesomeAssertions 9.6.0 Apache-2.0 (NuGet)"},
 {"id": "fa-equality", "sev": "warn", "scope": "cs", "title": "Should().Be / NotBe / BeSameAs / BeOneOf", "pairs": [["\\.Should\\(\\)\\.Be\\(", ".ShouldBe("], ["\\.Should\\(\\)\\.NotBe\\(", ".ShouldNotBe("], ["\\.Should\\(\\)\\.BeSameAs\\(", ".ShouldBeSameAs("], ["\\.Should\\(\\)\\.BeOneOf\\(", ".ShouldBeOneOf("]]},
 {"id": "fa-boolean", "sev": "warn", "scope": "cs", "title": "Should().BeTrue / BeFalse", "pairs": [["\\.Should\\(\\)\\.BeTrue\\(", ".ShouldBeTrue("], ["\\.Should\\(\\)\\.BeFalse\\(", ".ShouldBeFalse("]]},
 {"id": "fa-null-empty", "sev": "warn", "scope": "cs", "title": "Should().BeNull / BeEmpty family", "pairs": [["\\.Should\\(\\)\\.BeNull\\(", ".ShouldBeNull("], ["\\.Should\\(\\)\\.NotBeNull\\(", ".ShouldNotBeNull("], ["\\.Should\\(\\)\\.BeEmpty\\(", ".ShouldBeEmpty("], ["\\.Should\\(\\)\\.NotBeEmpty\\(", ".ShouldNotBeEmpty("], ["\\.Should\\(\\)\\.BeNullOrEmpty\\(", ".ShouldBeNullOrEmpty("], ["\\.Should\\(\\)\\.NotBeNullOrEmpty\\(", ".ShouldNotBeNullOrEmpty("]]},
 {"id": "fa-collections", "sev": "warn", "scope": "cs", "title": "Should().Contain / HaveCount / ContainSingle", "pairs": [["\\.Should\\(\\)\\.Contain\\(", ".ShouldContain("], ["\\.Should\\(\\)\\.NotContain\\(", ".ShouldNotContain("], ["\\.Should\\(\\)\\.ContainSingle\\(\\)", ".ShouldHaveSingleItem()"], ["\\.Should\\(\\)\\.HaveCount\\(", ".Count().ShouldBe("]]},
 {"id": "fa-equivalent", "sev": "warn", "scope": "cs", "title": "Should().BeEquivalentTo", "pairs": [["\\.Should\\(\\)\\.BeEquivalentTo\\(", ".ShouldBeEquivalentTo("]], "note": "comparison rules differ between the two libraries; run this test after the swap"},
 {"id": "fa-compare", "sev": "warn", "scope": "cs", "title": "Should().BeGreaterThan / BeLessThan / BeInRange", "pairs": [["\\.Should\\(\\)\\.BeGreaterThan\\(", ".ShouldBeGreaterThan("], ["\\.Should\\(\\)\\.BeLessThan\\(", ".ShouldBeLessThan("], ["\\.Should\\(\\)\\.BeGreaterThanOrEqualTo\\(", ".ShouldBeGreaterThanOrEqualTo("], ["\\.Should\\(\\)\\.BeGreaterOrEqualTo\\(", ".ShouldBeGreaterThanOrEqualTo("], ["\\.Should\\(\\)\\.BeLessThanOrEqualTo\\(", ".ShouldBeLessThanOrEqualTo("], ["\\.Should\\(\\)\\.BeLessOrEqualTo\\(", ".ShouldBeLessThanOrEqualTo("], ["\\.Should\\(\\)\\.BeInRange\\(", ".ShouldBeInRange("]]},
 {"id": "fa-strings", "sev": "warn", "scope": "cs", "title": "Should().StartWith / EndWith", "pairs": [["\\.Should\\(\\)\\.StartWith\\(", ".ShouldStartWith("], ["\\.Should\\(\\)\\.EndWith\\(", ".ShouldEndWith("]]},
 {"id": "fa-types", "sev": "warn", "scope": "cs", "title": "Should().BeOfType / BeAssignableTo", "pairs": [["\\.Should\\(\\)\\.BeOfType\u003c", ".ShouldBeOfType\u003c"], ["\\.Should\\(\\)\\.BeAssignableTo\u003c", ".ShouldBeAssignableTo\u003c"]]},
 {"id": "fa-throw", "sev": "warn", "scope": "cs", "title": "Should().Throw / ThrowAsync / NotThrow", "pairs": [["FluentActions\\.Invoking\\((.+)\\)\\.Should\\(\\)\\.Throw\u003c([^\u003e]+)\u003e\\(\\)", "Should.Throw\u003c$2\u003e($1)"], ["FluentActions\\.Awaiting\\((.+)\\)\\.Should\\(\\)\\.ThrowAsync\u003c([^\u003e]+)\u003e\\(\\)", "Should.ThrowAsync\u003c$2\u003e($1)"], ["\\b([A-Za-z_]\\w*)\\.Should\\(\\)\\.Throw\u003c([^\u003e]+)\u003e\\(\\)", "Should.Throw\u003c$2\u003e($1)"], ["\\b([A-Za-z_]\\w*)\\.Should\\(\\)\\.ThrowAsync\u003c([^\u003e]+)\u003e\\(\\)", "Should.ThrowAsync\u003c$2\u003e($1)"], ["\\b([A-Za-z_]\\w*)\\.Should\\(\\)\\.NotThrow\\(\\)", "Should.NotThrow($1)"], ["\\b([A-Za-z_]\\w*)\\.Should\\(\\)\\.NotThrowAsync\\(\\)", "Should.NotThrowAsync($1)"]]},
 {"id": "fa-withmessage", "sev": "warn", "scope": "cs", "title": ".WithMessage on a thrown exception", "pairs": [["\\.WithMessage\\(", ".Message.ShouldBe("]], "note": "FluentAssertions treats * in WithMessage as a wildcard; use Message.ShouldContain for partial text"},
 {"id": "fa-assertion-scope", "sev": "warn", "scope": "cs", "title": "AssertionScope", "note": "no 1:1 Shouldly type; group the checks as subject.ShouldSatisfyAllConditions(() =\u003e ..., () =\u003e ...)"},
 {"id": "fa-unmapped", "sev": "warn", "scope": "cs", "title": "Should() call with no 1:1 Shouldly method", "note": "rewrite this assertion by hand"}
]
;</script>
<script>// Shouldly Migration Check: maps FluentAssertions lines to Shouldly lines.
// Same file runs in VS Code (node) and in the free web page (browser).
(function () {
  var RULES = (typeof module !== 'undefined' && typeof require !== 'undefined') ? require('./rules.json') : window.SHM_RULES;
  var BY_ID = {};
  RULES.forEach(function (r) { BY_ID[r.id] = r; });
  var SEV_RANK = { info: 0, warn: 1, error: 2 };
  var SHOULDLY = '4.3.0';
  var V7_SAFE = '[7.2.2,8.0.0)';
  var PKG_RE = /<(PackageReference|PackageVersion|package)\b[^>]*?\b(?:Include|Update|id)\s*=\s*"(FluentAssertions(?:\.[\w.]+)?)"/i;
  var USING_MSBUILD_RE = /<Using\s+Include\s*=\s*"FluentAssertions(\.[\w.]+)?"/i;
  var USING_CS_RE = /^(\s*)(global\s+)?using\s+(static\s+)?FluentAssertions(\.[\w.]+)?\s*;/;

  function daysTail(today) {
    var m = String(today == null ? '' : today).match(/(\d{4})-(\d{2})-(\d{2})/);
    if (!m) return '';
    var d = Math.round((Date.UTC(+m[1], +m[2] - 1, +m[3]) - Date.UTC(2025, 0, 14)) / 864e5);
    return d >= 0 ? ' FluentAssertions 8.0.0 (NuGet 2025-01-14) changed the licence ' + d + ' days before ' + m[0] + '.' : '';
  }
  function major(v) { var m = String(v).match(/^\s*(\d+)/); return m ? +m[1] : null; }
  function classify(v) {
    if (v == null || !String(v).trim()) return 'central';
    v = String(v).trim();
    if (/^\*/.test(v)) return 'float';
    if (/^[\[(]/.test(v)) {
      var inner = v.replace(/^[\[(]|[\])]$/g, '').split(',');
      if (inner.length === 1) return major(inner[0]) >= 8 ? 'v8' : 'v7';
      var up = inner[1].trim();
      if (!up) return 'float';
      var um = major(up);
      if (um < 8) return 'v7';
      if (um === 8 && /\)$/.test(v) && /^8(\.0)*$/.test(up)) return 'v7';
      return major(inner[0]) >= 8 ? 'v8' : 'float';
    }
    return major(v) >= 8 ? 'v8' : 'v7';
  }
  function versionOf(lines, i) {
    var m = lines[i].match(/\b(?:Version|version|VersionOverride)\s*=\s*"([^"]*)"/);
    if (m) return m[1];
    if (/\/>\s*$/.test(lines[i])) return '';
    for (var j = i + 1; j < Math.min(lines.length, i + 5); j++) {
      var t = lines[j].match(/<Version>\s*([^<]+?)\s*<\/Version>/i);
      if (t) return t[1];
      if (/<\/(PackageReference|PackageVersion)>/i.test(lines[j])) break;
    }
    return '';
  }
  function shouldlyLine(line, v) {
    var out = line.replace(/"FluentAssertions"/i, '"Shouldly"');
    if (v) out = out.split('"' + v + '"').join('"' + SHOULDLY + '"');
    return out.trim();
  }
  function add(out, id, line, msg, fix, sev) {
    out.push({ check: id, sev: sev || BY_ID[id].sev, msg: msg, line: line, fix: fix || '' });
  }

  function check(text, opts) {
    opts = opts || {};
    var tail = daysTail(opts.today);
    var lines = String(text == null ? '' : text).replace(/\r\n?/g, '\n').split('\n');
    var findings = [];
    var csRules = RULES.filter(function (r) { return r.scope === 'cs' && r.pairs; });
    var inBlock = false;
    for (var i = 0; i < lines.length; i++) {
      var raw = lines[i], ln = i + 1, t = raw.trim();
      if (inBlock) { if (t.indexOf('*/') >= 0 || t.indexOf('-->') >= 0) inBlock = false; continue; }
      if (/^(\/\*|<!--)/.test(t) && t.indexOf('*/') < 0 && t.indexOf('-->') < 0) { inBlock = true; continue; }
      if (/^(\/\/|\*|<!--)/.test(t)) continue;

      var pm = raw.match(PKG_RE);
      if (pm) {
        var name = pm[2], v = versionOf(lines, i);
        if (name.toLowerCase() !== 'fluentassertions') {
          add(findings, 'fa-extension-package', ln, name + ' extends FluentAssertions and goes with it. Shouldly has no add-on to install: delete this line when the core package is replaced.', '(delete this line)');
          continue;
        }
        var kind = classify(v), fix = shouldlyLine(raw, v);
        if (kind === 'v8') add(findings, 'fa-v8-package', ln, 'FluentAssertions ' + v + ': free only for open-source and non-commercial use; commercial use needs a paid Xceed licence.' + tail + ' Replace with Shouldly ' + SHOULDLY + ' (BSD-3-Clause): ' + fix + ' , or keep the syntax with AwesomeAssertions 9.6.0 (Apache-2.0).', fix);
        else if (kind === 'float') add(findings, 'fa-floating-version', ln, 'FluentAssertions Version="' + v + '" can resolve to 8.x (commercial licence) on the next restore.' + tail + ' Pin Version="' + V7_SAFE + '" to stay on Apache-2.0, or switch: ' + fix, fix);
        else if (kind === 'v7') add(findings, 'fa-v7-apache', ln, 'FluentAssertions ' + v + ' is Apache-2.0; a bump to 8.x changes the licence. Pin Version="' + V7_SAFE + '" and ignore 8.x in Dependabot, or switch: ' + fix, fix);
        else add(findings, 'fa-central-version', ln, 'FluentAssertions version is not on this line (Directory.Packages.props or a <Version> element sets it). Check that file for 8.x; the Shouldly line here is: ' + fix, fix);
        continue;
      }
      var um = raw.match(USING_MSBUILD_RE);
      if (um) {
        var fx = um[1] ? '(delete this line)' : raw.replace(/"FluentAssertions"/i, '"Shouldly"').trim();
        add(findings, 'fa-msbuild-using', ln, 'Global using for FluentAssertions' + (um[1] || '') + ' in the project file. Shouldly: ' + fx, fx);
        continue;
      }
      var cu = raw.match(USING_CS_RE);
      if (cu) {
        var fu = cu[4] ? '(delete this line; using Shouldly; covers it)' : ((cu[2] || '') + 'using Shouldly;');
        add(findings, 'fa-using', ln, 'using FluentAssertions' + (cu[4] || '') + ';' + tail + ' Shouldly ' + SHOULDLY + ' (BSD-3-Clause): ' + fu + ' , or AwesomeAssertions 9.6.0 (Apache-2.0): using AwesomeAssertions;', fu);
        continue;
      }
      if (/\bAssertionScope\b/.test(raw)) {
        add(findings, 'fa-assertion-scope', ln, 'AssertionScope: ' + BY_ID['fa-assertion-scope'].note + '.', BY_ID['fa-assertion-scope'].note);
        continue;
      }
      if (raw.indexOf('.Should()') < 0 && raw.indexOf('.WithMessage(') < 0) continue;

      var cur = raw, hit = [], notes = [], sev = 'info';
      csRules.forEach(function (r) {
        if (r.id === 'fa-withmessage' && /ThrowAsync/.test(cur)) return;
        var matched = false;
        r.pairs.forEach(function (p) {
          var re = new RegExp(p[0], 'g');
          if (re.test(cur)) { matched = true; cur = cur.replace(new RegExp(p[0], 'g'), p[1]); }
        });
        if (matched) {
          hit.push(r);
          if (r.note && (r.id !== 'fa-withmessage' || /WithMessage\("[^"]*\*/.test(raw))) notes.push(r.note);
          if (SEV_RANK[r.sev] > SEV_RANK[sev]) sev = r.sev;
        }
      });
      if (/\.WithMessage\(/.test(cur) && /ThrowAsync/.test(cur)) notes.push('async: write (await Should.ThrowAsync<T>(act)).Message.ShouldBe(...)');
      if (/ShouldBeEquivalentTo\([^)]*=>/.test(cur)) notes.push('the options lambda has no Shouldly equivalent');
      var left = cur.indexOf('.Should()') >= 0 || /\.WithMessage\(/.test(cur);
      if (!hit.length) { add(findings, 'fa-unmapped', ln, 'Should() call with no 1:1 Shouldly method: ' + t + ' , rewrite this assertion by hand.', ''); continue; }
      if (left) notes.push('part of this line has no 1:1 Shouldly method; finish it by hand');
      var fixed = cur.trim();
      add(findings, hit[0].id, ln, hit.map(function (r) { return r.title; }).join(' + ') + ' → Shouldly: ' + fixed + (notes.length ? ' (' + notes.join('; ') + ')' : ''), fixed, left ? 'warn' : sev);
    }
    return { findings: findings };
  }

  var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
  if (typeof module !== 'undefined' && module.exports) module.exports = API;
  if (typeof window !== 'undefined') window.SHMENGINE = API;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
  const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
  return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
  const f = (res && res.findings) || [];
  const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
  f.forEach(function (x) { lines.push('  ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + '  ' + (x.line ? 'L' + x.line + '  ' : '') + (x.check || x.id || '') + '  ' + (x.msg || x.message || '')); });
  return lines.join('\n');
}
function toHtml(res) {
  const f = (res && res.findings) || [];
  if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
  const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
  return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var SHMREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = SHMREPORT;
else window.SHMREPORT = SHMREPORT;
</script>
<script>
(function(){
  var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
  today.value=new Date().toISOString().slice(0,10);
  function run(){ try { var res=window.SHMENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.SHMREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
  go.onclick=run; if (src.value.trim()) run();
})();
</script>


<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.shouldly-migration-check" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'shouldly-migration-check',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/shouldly-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'shouldly-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.shouldly-migration-check</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Shouldly Migration Check for FluentAssertions every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"shouldly-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>

<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Scan every project and test file in the workspace at once and export one dated Markdown migration plan, each FluentAssertions line mapped to its Shouldly line, for the licence review ticket.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Xceed lists a Fluent Assertions licence from 14.95 USD a year without support (xceed.com blog).</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_IVLqwb70CJwTRrL566kYsEiEzZsMUstMw0xdX2ZG8ID" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section><!--yj-trust-strip-s191--><aside data-yj="yj-trust-strip" style="max-width:46rem;margin:-1.8rem auto 2.4rem;padding:.9rem 1.2rem;border:1px solid #d8dee9;border-radius:12px;background:#f6f8fb;font:14.5px/1.55 system-ui,-apple-system,Segoe UI,sans-serif;color:#2b3640"><p style="margin:0 0 .3rem;font-weight:600">Buying here, plainly</p><ul style="margin:0;padding-left:1.2rem"><li style="margin:.25rem 0">Payment runs on Whop’s checkout: your card details go to Whop, not to us, and Whop emails you the receipt.</li><li style="margin:.25rem 0">Your licence key shows on the next page right after payment and stays in your Whop account (Your purchases).</li><li style="margin:.25rem 0">Chose a subscription? Cancel any time in your Whop account (Orders → Cancel membership); access lasts until the end of the period you paid.</li><li style="margin:.25rem 0">Your inputs are calculated in your browser and are not sent to us (unless you ask for the result by email).</li><li style="margin:.25rem 0">Questions before buying? support@getreadystack.com — we answer within 2 business days.</li></ul></aside><!--/yj-trust-strip-s191-->









<!--yj-embed-s178--><details id="embed" data-yj="yj-embed" style="max-width:46rem;margin:1.6rem auto;padding:.85rem 1.1rem;border:1px solid #8884;border-radius:10px"><summary style="cursor:pointer;font-weight:600">Embed this tool on your site (free)</summary><p style="margin:.6rem 0;opacity:.85">Paste this code into your page. It runs in your visitor’s browser and stays up to date. Nothing your visitors type is stored.</p><textarea readonly rows="6" style="width:100%;font:12px/1.4 ui-monospace,Menlo,Consolas,monospace;border:1px solid #8886;border-radius:7px;padding:.5rem">&lt;iframe src=&quot;https://getreadystack.com/embed/shouldly-migration-check/&quot; title=&quot;Shouldly Migration Check for FluentAssertions — free online check&quot; loading=&quot;lazy&quot; data-rs-embed=&quot;en-shouldly-migration-check&quot; style=&quot;width:100%;max-width:760px;height:640px;border:1px solid #e3e3e3;border-radius:10px&quot;&gt;&lt;/iframe&gt;
&lt;p style=&quot;font-size:13px;margin:6px 0 0&quot;&gt;&lt;a href=&quot;https://getreadystack.com/tools/shouldly-migration-check&quot;&gt;Shouldly Migration Check for FluentAssertions — free online check&lt;/a&gt; by &lt;a href=&quot;https://getreadystack.com/&quot;&gt;ReadyStack&lt;/a&gt;&lt;/p&gt;
&lt;script&gt;window.addEventListener(&quot;message&quot;,function(e){if(e.origin!==&quot;https://getreadystack.com&quot;||!e.data||e.data.type!==&quot;readystack-embed&quot;)return;var f=document.querySelectorAll(&quot;iframe[data-rs-embed]&quot;);for(var i=0;i&lt;f.length;i++){if(f[i].contentWindow===e.source)f[i].style.height=Math.min(e.data.h+6,8000)+&quot;px&quot;;}});&lt;/script&gt;</textarea><p style="margin:.5rem 0 0"><button type="button" data-yj="yj-embed-copy" style="font:inherit;padding:8px 14px;border:1px solid #8884;border-radius:9px;background:transparent;cursor:pointer">Copy code</button> &nbsp;<a href="/embed/">All embeddable tools</a></p><p style="margin:.7rem 0 0;font-size:.92em;opacity:.9">For businesses: your name and logo instead of ours, and each visitor’s result + e-mail in your inbox (only when they tick consent) — Pro from $19/month. <a href="/embed/pro/?ref=embed_box">See Pro &rarr;</a></p></details><script data-yj="yj-embed">(function(){var b=document.querySelector("[data-yj=yj-embed-copy]");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var a=b.closest("details").querySelector("textarea");var v=a.value;function d(){b.textContent="Copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(v).then(d,function(){a.select();});}else{a.select();}});})();</script><!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>

<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>

<script data-yj="yj-prefill-s181 v1">(function(){try{var q=new URLSearchParams(location.search);if(!q.toString())return;var P={"R": {"employee": ["employee|w-?2\\b|arbeitnehmer|angestellt|会社員|従業員|給与所得|trabajador|asalariad|empregad|clt\\b|salari", "Employee"], "self": ["self-?employ|freelanc|sole (trader|prop)|selbst(ä|ae)ndig|freiberuf|個人事業|フリーランス|aut(ó|o)nom|\\bmei\\b", "Freelancer / self-employed"], "landlord": ["landlord|rental|vermiet|大家|不動産|arrendador|alquiler|locador|aluguel", "Landlord"], "investor": ["invest|trader|anleger|投資|inversor|investidor", "Investor / trader"], "employer": ["employer|arbeitgeber|雇用主|事業主|empleador|empregador", "Employer"], "seller": ["seller|online shop|h(ä|ae)ndler|販売|vendedor|e-?commerce", "Online seller"]}, "C": {"US": "^(us|usa)$|united states|estados unidos|vereinigte staaten|アメリカ|米国", "UK": "^(uk|gb)$|united kingdom|great britain|reino unido|vereinigtes k|イギリス|英国", "DE": "^de$|germany|deutschland|alemania|alemanha|ドイツ", "JP": "^jp$|japan|jap(ó|ã|o)n?|日本", "ES": "^es$|spain|españa|espana|espanha|spanien|スペイン", "BR": "^br$|brazil|brasil|ブラジル"}, "B": "your 2027 dates", "L": "en"};function ours(el){return !!(el&&el.closest&&el.closest("[data-yj],[data-yj-cta],[data-yj-cal],.yj-email-gate,.yj-find,.yj-related,#yj-ask,#yjgo,#yjr,header,footer,nav"));}function fields(){var r=document.querySelector("main")||document.body;return [].filter.call(r.querySelectorAll("input,select,textarea"),function(el){var t=(el.type||"").toLowerCase();return !ours(el)&&!el.disabled&&["email","hidden","search","password","file","submit","button","reset","image"].indexOf(t)<0;});}function key(el){return el.getAttribute("data-k")||el.id||el.name||"";}function fire(el){["input","change"].forEach(function(n){try{el.dispatchEvent(new Event(n,{bubbles:true}))}catch(e){}});}function setv(el,v){v=String(v).slice(0,80);var t=(el.type||"").toLowerCase(),tg=el.tagName;if(tg=="SELECT"){var o=[].filter.call(el.options,function(o){return o.value==v||o.text.trim()==v})[0];if(!o)return false;el.value=o.value;}else if(t=="checkbox"||t=="radio"){if(!/^(1|0|true|false|on|off|yes|no)$/i.test(v))return false;el.checked=/^(1|true|on|yes)$/i.test(v);}else if(t=="number"||t=="range"){var n=parseFloat(v.replace(/\s/g,"").replace(",","."));if(!isFinite(n))return false;el.value=String(n);}else if(t=="date"){if(!/^\d{4}-\d{2}-\d{2}$/.test(v))return false;el.value=v;}else{if(/[<>]/.test(v))return false;el.value=v;}fire(el);return true;}function pick(el,rx){if(el.tagName!="SELECT")return false;var re=new RegExp(rx,"i");var o=[].filter.call(el.options,function(o){return re.test(o.value)||re.test(o.text)})[0];if(!o||el.value==o.value)return false;el.value=o.value;fire(el);return true;}function run(){var F=fields(),done={},n=0;q.forEach(function(v,k){if(n>=20||/^(ref|role|cc|utm_.*|fbclid|gclid)$/.test(k)||!/^[A-Za-z0-9_-]{1,40}$/.test(k))return;F.forEach(function(el){if(key(el)===k&&!done[k]&&setv(el,v)){done[k]=1;n++;}});});var cc=(q.get("cc")||"").toUpperCase().replace(/[^A-Z]/g,"");if(P.C[cc])F.forEach(function(el){var k=key(el);if(!done[k]&&/country|land|pa(i|í)s|pays|国/i.test(k+" "+(el.id||""))&&pick(el,P.C[cc])){done[k]=1;n++;}});var ro=(q.get("role")||"").replace(/[^a-z]/g,""),R=P.R[ro];if(R)F.forEach(function(el){var k=key(el);if(!done[k]&&el.tagName=="SELECT"&&[].some.call(el.options,function(o){return [].some.call(Object.keys(P.R),function(x){return new RegExp(P.R[x][0],"i").test(o.text)})})&&pick(el,R[0])){done[k]=1;n++;}});if(q.get("ref")=="calendar"){var h=document.querySelector("main h1")||document.querySelector("h1");var d=document.createElement("div");d.setAttribute("data-yj","yj-prefill-back");d.style.cssText="margin:.4rem 0 .8rem;font:15px/1.5 system-ui,sans-serif;opacity:.9";var a=document.createElement("a");a.href="/"+P.L+"/calendar/?ref=tool"+(R?"&role="+ro:"")+(P.C[cc]?"&cc="+cc:"");a.textContent=(R?R[1]+" \u00b7 ":"")+P.B+" \u2192";d.appendChild(a);if(h&&h.parentNode)h.parentNode.insertBefore(d,h.nextSibling);}document.documentElement.setAttribute("data-yj-prefilled",String(n));}if(document.readyState=="loading")document.addEventListener("DOMContentLoaded",run);else run();}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-10-12">Updated 2026-10-12</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-10-12","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku637262_result_card.jpg" alt="Migrate from FluentAssertions to Shouldly: 6 lines in one xUnit test file" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>shouldly migration: 6 findings in one xUnit test file for a .NET order service, written by a C# developer at a for-profit company and checked on 2026-10-12. Every finding comes back with the Shouldly line that replaces it:</p>
<table><thead><tr><th>Line</th><th>FluentAssertions</th><th>Shouldly</th></tr></thead><tbody><tr><td>3</td><td>using FluentAssertions;</td><td>using Shouldly;</td></tr><tr><td>18</td><td>total.Should().Be(119.00m);</td><td>total.ShouldBe(119.00m);</td></tr><tr><td>19</td><td>order.Lines.Should().HaveCount(3);</td><td>order.Lines.Count().ShouldBe(3);</td></tr><tr><td>20</td><td>order.Customer.Should().NotBeNull();</td><td>order.Customer.ShouldNotBeNull();</td></tr><tr><td>31</td><td>act.Should().Throw&lt;InvalidOperationException&gt;().WithMessage(&quot;Order is already paid&quot;);</td><td>Should.Throw&lt;InvalidOperationException&gt;(act).Message.ShouldBe(&quot;Order is already paid&quot;);</td></tr><tr><td>40</td><td>_service.Invoice(order).Should().BeEquivalentTo(expected);</td><td>_service.Invoice(order).ShouldBeEquivalentTo(expected);</td></tr></tbody></table>
<p>The same file rewritten with those six lines returns 0 findings.</p>
<h3>What changed on 2025-01-14</h3>
<p>FluentAssertions 8.0.0 was published on NuGet on 2025-01-14. Its package description says the version is free for open-source projects and non-commercial use, and that commercial use requires a paid licence from Xceed. Every 7.x release is Apache-2.0, and 7.2.2 (published 2026-03-16) is the latest one. The newest 8.x release is 8.11.0 (2026-09-14). On 2026-10-12 the change was 636 days old.</p>
<h3>Why a version list is not enough</h3>
<p>dotnet list package prints versions, never licences. A test project that says Version=&quot;[7.0.0,)&quot; or Version=&quot;*&quot; resolves to the newest 8.x on the next restore, and a Dependabot pull request that bumps 7.2.2 to 8.11.0 looks like any other green update. The check reads the project file and flags both: 8.x references as errors, floating ranges as warnings, with the pin [7.2.2,8.0.0) if you want to stay on Apache-2.0 for now.</p>
<h3>Two exits</h3>
<p>Shouldly 4.3.0 is BSD-3-Clause. Its syntax is different (x.ShouldBe(y) instead of x.Should().Be(y)), which is why each finding carries the rewritten line. AwesomeAssertions 9.6.0 is an Apache-2.0 fork that keeps the FluentAssertions syntax; the using-line finding names it as the second exit: using AwesomeAssertions;.</p>
<h3>What the 19 rules cover</h3>
<p>Six rules read project files: 8.x packages, floating versions, 7.x pins, versions set in Directory.Packages.props, FluentAssertions.* add-on packages, and the MSBuild global using item. Thirteen read C# test code: the using line, equality, booleans, null and empty, collections, BeEquivalentTo, comparisons, strings, types, Throw/ThrowAsync/NotThrow, WithMessage, AssertionScope, and any Should() call with no 1:1 Shouldly method, which is reported for a hand rewrite instead of guessed.</p>
<p>Two things the check will not pretend. BeEquivalentTo and ShouldBeEquivalentTo can compare objects by different rules, so that finding tells you to run the test after the swap. AssertionScope has no Shouldly type; the finding points at ShouldSatisfyAllConditions.</p>
<h3>Free and full</h3>
<p>The free check runs on the open file in VS Code or on the web page, with no key: one .csproj, Directory.Packages.props, packages.config or test file, every line mapped. The full version scans every project and test file in the workspace at once and exports one dated Markdown migration plan for the licence review ticket. Xceed lists a Fluent Assertions licence from 14.95 dollars a year without support, so the plan is the document a team lead needs either way: stay and license, or move.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku637262_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku637262_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Shouldly Migration Check do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads .csproj, Directory.Packages.props, packages.config and C# test files and lists every FluentAssertions line with the Shouldly line that replaces it, such as total.Should().Be(119.00m) to total.ShouldBe(119.00m). In one xUnit test file it found 6 findings with 19 rules: the using line, Be, HaveCount, NotBeNull, Throw with WithMessage, and BeEquivalentTo.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is Shouldly Migration Check for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">C# developers and .NET team leads at for-profit companies whose unit tests use FluentAssertions. Since 8.0.0 (NuGet 2025-01-14) FluentAssertions is free only for open-source and non-commercial use, so a company test project on 8.x needs a paid Xceed licence or a move to Shouldly (BSD-3-Clause) or AwesomeAssertions (Apache-2.0).</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just use dotnet list package or a chatbot?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">dotnet list package prints versions, never licences, and a range like [7.0.0,) or * silently resolves to the newest 8.x on restore. A chatbot rewrites the lines you paste and can invent Shouldly methods. This check reads your own file, flags floating ranges, and reports a Should() call with no 1:1 Shouldly method for a hand rewrite instead of guessing.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs the licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free with no key: check one open .csproj, Directory.Packages.props, packages.config or C# test file and see every FluentAssertions line with its Shouldly line, in VS Code or on the web page. The licence key scans every project and test file in the workspace at once and exports one dated Markdown migration plan for the licence review ticket.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does staying on FluentAssertions cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Xceed lists a Fluent Assertions licence from 14.95 USD a year without support, on its own blog. Staying on 7.x is free under Apache-2.0 if you pin Version=&quot;[7.2.2,8.0.0)&quot;. Moving to Shouldly 4.3.0 or AwesomeAssertions 9.6.0 costs the rewrite of each assertion line, which this check writes out for you.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">When does the FluentAssertions licence change next?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">No further dated change is announced. FluentAssertions 8.11.0 (NuGet 2026-09-14) is the latest release under the Xceed licence, and 7.2.2 (2026-03-16) is the latest Apache-2.0 release. Any new 7.x patch would stay Apache-2.0; every 8.x release so far carries the commercial-use condition.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Shouldly Migration Check do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads .csproj, Directory.Packages.props, packages.config and C# test files and lists every FluentAssertions line with the Shouldly line that replaces it, such as total.Should().Be(119.00m) to total.ShouldBe(119.00m). In one xUnit test file it found 6 findings with 19 rules: the using line, Be, HaveCount, NotBeNull, Throw with WithMessage, and BeEquivalentTo."}}, {"@type": "Question", "name": "Who is Shouldly Migration Check for?", "acceptedAnswer": {"@type": "Answer", "text": "C# developers and .NET team leads at for-profit companies whose unit tests use FluentAssertions. Since 8.0.0 (NuGet 2025-01-14) FluentAssertions is free only for open-source and non-commercial use, so a company test project on 8.x needs a paid Xceed licence or a move to Shouldly (BSD-3-Clause) or AwesomeAssertions (Apache-2.0)."}}, {"@type": "Question", "name": "Why not just use dotnet list package or a chatbot?", "acceptedAnswer": {"@type": "Answer", "text": "dotnet list package prints versions, never licences, and a range like [7.0.0,) or * silently resolves to the newest 8.x on restore. A chatbot rewrites the lines you paste and can invent Shouldly methods. This check reads your own file, flags floating ranges, and reports a Should() call with no 1:1 Shouldly method for a hand rewrite instead of guessing."}}, {"@type": "Question", "name": "What is free and what needs the licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free with no key: check one open .csproj, Directory.Packages.props, packages.config or C# test file and see every FluentAssertions line with its Shouldly line, in VS Code or on the web page. The licence key scans every project and test file in the workspace at once and exports one dated Markdown migration plan for the licence review ticket."}}, {"@type": "Question", "name": "What does staying on FluentAssertions cost?", "acceptedAnswer": {"@type": "Answer", "text": "Xceed lists a Fluent Assertions licence from 14.95 USD a year without support, on its own blog. Staying on 7.x is free under Apache-2.0 if you pin Version=\"[7.2.2,8.0.0)\". Moving to Shouldly 4.3.0 or AwesomeAssertions 9.6.0 costs the rewrite of each assertion line, which this check writes out for you."}}, {"@type": "Question", "name": "When does the FluentAssertions licence change next?", "acceptedAnswer": {"@type": "Answer", "text": "No further dated change is announced. FluentAssertions 8.11.0 (NuGet 2026-09-14) is the latest release under the Xceed licence, and 7.2.2 (2026-03-16) is the latest Apache-2.0 release. Any new 7.x patch would stay Apache-2.0; every 8.x release so far carries the commercial-use condition."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:637262,slug:"shouldly-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/apache-echarts-migration-check?ref=related"><span class="yj-rel-t">Apache ECharts Migration: Highcharts Licence Map</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cinc-migration-check?ref=related"><span class="yj-rel-t">Cinc Migration Check: Chef Infra licence gate lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/nicegui-migration-check?ref=related"><span class="yj-rel-t">NiceGUI Migration Check: PySimpleGUI Licence Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pekko-migration-gate?ref=related"><span class="yj-rel-t">Pekko Migration Gate — Akka BSL licence check</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/proxmox-migration-check-vmware-vsphere-licence-gate?ref=related"><span class="yj-rel-t">Proxmox Migration Check: VMware vSphere Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pyside6-migration-lint?ref=related"><span class="yj-rel-t">PySide6 Migration Lint: PyQt5/PyQt6 Licence Gate</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section><!--yj-trust-foot-s191--><footer data-yj="yj-trust-foot" style="margin:3rem auto 0;padding:1.2rem 1rem 2rem;border-top:1px solid rgba(128,128,128,.28);max-width:60rem;font:14px/1.6 system-ui,-apple-system,Segoe UI,sans-serif;color:#5b6570;text-align:center"><nav aria-label="ReadyStack" style="display:flex;gap:.4rem 1.1rem;justify-content:center;flex-wrap:wrap"><a href="/about" style="color:inherit">About</a><a href="/verify/" style="color:inherit">How we verify</a><a href="/privacy" style="color:inherit">Privacy</a><a href="/terms" style="color:inherit">Terms</a><a href="/contact.html" style="color:inherit">Contact</a><a href="mailto:support@getreadystack.com" style="color:inherit">support@getreadystack.com</a></nav><p style="margin:.6rem 0 0">&#128274; Secure checkout by Whop &middot; &copy; 2026 ReadyStack</p></footer><!--/yj-trust-foot-s191--></body>
</html>