diff --git a/README.md b/README.md index 6f32358..3685f24 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=aweXpect_Mockolate.Migration&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=aweXpect_Mockolate.Migration) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=aweXpect_Mockolate.Migration&metric=coverage)](https://sonarcloud.io/summary/overall?id=aweXpect_Mockolate.Migration) -A Roslyn analyzer and code-fix provider that migrates [Moq](https://github.com/devlooped/moq) usage to -[Mockolate](https://github.com/aweXpect/Mockolate). Drop the package into a project that uses Moq and the -analyzer flags each mock it can migrate; the accompanying code fix rewrites the setup, verification, and -event APIs to their Mockolate equivalents. +A Roslyn analyzer and code-fix provider that migrates [Moq](https://github.com/devlooped/moq) and +[NSubstitute](https://nsubstitute.github.io) usage to +[Mockolate](https://github.com/aweXpect/Mockolate). Drop the package into a project that uses one of +those libraries and the analyzer flags each mock it can migrate; the accompanying code fix rewrites +the setup, verification, and event APIs to their Mockolate equivalents. ## Installation @@ -19,19 +20,24 @@ dotnet add package Mockolate.Migration ``` The package only needs to be referenced while you are migrating — it ships the analyzer and code fixer, -not runtime code. Once Moq is gone from a project you can remove the reference again. +not runtime code. Once the source library is gone from a project you can remove the reference again. ## Usage -After installing the package, every supported Moq construct (starting from `new Mock()`) is reported -as a warning with diagnostic id **`MockolateM001`** (*Moq should be migrated.*). Apply the code fix -**Migrate Moq to Mockolate** from your IDE (Visual Studio, Rider, VS Code with C# Dev Kit) or via +After installing the package, every supported construct is reported as a warning. Apply the relevant +code fix from your IDE (Visual Studio, Rider, VS Code with C# Dev Kit) or via `dotnet format analyzers` to rewrite the call site. -The fixer rewrites the whole mock — the `new Mock()` construction, all `Setup…` calls on that mock, -the corresponding `Verify…` calls, event wiring, and trailing `.Object` accesses — in a single step. +| Diagnostic | Source library | Code fix title | +|---------------|----------------|---------------------------------| +| `MockolateM001` | Moq | *Migrate Moq to Mockolate* | +| `MockolateM002` | NSubstitute | *Migrate NSubstitute to Mockolate* | -## Supported migrations +The fixer rewrites the whole mock — the construction call, all setup calls on that mock, the +corresponding verification calls, event wiring, and trailing `.Object` accesses (Moq) — in a single +step. + +## Supported Moq migrations | Moq construct | Rewritten to | |----------------------------------------------------------|-----------------------------------------------------------------------------| @@ -57,3 +63,50 @@ the corresponding `Verify…` calls, event wiring, and trailing `.Object` access | `It.IsRegex(pattern, options)` | `It.Matches(pattern).AsRegex(options)` | | `It.Ref.IsAny` / `out` parameters | `It.IsAnyRef()` / `It.IsOut(() => value)` | | Nested mocks (`sut.Setup(m => m.Child.Prop)`) | Navigation chain is preserved: `sut.Child.Mock.Setup.Prop` | + +## Supported NSubstitute migrations + +| NSubstitute construct | Rewritten to | +|--------------------------------------------------------------------|---------------------------------------------------------------------------------------| +| `Substitute.For()` | `IFoo.CreateMock()` | +| `Substitute.For(args)` | `IFoo.CreateMock(args)` | +| `Substitute.For()` | `IFoo.CreateMock().Implementing()` (chains for additional types) | +| `Substitute.ForPartsOf()` | `MyClass.CreateMock()` (Mockolate calls base by default) | +| `Substitute.ForTypeForwardingTo(args)` | `TInterface.CreateMock().Wrapping(new TClass(args))` | +| `sub.Method(args).Returns(v)` | `sub.Mock.Setup.Method(args).Returns(v)` | +| `sub.Method(args).Returns(v1, v2, v3)` | `sub.Mock.Setup.Method(args).Returns(v1).Returns(v2).Returns(v3)` | +| `sub.Method(args).Throws()` / `Throws(ex)` | `sub.Mock.Setup.Method(args).Throws()` / `.Throws(ex)` | +| `sub.Method(args).ReturnsForAnyArgs(v)` | `sub.Mock.Setup.Method(args).AnyParameters().Returns(v)` | +| `sub.Method(args).ThrowsForAnyArgs()` | `sub.Mock.Setup.Method(args).AnyParameters().Throws()` | +| `sub.Method(args).Returns(v).AndDoes(cb)` | `sub.Mock.Setup.Method(args).Returns(v).Do(cb)` | +| `sub.Property.Returns(v)` | `sub.Mock.Setup.Property.Returns(v)` | +| `sub.Received().Method(args)` | `sub.Mock.Verify.Method(args).AtLeastOnce()` | +| `sub.Received(n).Method(args)` | `sub.Mock.Verify.Method(args).Exactly(n)` (`Once()` for `n == 1`) | +| `sub.DidNotReceive().Method(args)` | `sub.Mock.Verify.Method(args).Never()` | +| `sub.ReceivedWithAnyArgs().Method(default, default)` | `sub.Mock.Verify.Method(default, default).AnyParameters().AtLeastOnce()` | +| `sub.DidNotReceiveWithAnyArgs().Method(default, default)` | `sub.Mock.Verify.Method(default, default).AnyParameters().Never()` | +| `_ = sub.Received().Prop` | `sub.Mock.Verify.Prop.Got().AtLeastOnce()` | +| `sub.Received().Prop = v` | `sub.Mock.Verify.Prop.Set(v).AtLeastOnce()` | +| `sub.ClearReceivedCalls()` | `sub.Mock.ClearAllInteractions()` | +| `sub.MyEvent += Raise.Event()` | `sub.Mock.Raise.MyEvent(null, EventArgs.Empty)` | +| `sub.MyEvent += Raise.EventWith(sender, args)` | `sub.Mock.Raise.MyEvent(sender, args)` | +| `sub.MyEvent += Raise.EventWith(args)` | `sub.Mock.Raise.MyEvent(null, args)` | +| `sub.MyEvent += Raise.Event(args...)` | `sub.Mock.Raise.MyEvent(args...)` (delegate type dropped) | +| `sub.When(x => x.M(args)).Do(cb)` | `sub.Mock.Setup.M(args).Do(cb)` | +| `sub.When(x => x.M(args)).DoNotCallBase()` | `sub.Mock.Setup.M(args).SkippingBaseClass()` | +| `Arg.Any()` | `It.IsAny()` | +| `Arg.Is(predicate)` | `It.Satisfies(predicate)` | +| `Arg.Is(value)` / `Arg.Is(value)` | `It.Is(value)` / `It.Is(value)` | +| `Arg.Compat.X(...)` | same as the corresponding `Arg.X(...)` | +| Nested mocks (`sub.Child.M(args).Returns(v)`) | `sub.Child.Mock.Setup.M(args).Returns(v)` plus a `// TODO` comment to register `Child` | + +## Argument arity + +Mockolate exposes both a direct-value overload and a matcher overload for properties and for methods with up to +**four** parameters; methods with five or more parameters only expose the matcher overload. The migration rewrites +existing matcher expressions, but otherwise preserves the argument expressions written by the user: + +- Plain values are kept as plain values; the migration does not currently wrap them in `It.Is(value)` based on + method arity. +- Existing `It.Is(...)` / `Arg.Is(...)` matchers are always migrated to `It.Is(...)` and never collapsed back to a + bare value, even on properties or low-arity methods.