├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── sonarcloud.yml ├── .gitignore ├── MoqToNSubstitute.Tests ├── Conversion │ ├── MoqToNSubstituteConverterTests.cs │ └── MoqToNSubstituteTransformerTests.cs ├── Extensions │ └── SyntaxNodeExtensionsTests.cs ├── Helpers │ ├── FileIO.cs │ ├── FileIOTests.cs │ └── Validation.cs ├── MoqToNSubstitute.Tests.csproj ├── ProgramTests.cs ├── Resources │ ├── Moq │ │ ├── ArgumentSample.cs │ │ ├── AssignmentSample.cs │ │ ├── FieldsWithAssignment.cs │ │ ├── SetupAndVerify.cs │ │ └── VariableSample.cs │ ├── NSubstitute │ │ ├── ArgumentSampleReplaced.cs │ │ ├── AssignmentSampleReplaced.cs │ │ ├── FieldsWithAssignmentReplaced.cs │ │ ├── SetupAndVerifyReplaced.cs │ │ └── VariableSampleReplaced.cs │ └── TaxServiceTests.cs ├── Syntax │ └── CustomSyntaxRewriterTests.cs └── Usings.cs ├── MoqToNSubstitute.sln ├── MoqToNSubstitute ├── Conversion │ ├── ICodeConverter.cs │ ├── ICodeTransformer.cs │ ├── MoqToNSubstituteConverter.cs │ └── MoqToNSubstituteTransformer.cs ├── Extensions │ └── SyntaxNodeExtensions.cs ├── Models │ ├── CodeSyntax.cs │ └── Expression.cs ├── MoqToNSubstitute.csproj ├── Program.cs ├── Syntax │ └── CustomSyntaxRewriter.cs ├── Templates │ └── ReplacementTemplate.cs └── Utilities │ ├── DotNetPackageManager.cs │ ├── IPackageManager.cs │ └── Logger.cs ├── README.md └── license.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud Analysis 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | jobs: 9 | build: 10 | name: Build and analyze 11 | runs-on: windows-latest 12 | steps: 13 | - name: Build 14 | run: | 15 | dotnet tool install --global dotnet-coverage 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: 17 20 | distribution: 'zulu' # Alternative distribution options are available. 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 24 | - name: Cache SonarQube Cloud packages 25 | uses: actions/cache@v4 26 | with: 27 | path: ~\sonar\cache 28 | key: ${{ runner.os }}-sonar 29 | restore-keys: ${{ runner.os }}-sonar 30 | - name: Cache SonarQube Cloud scanner 31 | id: cache-sonar-scanner 32 | uses: actions/cache@v4 33 | with: 34 | path: .\.sonar\scanner 35 | key: ${{ runner.os }}-sonar-scanner 36 | restore-keys: ${{ runner.os }}-sonar-scanner 37 | - name: Install SonarQube Cloud scanner 38 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' 39 | shell: powershell 40 | run: | 41 | New-Item -Path .\.sonar\scanner -ItemType Directory 42 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner 43 | - name: Build and analyze 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 46 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 47 | shell: powershell 48 | run: | 49 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"SilverLiningComputing_MoqToNSubstituteConverter" /o:"slc" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml 50 | dotnet build MoqToNSubstitute.sln /t:Rebuild /p:Configuration=Release 51 | dotnet test MoqToNSubstitute.sln -c:Release 52 | dotnet-coverage collect 'dotnet test' -f xml -o 'coverage.xml' 53 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | code/.sonarlint/ 263 | /code/DocutechApi/DocutechApi.ruleset 264 | /code/DocutechApi/DocutechApi-1.ruleset 265 | .dccache 266 | # Local .terraform directories 267 | **/.terraform/* 268 | */.terraform/* 269 | 270 | # .tfstate files 271 | *.tfstate 272 | *.tfstate.* 273 | **.tfstate 274 | **.tfstate.* 275 | 276 | # Crash log files 277 | crash.log 278 | **crash.log 279 | *.log 280 | 281 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 282 | # .tfvars files are managed as part of configuration and so should be included in 283 | # version control. 284 | # 285 | # example.tfvars 286 | secrets.tfvars 287 | **secrets.tfvars 288 | secrets.tf 289 | **secrets.tf 290 | 291 | # Ignore override files as they are usually used to override resources locally and so 292 | # are not checked in 293 | override.tf 294 | override.tf.json 295 | *_override.tf 296 | **_override.tf 297 | *_override.tf.json 298 | **_override.tf.json 299 | 300 | # Include override files you do wish to add to version control using negated pattern 301 | # 302 | # !example_override.tf 303 | 304 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 305 | *tfplan* 306 | 307 | /.sonarlint/* 308 | /.vscode/* 309 | *.terraform.lock.hcl 310 | **/Live/* 311 | .template.config/ 312 | -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Conversion/MoqToNSubstituteConverterTests.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Conversion; 2 | 3 | [TestClass] 4 | public class MoqToNSubstituteConverterTests 5 | { 6 | [TestMethod] 7 | [DataRow("", false)] 8 | [DataRow("", true)] 9 | public void Test_Convert(string path, bool transform) 10 | { 11 | var packageManager = Substitute.For(); 12 | var codeTransformer = Substitute.For(); 13 | var moqToNSubstituteConverter = new MoqToNSubstituteConverter 14 | { 15 | PackageManager = packageManager, 16 | CodeTransformer = codeTransformer 17 | }; 18 | moqToNSubstituteConverter.Convert(path, transform); 19 | if (transform) 20 | { 21 | packageManager.DidNotReceive().Install(Arg.Any(), Arg.Is(x => x == "NSubstitute")); 22 | packageManager.DidNotReceive().Uninstall(Arg.Any(), Arg.Any()); 23 | } 24 | else 25 | { 26 | packageManager.DidNotReceive().Install(Arg.Any(), Arg.Any()); 27 | packageManager.DidNotReceive().Uninstall(Arg.Any(), Arg.Any()); 28 | } 29 | codeTransformer.DidNotReceive().Transform(Arg.Any(), transform); 30 | 31 | } 32 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Conversion/MoqToNSubstituteTransformerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp; 2 | 3 | namespace MoqToNSubstitute.Tests.Conversion; 4 | 5 | [TestClass] 6 | public class MoqToNSubstituteTransformerTests 7 | { 8 | private string? _fileContents; 9 | private Assembly? _assembly; 10 | private CodeSyntax? _substitutions; 11 | 12 | [TestInitialize] 13 | public void TestInitialize() 14 | { 15 | _assembly = Assembly.GetExecutingAssembly(); 16 | _substitutions = ReplacementTemplate.ReturnReplacementSyntax(); 17 | 18 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("VariableSample.cs")); 19 | Assert.IsFalse(string.IsNullOrEmpty(resourceName)); 20 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 21 | } 22 | 23 | [TestMethod] 24 | public void Test_GetNodes() 25 | { 26 | Assert.IsNotNull(_fileContents); 27 | MoqToNSubstituteTransformer.GetNodeTypesFromString(_fileContents); 28 | } 29 | 30 | [TestMethod] 31 | public void Test_ReplaceArgumentNodes() 32 | { 33 | Assert.IsNotNull(_assembly); 34 | Assert.IsNotNull(_substitutions); 35 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("ArgumentSample.cs")); 36 | var fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 37 | var tree = CSharpSyntaxTree.ParseText(fileContents); 38 | var root = tree.GetRoot(); 39 | var testNode = root.ReplaceArgumentNodes(_substitutions, ".Object", "It.IsAny", "It.Is"); 40 | Assert.IsNotNull(testNode); 41 | resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("ArgumentSampleReplaced.cs")); 42 | fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 43 | Assert.IsNotNull(fileContents); 44 | Assert.AreEqual(fileContents, testNode.ToString()); 45 | } 46 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Extensions/SyntaxNodeExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | 4 | namespace MoqToNSubstitute.Tests.Extensions; 5 | 6 | [TestClass] 7 | public class SyntaxNodeExtensionsTests 8 | { 9 | private static Assembly? _assembly; 10 | private static string? _fileContents; 11 | private static CodeSyntax? _substitutions; 12 | 13 | [ClassInitialize] 14 | public static void Initialize(TestContext context) 15 | { 16 | _ = context; 17 | _substitutions = ReplacementTemplate.ReturnReplacementSyntax(); 18 | _assembly = Assembly.GetExecutingAssembly(); 19 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("TaxServiceTests.cs")); 20 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 21 | } 22 | 23 | [TestMethod] 24 | public void Test_GetNodes() 25 | { 26 | Assert.IsNotNull(_fileContents); 27 | var tree = CSharpSyntaxTree.ParseText(_fileContents); 28 | var root = tree.GetRoot(); 29 | 30 | var assignmentExpressionSyntaxCollection = root.GetNodes("new Mock"); 31 | Assert.IsNotNull(assignmentExpressionSyntaxCollection); 32 | var assignmentExpressionSyntaxArray = assignmentExpressionSyntaxCollection.ToArray(); 33 | Assert.IsNotNull(assignmentExpressionSyntaxArray); 34 | Assert.AreEqual(1, assignmentExpressionSyntaxArray.Length); 35 | } 36 | 37 | [TestMethod] 38 | public void Test_ReplaceVariableNodes() 39 | { 40 | Assert.IsNotNull(_assembly); 41 | Assert.IsNotNull(_substitutions); 42 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("VariableSample.cs")); 43 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 44 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 45 | var tree = CSharpSyntaxTree.ParseText(_fileContents); 46 | var root = tree.GetRoot(); 47 | var rootFields = root.ReplaceVariableNodes(_substitutions, "Mock"); 48 | var rootObject = rootFields.ReplaceObjectCreationNodes(_substitutions, "Mock"); 49 | var rootReplaced = rootObject.ReplaceArgumentNodes(_substitutions, ".Object"); 50 | resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("VariableSampleReplaced.cs")); 51 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 52 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 53 | Assert.AreEqual(_fileContents, rootReplaced.ToString()); 54 | } 55 | 56 | [TestMethod] 57 | public void Test_ReplaceAssignmentNodes() 58 | { 59 | Assert.IsNotNull(_assembly); 60 | Assert.IsNotNull(_substitutions); 61 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("AssignmentSample.cs")); 62 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 63 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 64 | var tree = CSharpSyntaxTree.ParseText(_fileContents); 65 | var root = tree.GetRoot(); 66 | var rootFields = root.ReplaceAssignmentNodes(_substitutions, "new Mock"); 67 | resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("AssignmentSampleReplaced.cs")); 68 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 69 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 70 | Assert.AreEqual(_fileContents, rootFields.ToString()); 71 | } 72 | 73 | [TestMethod] 74 | public void Test_replace_fields_with_assignment_nodes() 75 | { 76 | Assert.IsNotNull(_assembly); 77 | Assert.IsNotNull(_substitutions); 78 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("FieldsWithAssignment.cs")); 79 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 80 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 81 | var tree = CSharpSyntaxTree.ParseText(_fileContents); 82 | var root = tree.GetRoot(); 83 | var rootFields = root.ReplaceVariableNodes(_substitutions, "Mock<"); 84 | var rootNewObject = rootFields.ReplaceObjectCreationNodes(_substitutions, "Mock"); 85 | resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("FieldsWithAssignmentReplaced.cs")); 86 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 87 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 88 | Assert.AreEqual(_fileContents, rootNewObject.ToString()); 89 | } 90 | 91 | [TestMethod] 92 | public void Test_setup_and_verify() 93 | { 94 | Assert.IsNotNull(_assembly); 95 | Assert.IsNotNull(_substitutions); 96 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("SetupAndVerify.cs")); 97 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 98 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 99 | var tree = CSharpSyntaxTree.ParseText(_fileContents); 100 | var root = tree.GetRoot(); 101 | var rootExpression = root.ReplaceExpressionNodes(_substitutions, ".Setup(", ".Verify("); 102 | resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("SetupAndVerifyReplaced.cs")); 103 | _fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 104 | Assert.IsFalse(string.IsNullOrEmpty(_fileContents)); 105 | Assert.AreEqual(_fileContents, rootExpression.ToString()); 106 | } 107 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Helpers/FileIO.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Helpers; 2 | 3 | internal static class FileIO 4 | { 5 | public static string ReadFileFromEmbeddedResources(string resourceName) 6 | { 7 | var assembly = Assembly.GetExecutingAssembly(); 8 | using var stream = assembly.GetManifestResourceStream(resourceName); 9 | if (stream == null) return ""; 10 | using var reader = new StreamReader(stream); 11 | return reader.ReadToEnd(); 12 | } 13 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Helpers/FileIOTests.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Helpers; 2 | 3 | [TestClass] 4 | public class FileIOTests 5 | { 6 | [TestMethod] 7 | public void Test_ReadFileFromEmbeddedResources() 8 | { 9 | var assembly = Assembly.GetExecutingAssembly(); 10 | var resourceName = assembly.GetManifestResourceNames().Single(n => n.EndsWith("TaxServiceTests.cs")); 11 | var fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 12 | Assert.IsFalse(string.IsNullOrEmpty(fileContents)); 13 | } 14 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Helpers/Validation.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Helpers 2 | { 3 | /// 4 | /// Unit test validation methods 5 | /// 6 | public static class Validation 7 | { 8 | /// 9 | /// Takes 2 objects of type T and makes sure the ValueTypes are equal 10 | /// 11 | /// The type of object to compare 12 | /// The object with the expected values 13 | /// The object with the actual values 14 | /// True if the objects are the same, false otherwise 15 | public static bool Validate(T expected, T actual) where T : class? 16 | { 17 | var properties = typeof(T).GetProperties(); 18 | foreach (var property in properties) 19 | { 20 | var expectedValue = property.GetValue(expected); 21 | var actualValue = property.GetValue(actual); 22 | 23 | if (expectedValue != null && !expectedValue.GetType().IsValueType) 24 | { 25 | Validate(expectedValue, actualValue); 26 | } 27 | else 28 | { 29 | Assert.AreEqual(expectedValue, actualValue); 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/MoqToNSubstitute.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 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 | all 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests; 2 | 3 | [TestClass] 4 | public class ProgramTests 5 | { 6 | private MethodInfo? _entryPoint; 7 | 8 | [TestInitialize] 9 | public void TestInitialize() 10 | { 11 | Program.MoqToNSubstituteConverter = Substitute.For(); 12 | Program.MoqToNSubstituteConverter.Convert(Arg.Any(), Arg.Any()); 13 | _entryPoint = typeof(Program).Assembly.EntryPoint!; 14 | } 15 | 16 | [TestMethod] 17 | public void Test_Main_no_arguments() 18 | { 19 | Assert.IsNotNull(_entryPoint); 20 | _entryPoint.Invoke(null, new object[] { Array.Empty() }); 21 | Program.MoqToNSubstituteConverter.Received(1).Convert(); 22 | } 23 | 24 | [TestMethod] 25 | [DataRow("false")] 26 | [DataRow("C:\\folder\\")] 27 | public void Test_Main_one_argument(string arg) 28 | { 29 | Assert.IsNotNull(_entryPoint); 30 | _entryPoint.Invoke(null, new object[] { new[] { arg } }); 31 | if (bool.TryParse(arg, out var transform)) 32 | { 33 | Program.MoqToNSubstituteConverter.Received(1).Convert(Arg.Is(x => x == ""), Arg.Is(x => x == transform)); 34 | } 35 | else 36 | { 37 | Program.MoqToNSubstituteConverter.Received(1).Convert(Arg.Is(x => x == arg)); 38 | } 39 | } 40 | 41 | [TestMethod] 42 | [DataRow("true", "c:\\folder\\code\\", "c:\\folder\\code\\", true)] 43 | [DataRow("false", "c:\\folder\\code\\", "c:\\folder\\code\\", false)] 44 | [DataRow("c:\\folder\\code\\", "true", "c:\\folder\\code\\", true)] 45 | [DataRow("c:\\folder\\code\\", "false", "c:\\folder\\code\\", false)] 46 | [DataRow("false", "false", null, false)] 47 | [DataRow("true", "false", null, true)] 48 | [DataRow("false", "true", null, false)] 49 | [DataRow("true", "", null, true)] 50 | [DataRow("", "true", null, true)] 51 | [DataRow("", "", null, null)] 52 | [DataRow("c:\\folder\\code\\", "c:\\folder\\", "c:\\folder\\code\\", null)] 53 | public void Text_Main_with_two_arguments(string arg1, string arg2, string? param1, bool? param2) 54 | { 55 | Assert.IsNotNull(_entryPoint); 56 | var args = new[] { arg1, arg2 }; 57 | _entryPoint.Invoke(null, new object[] { args }); 58 | 59 | if (param1 != null && param2 != null) 60 | { 61 | Program.MoqToNSubstituteConverter.Received(1).Convert(Arg.Is(x => x == param1), Arg.Is(x => x == param2)); 62 | } 63 | 64 | if (param1 == null && param2 != null) 65 | { 66 | Program.MoqToNSubstituteConverter.Received(1).Convert(Arg.Is(x => x == ""), Arg.Is(x => x == param2)); 67 | } 68 | 69 | if (param1 == null && param2 == null) 70 | { 71 | Program.MoqToNSubstituteConverter.Received(1).Convert(); 72 | } 73 | 74 | if (param1 != null && param2 == null) 75 | { 76 | Program.MoqToNSubstituteConverter.Received(1).Convert(Arg.Is(x => x == param1)); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/Moq/ArgumentSample.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Resources.Moq 2 | { 3 | [TestClass] 4 | public class ArgumentSample 5 | { 6 | private SpectralService _plotFactoryMock; 7 | 8 | [TestMethod] 9 | public void Test_argument_replacement() 10 | { 11 | _spectralService = new SpectralService(_logger, _plotFactoryMock.Object); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/Moq/AssignmentSample.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class AssignmentSample 5 | { 6 | [TestMethod] 7 | public void Test_Assignments() 8 | { 9 | _testClass = new Mock(); 10 | _testClass = new Mock(_mockClass.Object, _realClass); 11 | _testClass = new Mock(_mockClass.Object, _realClass) { CallBase = true }; 12 | _testClass = new Mock() { CallBase = true }; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/Moq/FieldsWithAssignment.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class VariableSample 5 | { 6 | private Mock _testClassMock = new Mock(); 7 | private Mock? _testClassMock = new Mock(); 8 | } 9 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/Moq/SetupAndVerify.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MoqToNSubstitute.Tests.Resources.Moq 4 | { 5 | internal class SetupAndVerify 6 | { 7 | [TestMethod] 8 | public void Test_setup() 9 | { 10 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).Returns(0); 11 | _classMock.Setup(mock => mock.Setup(It.IsAny(), It.IsAny())).Returns(0); 12 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).ReturnsAsync(0); 13 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())) 14 | .ReturnsAsync(0); 15 | _classMock.Setup(x => x.Setup( 16 | It.IsAny(), 17 | It.IsAny())) 18 | .ReturnsAsync(0); 19 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).ThrowsAsync(new Exception()); 20 | _classMock.Setup(x => x.Setup(It.IsAny(), It.Is(p => p == 4))).ThrowsAsync(new Exception()); 21 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).Throws(new Exception); 22 | _classMock.Setup(x => x.Setup()).Returns(0).Verifiable(); 23 | _classMock.SetupGet(_ => _.Value).Returns(12); 24 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())); 25 | _classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).Result.Returns(0); 26 | } 27 | 28 | [TestMethod] 29 | public void Test_verify() 30 | { 31 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Once); 32 | _classMock.Verify(mock => mock.Setup(It.IsAny(), It.IsAny()), Times.Exactly(2)); 33 | _classMock 34 | .Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Once); 35 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Never); 36 | _classMock.Verify(x => x.Setup( 37 | It.IsAny(), 38 | It.IsAny()), Times.Once); 39 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Exactly(3)); 40 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Exactly(4)); 41 | _classMock.Verify(x => x.Setup(), Times.Once); 42 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Never); 43 | _classMock.Verify(x => x.Setup(It.IsAny(), It.IsAny()), Times.Once); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/Moq/VariableSample.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class VariableSample 5 | { 6 | private Mock _testClassMock; 7 | private Mock? _testClassMock; 8 | private Mock _testClassMock = new(); 9 | private Mock? _testClassMock = new(); 10 | 11 | [TestMethod] 12 | public void Test_Assignments() 13 | { 14 | Mock testClass = new(); 15 | Mock testClass = new Mock(); 16 | Mock testClass = new Mock(_mockClass.Object, _realClass); 17 | Mock testClass = new Mock(_mockClass?.Object, _realClass); 18 | Mock testClass = new Mock(_mockClass.Object, _realClass) { CallBase = true }; 19 | var testClass = new Mock(); 20 | var testClass = new Mock(_mockClass.Object, _realClass); 21 | var testClass = new Mock(_mockClass.Object, _realClass) { CallBase = true }; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/NSubstitute/ArgumentSampleReplaced.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests.Resources.Moq 2 | { 3 | [TestClass] 4 | public class ArgumentSample 5 | { 6 | private SpectralService _plotFactoryMock; 7 | 8 | [TestMethod] 9 | public void Test_argument_replacement() 10 | { 11 | _spectralService = new SpectralService(_logger, _plotFactoryMock); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/NSubstitute/AssignmentSampleReplaced.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class AssignmentSample 5 | { 6 | [TestMethod] 7 | public void Test_Assignments() 8 | { 9 | _testClass = Substitute.For(); 10 | _testClass = Substitute.For(_mockClass.Object, _realClass); 11 | _testClass = Substitute.ForPartsOf(_mockClass.Object, _realClass) ; 12 | _testClass = Substitute.ForPartsOf() ; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/NSubstitute/FieldsWithAssignmentReplaced.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class VariableSample 5 | { 6 | private ITestClass _testClassMock = Substitute.For(); 7 | private ITestClass? _testClassMock = Substitute.For(); 8 | } 9 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/NSubstitute/SetupAndVerifyReplaced.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MoqToNSubstitute.Tests.Resources.Moq 4 | { 5 | internal class SetupAndVerify 6 | { 7 | [TestMethod] 8 | public void Test_setup() 9 | { 10 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 11 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 12 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 13 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 14 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 15 | _classMock.Setup(Arg.Any(), Arg.Any()).Throws(new Exception()); 16 | _classMock.Setup(Arg.Any(), Arg.Is(p => p == 4)).Throws(new Exception()); 17 | _classMock.Setup(Arg.Any(), Arg.Any()).Throws(new Exception); 18 | _classMock.Setup().Returns(0); 19 | _classMock.SetupGet(_ => _.Value).Returns(12); 20 | _classMock.Setup(Arg.Any(), Arg.Any().Throws); 21 | _classMock.Setup(Arg.Any(), Arg.Any()).Returns(0); 22 | } 23 | 24 | [TestMethod] 25 | public void Test_verify() 26 | { 27 | _classMock.Received(1).Setup(Arg.Any(), Arg.Any()); 28 | _classMock.Received(2).Setup(Arg.Any(), Arg.Any()); 29 | _classMock.Received(1).Setup(Arg.Any(), Arg.Any()); 30 | _classMock.DidNotReceive().Setup(Arg.Any(), Arg.Any()); 31 | _classMock.Received(1).Setup(Arg.Any(),Arg.Any()); 32 | _classMock.Received(3).Setup(Arg.Any(), Arg.Any()); 33 | _classMock.Received(4).Setup(Arg.Any(), Arg.Any()); 34 | _classMock.Received(1).Setup(); 35 | _classMock.DidNotReceive().Setup(Arg.Any(), Arg.Any()); 36 | _classMock.Received(1).Setup(Arg.Any(), Arg.Any()); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/NSubstitute/VariableSampleReplaced.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Tests 2 | { 3 | [TestClass] 4 | public class VariableSample 5 | { 6 | private ITestClass _testClassMock; 7 | private ITestClass? _testClassMock; 8 | private ITestClass _testClassMock = new(); 9 | private ITestClass? _testClassMock = new(); 10 | 11 | [TestMethod] 12 | public void Test_Assignments() 13 | { 14 | ITestClass testClass = new(); 15 | ITestClass testClass = Substitute.For(); 16 | ITestClass testClass = Substitute.For(_mockClass, _realClass); 17 | ITestClass testClass = Substitute.For(_mockClass, _realClass); 18 | ITestClass testClass = Substitute.ForPartsOf(_mockClass, _realClass); 19 | var testClass = Substitute.For(); 20 | var testClass = Substitute.For(_mockClass, _realClass); 21 | var testClass = Substitute.ForPartsOf(_mockClass, _realClass); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Resources/TaxServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Calculator.Services; 2 | 3 | namespace Calculator.Tests.Services 4 | { 5 | [TestClass] 6 | public class TaxServiceTests 7 | { 8 | private Mock? _calculatorServiceMock; 9 | 10 | [TestInitialize] 11 | public void TestInitialize() 12 | { 13 | _calculatorServiceMock = new Mock(); 14 | _calculatorServiceMock.Setup(x => x.Divide(It.IsAny(), It.IsAny())).Returns(0.5); 15 | } 16 | 17 | [TestMethod] 18 | public void Test_calculate_taxes() 19 | { 20 | var taxService = new TaxService(new CalculatorService()); 21 | var taxPayment = taxService.CalculateTaxes(32, 2500); 22 | Assert.AreEqual(800, taxPayment); 23 | } 24 | 25 | [TestMethod] 26 | public void Test_calculate_taxes_unit_test() 27 | { 28 | Assert.IsNotNull(_calculatorServiceMock); 29 | var taxService = new TaxService(_calculatorServiceMock.Object); 30 | var taxPayment = taxService.CalculateTaxes(50, 2500); 31 | Assert.AreEqual(1250, taxPayment); 32 | _calculatorServiceMock.Verify(x => x.Divide(It.IsAny(), It.IsAny()), Times.Once); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Syntax/CustomSyntaxRewriterTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using MoqToNSubstitute.Syntax; 5 | 6 | namespace MoqToNSubstitute.Tests.Syntax; 7 | 8 | [TestClass] 9 | public class CustomSyntaxRewriterTests 10 | { 11 | private static CustomSyntaxRewriter? _customSyntaxRewriter; 12 | private static SyntaxNode? _root; 13 | private static Assembly? _assembly; 14 | 15 | [ClassInitialize] 16 | public static void ClassInitialize(TestContext context) 17 | { 18 | _ = context; 19 | var substitutions = ReplacementTemplate.ReturnReplacementSyntax(); 20 | _customSyntaxRewriter = new CustomSyntaxRewriter(substitutions); 21 | _assembly = Assembly.GetExecutingAssembly(); 22 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("TaxServiceTests.cs")); 23 | var fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 24 | var tree = CSharpSyntaxTree.ParseText(fileContents); 25 | _root = tree.GetRoot(); 26 | } 27 | 28 | [TestMethod] 29 | public void Test_VisitObjectCreationExpression() 30 | { 31 | Assert.IsNotNull(_root); 32 | Assert.IsNotNull(_customSyntaxRewriter); 33 | var testNode = _root.GetNodes("Mock").FirstOrDefault(); 34 | Assert.IsNotNull(testNode); 35 | Assert.AreEqual("new Mock()", testNode.ToString()); 36 | var replacementNode = _customSyntaxRewriter.VisitObjectCreationExpression(testNode); 37 | Assert.IsNotNull(replacementNode); 38 | Assert.AreEqual("Substitute.For()", replacementNode.ToString()); 39 | } 40 | 41 | [TestMethod] 42 | public void Test_VisitArgument() 43 | { 44 | Assert.IsNotNull(_root); 45 | Assert.IsNotNull(_customSyntaxRewriter); 46 | var testNode = _root.GetNodes(".Object").FirstOrDefault(); 47 | Assert.IsNotNull(testNode); 48 | Assert.AreEqual("_calculatorServiceMock.Object", testNode.ToString()); 49 | var replacementNode = _customSyntaxRewriter.VisitArgument(testNode); 50 | Assert.IsNotNull(replacementNode); 51 | Assert.AreEqual("_calculatorServiceMock", replacementNode.ToString()); 52 | } 53 | 54 | [TestMethod] 55 | [DataRow(true)] 56 | [DataRow(false)] 57 | public void Test_VisitVariableDeclaration(bool isRegEx) 58 | { 59 | Assert.IsNotNull(_customSyntaxRewriter); 60 | const string node = "Mock testClass = new Mock();"; 61 | var tree = CSharpSyntaxTree.ParseText(node); 62 | var root = tree.GetRoot(); 63 | var testNode = root.GetNodes("Mock").FirstOrDefault(); 64 | Assert.IsNotNull(testNode); 65 | Assert.AreEqual("Mock testClass = new Mock()", testNode.ToString()); 66 | if (isRegEx) 67 | { 68 | var replacementNode = _customSyntaxRewriter.VisitVariableDeclaration(testNode); 69 | Assert.IsNotNull(replacementNode); 70 | Assert.AreEqual("ITestClass testClass = new Mock()", replacementNode.ToString()); 71 | } 72 | else 73 | { 74 | var substitutions = new CodeSyntax 75 | { 76 | VariableType = new List 77 | { 78 | new("Mock<", "", false) 79 | } 80 | }; 81 | var customSyntaxRewriter = new CustomSyntaxRewriter(substitutions); 82 | var replacementNode = customSyntaxRewriter.VisitVariableDeclaration(testNode); 83 | Assert.IsNotNull(replacementNode); 84 | Assert.AreEqual("ITestClass> testClass = new Mock()", replacementNode.ToString()); 85 | } 86 | } 87 | 88 | [TestMethod] 89 | public void Test_VisitAssignmentExpression() 90 | { 91 | Assert.IsNotNull(_customSyntaxRewriter); 92 | const string node = "_testClass = new Mock(_mockClass.Object, _realClass);"; 93 | var tree = CSharpSyntaxTree.ParseText(node); 94 | var root = tree.GetRoot(); 95 | var testNode = root.GetNodes("Mock").FirstOrDefault(); 96 | Assert.IsNotNull(testNode); 97 | Assert.AreEqual("_testClass = new Mock(_mockClass.Object, _realClass)", testNode.ToString()); 98 | var replacementNode = _customSyntaxRewriter.VisitAssignmentExpression(testNode); 99 | Assert.IsNotNull(replacementNode); 100 | Assert.AreEqual("_testClass = Substitute.For", replacementNode.ToString()); 101 | } 102 | 103 | [TestMethod] 104 | public void Test_VisitExpressionStatement() 105 | { 106 | Assert.IsNotNull(_customSyntaxRewriter); 107 | Assert.IsNotNull(_assembly); 108 | var resourceName = _assembly.GetManifestResourceNames().Single(n => n.EndsWith("SetupAndVerify.cs")); 109 | var fileContents = FileIO.ReadFileFromEmbeddedResources(resourceName); 110 | var tree = CSharpSyntaxTree.ParseText(fileContents); 111 | var root = tree.GetRoot(); 112 | var testNode = root.GetNodes(".Setup").FirstOrDefault(); 113 | Assert.IsNotNull(testNode); 114 | Assert.AreEqual("_classMock.Setup(x => x.Setup(It.IsAny(), It.IsAny())).Returns(0);", testNode.ToString()); 115 | var replacementNode = _customSyntaxRewriter.VisitExpressionStatement(testNode); 116 | Assert.IsNotNull(replacementNode); 117 | Assert.AreEqual("_classMock.Setup(Arg.Any(), Arg.Any()).Returns(0);", replacementNode.ToString()); 118 | } 119 | } -------------------------------------------------------------------------------- /MoqToNSubstitute.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | global using MoqToNSubstitute.Conversion; 3 | global using MoqToNSubstitute.Extensions; 4 | global using MoqToNSubstitute.Models; 5 | global using MoqToNSubstitute.Templates; 6 | global using MoqToNSubstitute.Tests.Helpers; 7 | global using MoqToNSubstitute.Utilities; 8 | global using NSubstitute; 9 | global using System.Reflection; 10 | -------------------------------------------------------------------------------- /MoqToNSubstitute.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33122.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MoqToNSubstitute", "MoqToNSubstitute\MoqToNSubstitute.csproj", "{AB1125BB-05F2-4CB6-8979-A33554E8CF36}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MoqToNSubstitute.Tests", "MoqToNSubstitute.Tests\MoqToNSubstitute.Tests.csproj", "{872E9800-37E7-4C16-8C23-A29470E59CAA}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {AB1125BB-05F2-4CB6-8979-A33554E8CF36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {AB1125BB-05F2-4CB6-8979-A33554E8CF36}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {AB1125BB-05F2-4CB6-8979-A33554E8CF36}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {AB1125BB-05F2-4CB6-8979-A33554E8CF36}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {872E9800-37E7-4C16-8C23-A29470E59CAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {872E9800-37E7-4C16-8C23-A29470E59CAA}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {872E9800-37E7-4C16-8C23-A29470E59CAA}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {872E9800-37E7-4C16-8C23-A29470E59CAA}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {57B5FFA5-C695-4C6F-B028-A52D1F354E6F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /MoqToNSubstitute/Conversion/ICodeConverter.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Conversion; 2 | 3 | internal interface ICodeConverter 4 | { 5 | void Convert(string path = "", bool transform = false); 6 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Conversion/ICodeTransformer.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Conversion; 2 | 3 | internal interface ICodeTransformer 4 | { 5 | void Transform(string sourceFilePath, bool transform = false); 6 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Conversion/MoqToNSubstituteConverter.cs: -------------------------------------------------------------------------------- 1 | using MoqToNSubstitute.Utilities; 2 | 3 | namespace MoqToNSubstitute.Conversion; 4 | 5 | internal class MoqToNSubstituteConverter : ICodeConverter 6 | { 7 | internal IPackageManager PackageManager = new DotNetPackageManager(); 8 | internal ICodeTransformer CodeTransformer = new MoqToNSubstituteTransformer(); 9 | 10 | public void Convert(string path = "", bool transform = false) 11 | { 12 | var solutionDir = string.IsNullOrEmpty(path) 13 | ? Directory.GetCurrentDirectory() 14 | : Path.GetFullPath(path); 15 | if (!Directory.Exists(solutionDir)) 16 | { 17 | Logger.Log("The directory does not exist"); 18 | return; 19 | } 20 | var csFiles = Directory.GetFiles(solutionDir, "*.cs", SearchOption.AllDirectories); 21 | var csprojFiles = Directory.GetFiles(solutionDir, "*.csproj", SearchOption.AllDirectories); 22 | 23 | if (transform) 24 | { 25 | Logger.Log("Processing project files..."); 26 | 27 | foreach (var projectFile in csprojFiles) 28 | { 29 | Logger.Log($"Installing NSubstitute to {projectFile}"); 30 | PackageManager.Install(projectFile, "NSubstitute"); 31 | } 32 | } 33 | 34 | Logger.Log("Processing source files..."); 35 | foreach (var sourceFile in csFiles) 36 | { 37 | Logger.Log(transform ? $"Transforming {sourceFile}" : $"Analyzing {sourceFile}"); 38 | CodeTransformer.Transform(sourceFile, transform); 39 | } 40 | Logger.Log(transform ? "Completed transformations." : "Completed analysis"); 41 | } 42 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Conversion/MoqToNSubstituteTransformer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using MoqToNSubstitute.Extensions; 4 | using MoqToNSubstitute.Templates; 5 | using MoqToNSubstitute.Utilities; 6 | using System.Runtime.CompilerServices; 7 | 8 | [assembly: InternalsVisibleTo("MoqToNSubstitute.Tests")] 9 | 10 | namespace MoqToNSubstitute.Conversion; 11 | 12 | internal class MoqToNSubstituteTransformer : ICodeTransformer 13 | { 14 | public void Transform(string sourceFilePath, bool transform = false) 15 | { 16 | var substitutions = ReplacementTemplate.ReturnReplacementSyntax(); 17 | var sourceText = File.ReadAllText(sourceFilePath); 18 | 19 | if (!sourceText.Contains("Mock")) return; 20 | 21 | var tree = CSharpSyntaxTree.ParseText(sourceText); 22 | var root = tree.GetRoot(); 23 | 24 | var rootObject = root.ReplaceArgumentNodes(substitutions, ".Object", "It.IsAny", "It.Is"); 25 | var rootAssignment = rootObject.ReplaceAssignmentNodes(substitutions, "Mock"); 26 | var rootNewObject = rootAssignment.ReplaceObjectCreationNodes(substitutions, "new Mock"); 27 | var rootFields = rootNewObject.ReplaceVariableNodes(substitutions, "Mock<"); 28 | var rootExpression = rootFields.ReplaceExpressionNodes(substitutions, ".Setup(", ".Verify("); 29 | 30 | Logger.Log($"Modified File: \r\n{rootExpression}\r\n"); 31 | var modifiedCode = rootExpression.NormalizeWhitespace().ToFullString(); 32 | if (root.ToFullString() == rootExpression.ToFullString() || !transform) return; 33 | File.WriteAllText(sourceFilePath, modifiedCode); 34 | } 35 | 36 | internal static void GetNodeTypesFromFile(string sourceFilePath) 37 | { 38 | var sourceText = File.ReadAllText(sourceFilePath); 39 | if (!sourceText.Contains("Mock")) return; 40 | GetNodeTypesFromString(sourceText); 41 | } 42 | 43 | internal static void GetNodeTypesFromString(string sourceText) 44 | { 45 | var tree = CSharpSyntaxTree.ParseText(sourceText); 46 | var root = tree.GetRoot(); 47 | 48 | var descendantNodes = root.DescendantNodes(); 49 | var descendantNodeArray = descendantNodes.ToArray(); 50 | 51 | foreach (var node in descendantNodeArray) 52 | { 53 | var originalCode = node.ToString(); 54 | var nodeType = node.GetType().ToString(); 55 | 56 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {originalCode}, Node Type: {nodeType}"); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Extensions/SyntaxNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using MoqToNSubstitute.Models; 4 | using MoqToNSubstitute.Syntax; 5 | using MoqToNSubstitute.Utilities; 6 | 7 | namespace MoqToNSubstitute.Extensions; 8 | 9 | internal static class SyntaxNodeExtensions 10 | { 11 | internal static IEnumerable GetNodes(this SyntaxNode root, params string[] matchStrings) 12 | { 13 | return root.DescendantNodes().OfType().Where(node => 14 | { 15 | return (Array.Exists(matchStrings, match => node is not null && node.ToString()!.Contains(match))); 16 | }); 17 | } 18 | 19 | internal static SyntaxNode ReplaceAssignmentNodes(this SyntaxNode root, CodeSyntax codeSyntax, params string[] matchText) 20 | { 21 | var customSyntaxRewriter = new CustomSyntaxRewriter(codeSyntax); 22 | return root.ReplaceNodes(root.GetNodes(matchText), 23 | (node, _) => 24 | { 25 | var newNode = customSyntaxRewriter.VisitAssignmentExpression(node); 26 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {node}, Transformed: {newNode}"); 27 | return newNode ?? node; 28 | } 29 | ); 30 | } 31 | 32 | internal static SyntaxNode ReplaceObjectCreationNodes(this SyntaxNode root, CodeSyntax codeSyntax, params string[] matchText) 33 | { 34 | var customSyntaxRewriter = new CustomSyntaxRewriter(codeSyntax); 35 | return root.ReplaceNodes(root.GetNodes(matchText), 36 | (node, _) => 37 | { 38 | var newNode = customSyntaxRewriter.VisitObjectCreationExpression(node); 39 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {node}, Transformed: {newNode}"); 40 | return newNode ?? node; 41 | } 42 | ); 43 | } 44 | 45 | internal static SyntaxNode ReplaceExpressionNodes(this SyntaxNode root, CodeSyntax codeSyntax, params string[] matchText) 46 | { 47 | var customSyntaxRewriter = new CustomSyntaxRewriter(codeSyntax); 48 | return root.ReplaceNodes(root.GetNodes(matchText), 49 | (node, _) => 50 | { 51 | var newNode = customSyntaxRewriter.VisitExpressionStatement(node); 52 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {node}, Transformed: {newNode}"); 53 | return newNode ?? node; 54 | } 55 | ); 56 | } 57 | 58 | internal static SyntaxNode ReplaceVariableNodes(this SyntaxNode root, CodeSyntax codeSyntax, params string[] matchText) 59 | { 60 | var customSyntaxRewriter = new CustomSyntaxRewriter(codeSyntax); 61 | return root.ReplaceNodes(root.GetNodes(matchText), 62 | (node, _) => 63 | { 64 | var newNode = customSyntaxRewriter.VisitVariableDeclaration(node); 65 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {node}, Transformed: {newNode}"); 66 | return newNode ?? node; 67 | } 68 | ); 69 | } 70 | 71 | internal static SyntaxNode ReplaceArgumentNodes(this SyntaxNode root, CodeSyntax codeSyntax, params string[] matchText) 72 | { 73 | var customSyntaxRewriter = new CustomSyntaxRewriter(codeSyntax); 74 | return root.ReplaceNodes(root.GetNodes(matchText), 75 | (node, _) => 76 | { 77 | var newNode = customSyntaxRewriter.VisitArgument(node); 78 | Logger.Log($"Line: {node.GetLocation().GetLineSpan().StartLinePosition.Line}, Original: {node}, Transformed: {newNode}"); 79 | return newNode ?? node; 80 | } 81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Models/CodeSyntax.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Models; 2 | 3 | /// 4 | /// The model that defines all the replacements as string replacements 5 | /// or regular expression replacements 6 | /// 7 | public class CodeSyntax 8 | { 9 | /// 10 | /// The list of identifier replacements in the type ObjectCreationExpressionSyntax 11 | /// 12 | public IEnumerable? Identifier { get; set; } 13 | /// 14 | /// The list of argument replacements in the type ArgumentSyntax 15 | /// 16 | public IEnumerable? Argument { get; set; } 17 | /// 18 | /// The list of variable type replacements in the type VariableDeclarationSyntax 19 | /// 20 | public IEnumerable? VariableType { get; set; } 21 | /// 22 | /// The list of assignment replacements in the type AssignmentExpressionSyntax 23 | /// 24 | public IEnumerable? AssignmentExpression { get; set; } 25 | /// 26 | /// The list of statement replacements in the type ExpressionStatementSyntax 27 | /// 28 | public IEnumerable? ExpressionStatement { get; set; } 29 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Models/Expression.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Models; 2 | 3 | /// 4 | /// The replacement expression 5 | /// 6 | public class Expression 7 | { 8 | /// 9 | /// The constructor that passes in the replacement values 10 | /// 11 | /// The original text to replace 12 | /// The replacement text 13 | /// If this is true, it will use RegEx for the 14 | /// replacement, otherwise it will use a string replace 15 | public Expression(string original, string replacement, bool isRegex) 16 | { 17 | Original = original; 18 | Replacement = replacement; 19 | IsRegex = isRegex; 20 | } 21 | 22 | /// 23 | /// The original text to replace 24 | /// 25 | public string Original { get; set; } 26 | 27 | /// 28 | /// The replacement text 29 | /// 30 | public string Replacement { get; set; } 31 | 32 | /// 33 | /// If this is true, it will use RegEx for the 34 | /// replacement, otherwise it will use a string replace 35 | /// 36 | public bool IsRegex { get; set; } 37 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/MoqToNSubstitute.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | True 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /MoqToNSubstitute/Program.cs: -------------------------------------------------------------------------------- 1 | using MoqToNSubstitute.Conversion; 2 | using MoqToNSubstitute.Utilities; 3 | using System.Runtime.CompilerServices; 4 | 5 | [assembly: InternalsVisibleTo("MoqToNSubstitute.Tests")] 6 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 7 | 8 | Logger.Log("Starting process..."); 9 | 10 | switch (args.Length) 11 | { 12 | case 0: 13 | Logger.Log("No arguments, running analysis on current directory"); 14 | MoqToNSubstituteConverter.Convert(); 15 | break; 16 | case 1: 17 | var onlyArg = args[0]; 18 | if (bool.TryParse(onlyArg, out var transform)) 19 | { 20 | Logger.Log(transform ? "Running transformation on current directory" : "Running analysis on current directory"); 21 | MoqToNSubstituteConverter.Convert("", transform); 22 | } 23 | else 24 | { 25 | Logger.Log($"Running analysis on: {onlyArg}"); 26 | MoqToNSubstituteConverter.Convert(onlyArg); 27 | } 28 | break; 29 | default: 30 | var first = args[0]; 31 | var second = args[1]; 32 | OrganizeArguments(first, second); 33 | break; 34 | } 35 | 36 | /// 37 | /// A partial class for Program so we could define am ICodeConverter parameter 38 | /// This was to facilitate unit testing 39 | /// 40 | public static partial class Program 41 | { 42 | internal static ICodeConverter MoqToNSubstituteConverter { get; set; } = new MoqToNSubstituteConverter(); 43 | 44 | internal static void OrganizeArguments(string first, string second) 45 | { 46 | // Both values were boolean 47 | if (bool.TryParse(first, out var firstBool) && bool.TryParse(second, out _)) 48 | { 49 | Logger.Log(firstBool ? "Running transformation on current directory" : "Running analysis on current directory"); 50 | // Use the first boolean value 51 | MoqToNSubstituteConverter.Convert("", firstBool); 52 | } 53 | // First value is not boolean but the second one is 54 | else if (!bool.TryParse(first, out _) && bool.TryParse(second, out var secondBool)) 55 | { 56 | Logger.Log(secondBool ? $"Running transformation on: {first}" : $"Running analysis on: {first}"); 57 | MoqToNSubstituteConverter.Convert(first, secondBool); 58 | } 59 | // Neither value is a boolean 60 | else if (!bool.TryParse(first, out _) && !bool.TryParse(second, out _)) 61 | { 62 | Logger.Log($"Running analysis on: {first}"); 63 | MoqToNSubstituteConverter.Convert(first); 64 | } 65 | else 66 | { 67 | Logger.Log(firstBool ? $"Running transformation on: {second}" : $"Running analysis on: {second}"); 68 | MoqToNSubstituteConverter.Convert(second, firstBool); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Syntax/CustomSyntaxRewriter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using MoqToNSubstitute.Models; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace MoqToNSubstitute.Syntax; 8 | 9 | /// 10 | /// Class that inherits CSharpSyntaxRewriter and overrides the node replacements 11 | /// 12 | public class CustomSyntaxRewriter : CSharpSyntaxRewriter 13 | { 14 | private readonly CodeSyntax _substitutions; 15 | 16 | /// 17 | /// Constructor for the CustomSyntaxRewriter 18 | /// 19 | /// The model with the lists of substitutions 20 | public CustomSyntaxRewriter(CodeSyntax substitutions) 21 | { 22 | _substitutions = substitutions; 23 | } 24 | 25 | /// 26 | /// Method to modify the node using the list of values defined in the CodeSyntax model 27 | /// 28 | /// The node to modify 29 | /// The modified syntax node 30 | public override SyntaxNode? VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) 31 | { 32 | var originalType = node.Type.ToString(); 33 | var initializer = node.Initializer?.ToString(); 34 | var replacementCode = originalType; 35 | if (_substitutions.Identifier == null) return base.VisitObjectCreationExpression(node); 36 | foreach (var identifier in _substitutions.Identifier) 37 | { 38 | // RegEx is not feasible for this syntax so use IsRegEx here to check the initializer 39 | if (identifier.IsRegex) 40 | { 41 | // if the initializer has a value then do a replace 42 | if (!string.IsNullOrEmpty(initializer)) 43 | { 44 | replacementCode = replacementCode.Replace(identifier.Original, 45 | identifier.Replacement); 46 | } 47 | } 48 | else 49 | { 50 | replacementCode = replacementCode.Replace(identifier.Original, 51 | identifier.Replacement); 52 | } 53 | } 54 | 55 | // Create a new "Replacement(arguments)" expression 56 | var substituteExpression = SyntaxFactory.ParseExpression($"{replacementCode}{node.ArgumentList}"); 57 | 58 | // Return the node with the new expression 59 | return substituteExpression; 60 | } 61 | 62 | /// 63 | /// Method to modify the node using the list of values defined in the CodeSyntax model 64 | /// 65 | /// The node to modify 66 | /// The modified syntax node 67 | public override SyntaxNode? VisitArgument(ArgumentSyntax node) 68 | { 69 | var originalCode = node.ToString(); 70 | if (_substitutions.Argument == null) return base.VisitArgument(node); 71 | var replacementCode = _substitutions.Argument.Aggregate(originalCode, (current, argument) => current.Replace(argument.Original, argument.Replacement)); 72 | return originalCode != replacementCode ? node.WithExpression(SyntaxFactory.ParseExpression(replacementCode)) : base.VisitArgument(node); 73 | } 74 | 75 | /// 76 | /// Method to modify the node using the list of values defined in the CodeSyntax model 77 | /// 78 | /// The node to modify 79 | /// The modified syntax node 80 | public override SyntaxNode? VisitVariableDeclaration(VariableDeclarationSyntax node) 81 | { 82 | var originalType = node.Type.ToString(); 83 | var replacementCode = originalType; 84 | if (_substitutions.VariableType == null) return base.VisitVariableDeclaration(node); 85 | foreach (var variableType in _substitutions.VariableType) 86 | { 87 | if (variableType.IsRegex) 88 | { 89 | replacementCode = Regex.Replace(replacementCode, variableType.Original, variableType.Replacement, RegexOptions.Compiled | RegexOptions.CultureInvariant | 90 | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(10)); 91 | } 92 | else 93 | { 94 | replacementCode = replacementCode.Replace(variableType.Original, 95 | variableType.Replacement); 96 | } 97 | } 98 | return node.Update(SyntaxFactory.ParseTypeName($"{replacementCode} "), node.Variables); 99 | } 100 | 101 | /// 102 | /// Method to modify the node using the list of values defined in the CodeSyntax model 103 | /// 104 | /// The node to modify 105 | /// The modified syntax node 106 | public override SyntaxNode? VisitAssignmentExpression(AssignmentExpressionSyntax node) 107 | { 108 | var originalAssignment = node.Right.ToString(); 109 | var replacementCode = originalAssignment; 110 | if (_substitutions.AssignmentExpression == null) return base.VisitAssignmentExpression(node); 111 | foreach (var assignment in _substitutions.AssignmentExpression) 112 | { 113 | if (assignment.IsRegex) 114 | { 115 | replacementCode = Regex.Replace(replacementCode, assignment.Original, assignment.Replacement, RegexOptions.Compiled | RegexOptions.CultureInvariant | 116 | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(10)); 117 | } 118 | else 119 | { 120 | replacementCode = replacementCode.Replace(assignment.Original, 121 | assignment.Replacement); 122 | } 123 | } 124 | return node.Update(node.Left, node.OperatorToken, SyntaxFactory.ParseTypeName(replacementCode)); 125 | } 126 | 127 | /// 128 | /// Method to modify the node using the list of values defined in the CodeSyntax model 129 | /// 130 | /// The node to modify 131 | /// The modified syntax node 132 | public override SyntaxNode? VisitExpressionStatement(ExpressionStatementSyntax node) 133 | { 134 | var originalCode = node.ToString(); 135 | var replacementCode = originalCode; 136 | if (_substitutions.ExpressionStatement == null) return base.VisitExpressionStatement(node); 137 | foreach (var statement in _substitutions.ExpressionStatement) 138 | { 139 | if (statement.IsRegex) 140 | { 141 | replacementCode = Regex.Replace(replacementCode, statement.Original, statement.Replacement, RegexOptions.Compiled | RegexOptions.CultureInvariant | 142 | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase, TimeSpan.FromSeconds(10)); 143 | } 144 | else 145 | { 146 | replacementCode = replacementCode.Replace(statement.Original, 147 | statement.Replacement); 148 | } 149 | } 150 | return node.WithExpression(SyntaxFactory.ParseExpression(replacementCode)); 151 | } 152 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Templates/ReplacementTemplate.cs: -------------------------------------------------------------------------------- 1 | using MoqToNSubstitute.Models; 2 | 3 | namespace MoqToNSubstitute.Templates; 4 | 5 | internal static class ReplacementTemplate 6 | { 7 | internal static CodeSyntax ReturnReplacementSyntax() 8 | { 9 | return new CodeSyntax 10 | { 11 | Identifier = new List 12 | { 13 | new("Mock", "Substitute.ForPartsOf", true), 14 | new("Mock", "Substitute.For", false), 15 | }, 16 | Argument = new List 17 | { 18 | new("?.Object", "", false), 19 | new(".Object", "", false), 20 | new("It.IsAny", "Arg.Any", false), 21 | new("It.Is", "Arg.Is", false) 22 | }, 23 | VariableType = new List 24 | { 25 | new("Mock\\<(?.+)\\>", "${start}", true) 26 | }, 27 | AssignmentExpression = new List 28 | { 29 | new("new Mock(?.+).*\\{.*CallBase = true.*\\}", "Substitute.ForPartsOf${start}", true), 30 | new("new Mock", "Substitute.For", false), 31 | }, 32 | ExpressionStatement = new List 33 | { 34 | new("\r\n *", "", true), 35 | new("It.IsAny", "Arg.Any", false), 36 | new("It.Is", "Arg.Is", false), 37 | new(".Verifiable()", "", false), 38 | new(".Result", "", false), 39 | new("(?.+)\\.Setup\\(.+ => [^.]+\\.(?.+)\\)\\.ReturnsAsync(?.+);", "${start}.${middle}.Returns${end}", true), 40 | new("(?.+)\\.Setup\\(.+ => [^.]+\\.(?.+)\\)\\.Returns(?.+);", "${start}.${middle}.Returns${end}", true), 41 | new("(?.+)\\.Setup\\(.+ => [^.]+\\.(?.+)\\)\\.ThrowsAsync(?.+);", "${start}.${middle}.Throws${end}", true), 42 | new("(?.+)\\.Setup\\(.+ => [^.]+\\.(?.+)\\)\\.Throws(?.+);", "${start}.${middle}.Throws${end}", true), 43 | new("(?.+)\\.Setup\\(.+ => [^.]+\\.(?.+)\\)(?.+);", "${start}.${middle}.Throws${end}", true), 44 | new("(?.+)\\.Verify\\(.+ => [^.]+\\.(?.+)\\, Times.Once\\);", "${start}.Received(1).${middle}", true), 45 | new("(?.+)\\.Verify\\(.+ => [^.]+\\.(?.+)\\, Times.Never\\);", "${start}.DidNotReceive().${middle}", true), 46 | new("(?.+)\\.Verify\\(.+ => [^.]+\\.(?.+)\\, Times.Exactly(?.+)\\);", "${start}.Received${times}.${middle}", true), 47 | new("(?.+)\\.Verify\\(.+ => [^.]+\\.(?.+);", "${start}.Received().${middle}", true), 48 | } 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Utilities/DotNetPackageManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace MoqToNSubstitute.Utilities; 4 | 5 | internal class DotNetPackageManager : IPackageManager 6 | { 7 | 8 | /// 9 | /// Uninstall a package from csproj 10 | /// 11 | /// The project path 12 | /// The package name 13 | public void Uninstall(string projectPath, string packageName) 14 | { 15 | RunCommand($"dotnet remove {projectPath} package {packageName}"); 16 | } 17 | 18 | /// 19 | /// Install a package into csproj 20 | /// 21 | /// The project path 22 | /// The package name 23 | public void Install(string projectPath, string packageName) 24 | { 25 | RunCommand($"dotnet add {projectPath} package {packageName}"); 26 | } 27 | 28 | private static void RunCommand(string command) 29 | { 30 | var process = new Process 31 | { 32 | StartInfo = new ProcessStartInfo 33 | { 34 | RedirectStandardInput = true, 35 | RedirectStandardOutput = true, 36 | CreateNoWindow = true, 37 | UseShellExecute = true 38 | } 39 | }; 40 | 41 | process.Start(); 42 | process.StandardInput.WriteLine(command); 43 | process.StandardInput.Close(); 44 | Logger.Log(process.StandardOutput.ReadToEnd()); 45 | process.WaitForExit(); 46 | } 47 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Utilities/IPackageManager.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Utilities; 2 | internal interface IPackageManager 3 | { 4 | /// 5 | /// Uninstall a package from csproj 6 | /// 7 | /// The project path 8 | /// The package name 9 | 10 | void Uninstall(string projectPath, string packageName); 11 | 12 | /// 13 | /// Install a package into csproj 14 | /// 15 | /// The project path 16 | /// The package name 17 | void Install(string projectPath, string packageName); 18 | } -------------------------------------------------------------------------------- /MoqToNSubstitute/Utilities/Logger.cs: -------------------------------------------------------------------------------- 1 | namespace MoqToNSubstitute.Utilities; 2 | 3 | /// 4 | /// A console and file logger 5 | /// 6 | public static class Logger 7 | { 8 | private static readonly string LogPath = Path.Combine(Directory.GetCurrentDirectory(), "logs", $"log_{DateTime.Now:yyyyMMddHHmmss}.txt"); 9 | 10 | /// 11 | /// Constructor for the logger that will create the logs folder 12 | /// 13 | /// Throws an exception if creating the folder fails 14 | static Logger() 15 | { 16 | try 17 | { 18 | Directory.CreateDirectory(Path.GetDirectoryName(LogPath) ?? throw new InvalidOperationException()); 19 | } 20 | catch (Exception e) 21 | { 22 | Console.WriteLine($"An error occurred trying to create the log folder: {e}"); 23 | } 24 | } 25 | 26 | /// 27 | /// Static method to log to the console and the log file 28 | /// 29 | /// The log message 30 | public static void Log(string message) 31 | { 32 | Console.WriteLine(message); 33 | File.AppendAllText(LogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}\n"); 34 | } 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoqToNSubstituteConverter 2 | ## A console application to convert Moq in unit tests to NSubstitute 3 | 4 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=SilverLiningComputing_MoqToNSubstituteConverter&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=SilverLiningComputing_MoqToNSubstituteConverter) 5 | 6 | ### This project only has replacement code for the following: 7 | ``` csharp 8 | Mock = ClassToMock 9 | new Mock() = Substitute.For() 10 | new Mock() { CallBase = true } = Substitute.ForPartsOf 11 | It.IsAny = Arg.Any 12 | It.Is = Arg.Is 13 | .Object will be removed 14 | .Verifiable will be removed 15 | .Result will be removed 16 | .Setup(name => name will be removed 17 | .Verify(name => name = Received() 18 | ``` 19 | ``` csharp 20 | .Returns, .ReturnsAsync = .Returns 21 | .Throws, .ThrowsAsync = .Throws 22 | ``` 23 | ``` csharp 24 | Times.Once = .Received(1) 25 | Times.Exactly(3) = .Received(3) 26 | Times.Never = .DidNotReceive() 27 | ``` 28 | 29 | ## Limitations: 30 | * This will work best on well formatted code 31 | * There is no replacment for `.VerifyAll` or `.Protected` 32 | * `It.IsAnyType` does not currently have a replacement 33 | * Some of the replaced statements no longer have the correct indentation, they are left justified, this could be fixed in Visual Studio by reformatting the document 34 | * The replacement for `Mock = new()` is not technically correct, it will replace it with `ClassToMock = new()` but it should be `ClassToMock = Substitute.For()` 35 | * Once the replacement is complete `using NSubstitute;` will need to be added to each modified file or `global using NSubstitute;` in the Usings.cs file 36 | * If the method you are setting up to "mock" is asynchronous, you may need to add await to the call 37 | * If there are Null Conditional Operators ?. in the code, these get left behind and will need to be removed - hopefully this issue is now resolved https://github.com/SilverLiningComputing/MoqToNSubstituteConverter/issues/12 38 | 39 | ## How to Run the code (PowerShell) 40 | By default the executable will run in the current directory and sub directories, it perfoms analysis only and logs the results of the analysis in the log file. 41 | ``` 42 | .\MoqToNSubstitute.exe 43 | ``` 44 | To perform analysis on a specific directory: 45 | ``` 46 | .\MoqToNSubstitute.exe C:\Users\user\Documents\Projects\Vts.Api\Vts.Api.Tests\ 47 | ``` 48 | To perform transformation on a specific directory: 49 | ``` 50 | .\MoqToNSubstitute.exe C:\Users\user\Documents\Projects\Vts.Api\Vts.Api.Tests\ true 51 | ``` 52 | 53 | ## How to Debug the code (Visual Studio) 54 | If you place the folder for the code to be analyzed in the bin folder of the main project `\MoqToNSubstitute\MoqToNSubstitute\bin\Debug\net6.0\` you can set breakpoints and step through the code, it will analyze by default. The log file will be located in the logs folder in that same location `\MoqToNSubstitute\MoqToNSubstitute\bin\Debug\net6.0\logs`. 55 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | ## The MIT License (MIT) 2 | 3 | #### Copyright (c) 2023 Silver Lining Computing, LLC 4 | 5 | ### Acknowledgement 6 | Use the following acknowledgement in publications or applications that make use of this open source software or underlying technology and research: 7 | 8 | __"This work was made possible through open-source software resources offered by Silver Lining Computing, LLC."__ 9 | 10 | _Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:_ 11 | 12 | _The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software._ 13 | 14 | _THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE._ 15 | --------------------------------------------------------------------------------