├── .gitattributes ├── .github └── workflows │ ├── dotnet-build.yml │ └── nuget-push.yml ├── .gitignore ├── .vscode └── settings.json ├── Directory.Build.props ├── How to Contribute.md ├── LICENSE ├── MSTest.Extensions.sln ├── MSTest.Extensions.sln.DotSettings ├── README.md ├── build ├── GenericGenerator │ ├── GenericGenerator.csproj │ ├── GenericTypeGenerator.cs │ └── Program.cs ├── Version.props └── code-style.ruleset ├── demo └── dotnetCampus.UITest.WPF.Demo │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── FooTest.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ └── dotnetCampus.UITest.WPF.Demo.csproj ├── docs ├── README.md ├── en │ └── README.md ├── images │ ├── 2018-02-12-08-54-31.png │ ├── 2018-02-13-13-09-26.png │ ├── 2018-02-13-14-35-29.png │ ├── 2018-02-27-11-11-10.png │ ├── 2018-02-27-11-15-09.png │ ├── 2018-02-27-11-32-59.png │ ├── 2018-02-27-11-33-49.png │ ├── 2018-02-27-11-36-50.png │ ├── 2018-02-27-11-38-16.png │ ├── 2018-02-27-11-39-09.png │ ├── 2018-02-27-11-48-07.png │ ├── 2018-02-27-11-50-18.png │ ├── 2018-02-27-11-51-43.png │ ├── 2018-02-27-11-56-40.png │ ├── 2018-02-27-11-59-28.png │ ├── 2018-02-27-12-01-29.png │ ├── 2018-02-27-12-16-05.png │ ├── 2018-02-27-12-19-51.png │ ├── unit-test-result-of-demo.jp.png │ ├── unit-test-result-of-demo.png │ ├── unit-test-result-of-demo.zh-chs.png │ └── unit-test-result-of-demo.zh-cht.png ├── jp │ └── README.jp.md ├── zh-chs │ ├── README.md │ ├── README.zh-chs.md │ └── resharper-code-templates.md └── zh-cht │ ├── README.md │ └── README.zh-cht.md ├── icon.png ├── src ├── MSTest.Extensions │ ├── AssertExtensions │ │ ├── AssertExtensions.cs │ │ ├── CollectionAssertExtensions.cs │ │ └── StringAssertExtensions.cs │ ├── Contracts │ │ ├── ContractTest.01.cs │ │ ├── ContractTest.cs │ │ ├── ContractTestCaseAttribute.cs │ │ ├── ContractTestConfiguration.cs │ │ └── ContractTestContext.cs │ ├── Core │ │ ├── ContractTestCase.cs │ │ ├── ITestCase.cs │ │ ├── ReadonlyTestCase.cs │ │ ├── TestCaseIndexer.cs │ │ ├── TestMethodProxy.cs │ │ └── ThreadSafeStringWriter.cs │ ├── CustomTestManagers │ │ ├── CustomTestManager.cs │ │ ├── CustomTestManagerRunContext.cs │ │ ├── TestExceptionResult.cs │ │ └── TestManagerRunResult.cs │ ├── MSTest.Extensions.csproj │ ├── Properties │ │ ├── Annotations.cs │ │ └── AssemblyInfo.cs │ └── Utils │ │ ├── Constants.cs │ │ └── ReflectionExtensions.cs └── dotnetCampus.UITest.WPF │ ├── UIContractTestCaseAttribute.cs │ ├── UITestManager.cs │ ├── UITestMethodProxy.cs │ └── dotnetCampus.UITest.WPF.csproj └── tests └── MSTest.Extensions.Tests ├── Contracts ├── ContractTestCaseAttributeTests.cs ├── ContractTestContextTests.cs ├── ContractTestGenericTests.cs ├── ContractTestInitilizeCleanupTests.cs └── ContractTestTests.cs ├── Core ├── ContractTestCaseTests.cs └── TestCaseIndexerTests.cs ├── MSTest.Extensions.Tests.csproj └── Properties └── GlobalSuppressions.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | configuration: [Debug, Release] 14 | runs-on: windows-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: | 23 | 3.1.x 24 | 5.0.x 25 | 6.0.x 26 | 27 | - name: Build 28 | run: dotnet build --configuration $env:Configuration 29 | env: 30 | Configuration: ${{ matrix.configuration }} 31 | - name: Test 32 | run: dotnet test --configuration $env:Configuration 33 | env: 34 | Configuration: ${{ matrix.configuration }} 35 | -------------------------------------------------------------------------------- /.github/workflows/nuget-push.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: | 19 | 3.1.x 20 | 5.0.x 21 | 6.0.x 22 | 23 | - name: Install tool 24 | run: dotnet tool install -g dotnetCampus.TagToVersion 25 | 26 | - name: Set tag to version 27 | run: dotnet TagToVersion -t ${{ github.ref }} 28 | 29 | - name: Pack 30 | run: dotnet build --configuration Release 31 | - name: Push 32 | run: dotnet nuget push .\bin\Release\*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NugetKey }} --skip-duplicate --no-symbols 1 33 | -------------------------------------------------------------------------------- /.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 | 263 | # GenericGeneratedCode 264 | **/*.02.cs 265 | **/*.03.cs 266 | **/*.04.cs 267 | **/*.05.cs 268 | **/*.06.cs 269 | **/*.07.cs 270 | **/*.08.cs 271 | **/*.09.cs 272 | **/*.10.cs 273 | **/*.11.cs 274 | **/*.12.cs 275 | **/*.13.cs 276 | **/*.14.cs 277 | **/*.15.cs 278 | **/*.16.cs -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pasteImage.path": "${projectRoot}/docs/images", 3 | "pasteImage.basePath": "${projectRoot}", 4 | "pasteImage.prefix": "/" 5 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)bin\$(Configuration) 5 | dotnet-campus 6 | dotnet-campus 7 | latest 8 | false 9 | source;dotnet;nuget;msbuild 10 | MIT 11 | 12 | 13 | 14 | icon.png 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /How to Contribute.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | ## Contributing to Code 3 | There are many ways to contribute to the Code project: logging bugs, submitting pull requests, reporting issues, and creating suggestions. 4 | 5 | After cloning and building the repo, check out the [issues list](https://github.com/dotnet-campus/MSTestEnhancer/issues). Issues are good candidates to pick up if you are in the code for the first time. 6 | 7 | ## Build and Run 8 | If you want to understand how Code works or want to debug an issue, you'll want to get the source, build it, and run the tool locally. 9 | 10 | ### Getting the sources 11 | git clone https://github.com/dotnet-campus/MSTestEnhancer.git 12 | 13 | ### Prerequisites 14 | + [Git](https://git-scm.com/) 15 | + [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) 16 | + [.NET Core](https://docs.microsoft.com/en-us/dotnet/core/) 17 | + [Unit Test](https://msdn.microsoft.com/en-us/library/dd264975.aspx) 18 | + [MStest V2](https://github.com/Microsoft/testfx) 19 | 20 | Finally, install MSTest V2 packages using Nuget: 21 | 22 | Install-Package MSTest.TestFramework 23 | 24 | ### Build 25 | + Building with Visual Studio(VS) 26 | 27 | You can open /MSTest.Extensions.sln in VS and trigger a build of the entire code base using Build Solution(Ctrl+Shift+B) from the Solution Explorer or the Build menu. 28 | 29 | The bits get dropped at /bin/Debug/. 30 | 31 | + Building with command line(CLI) 32 | 33 | MSBuild.exe /MSTest.Extensions.sln 34 | 35 | ### Test 36 | + Running tests with Visual Studio 37 | 38 | All the tests in the MSTestEnhancer repo can be run via the Visual Studio Test Explorer. Building /MSTest.Extensions.sln as described in the build section above should populate all the tests in the Test Explorer. 39 | 40 | + Running tests with command line(CLI) 41 | 42 | vstest.console.exe /tests/MSTest.Extensions.Tests/bin/Debug/net47/MSTest.Extensions.Tests.dll 43 | 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 dotnet职业技术学院 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MSTest.Extensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTest.Extensions", "src\MSTest.Extensions\MSTest.Extensions.csproj", "{055E6C63-3CC7-427E-B14F-69C85D7E971A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1C4D72B5-A7C1-4AAE-A66F-4DDAC5979C28}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DA39C42F-B4EA-43F0-AB8C-40987B63F4D6}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSTest.Extensions.Tests", "tests\MSTest.Extensions.Tests\MSTest.Extensions.Tests.csproj", "{7AB54453-67E4-46BC-A314-EBA72C082E7E}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_SolutionItems", "_SolutionItems", "{F30DFDFF-C789-402F-888A-385F1D38EB11}" 15 | ProjectSection(SolutionItems) = preProject 16 | .gitattributes = .gitattributes 17 | .gitignore = .gitignore 18 | build\code-style.ruleset = build\code-style.ruleset 19 | LICENSE = LICENSE 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.UITest.WPF", "src\dotnetCampus.UITest.WPF\dotnetCampus.UITest.WPF.csproj", "{E62C4947-CD69-411D-8749-78C8B2C25ACE}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.UITest.WPF.Demo", "demo\dotnetCampus.UITest.WPF.Demo\dotnetCampus.UITest.WPF.Demo.csproj", "{F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {055E6C63-3CC7-427E-B14F-69C85D7E971A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {055E6C63-3CC7-427E-B14F-69C85D7E971A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {055E6C63-3CC7-427E-B14F-69C85D7E971A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {055E6C63-3CC7-427E-B14F-69C85D7E971A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {7AB54453-67E4-46BC-A314-EBA72C082E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {7AB54453-67E4-46BC-A314-EBA72C082E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {7AB54453-67E4-46BC-A314-EBA72C082E7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {7AB54453-67E4-46BC-A314-EBA72C082E7E}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {E62C4947-CD69-411D-8749-78C8B2C25ACE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {E62C4947-CD69-411D-8749-78C8B2C25ACE}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {E62C4947-CD69-411D-8749-78C8B2C25ACE}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {E62C4947-CD69-411D-8749-78C8B2C25ACE}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(NestedProjects) = preSolution 53 | {055E6C63-3CC7-427E-B14F-69C85D7E971A} = {1C4D72B5-A7C1-4AAE-A66F-4DDAC5979C28} 54 | {7AB54453-67E4-46BC-A314-EBA72C082E7E} = {DA39C42F-B4EA-43F0-AB8C-40987B63F4D6} 55 | {E62C4947-CD69-411D-8749-78C8B2C25ACE} = {1C4D72B5-A7C1-4AAE-A66F-4DDAC5979C28} 56 | {F1D52FE3-2E23-4C7D-AA64-CAC204B4EBBF} = {DA39C42F-B4EA-43F0-AB8C-40987B63F4D6} 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {21BFBA8A-2901-4A78-A722-0DDA95D3773F} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /MSTest.Extensions.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | 055E6C63-3CC7-427E-B14F-69C85D7E971A/d:Properties 4 | *.02.cs 5 | *.03.cs 6 | *.04.cs 7 | *.05.cs 8 | *.06.cs 9 | *.07.cs 10 | *.08.cs 11 | True 12 | ERROR 13 | ERROR 14 | ERROR 15 | ERROR 16 | ERROR 17 | ERROR 18 | ERROR 19 | ERROR 20 | ERROR 21 | WARNING 22 | ERROR 23 | ERROR 24 | ERROR 25 | ERROR 26 | ERROR 27 | ERROR 28 | ERROR 29 | ERROR 30 | ERROR 31 | ERROR 32 | ERROR 33 | ERROR 34 | ERROR 35 | ERROR 36 | ERROR 37 | ERROR 38 | WARNING 39 | ERROR 40 | ERROR 41 | ERROR 42 | ERROR 43 | ERROR 44 | ERROR 45 | ERROR 46 | WARNING 47 | WARNING 48 | True 49 | True 50 | Contract Unit Test 51 | 4 52 | Test Fixture 53 | 5 54 | True 55 | True 56 | Unit Tests 57 | Convert to MSTestEnhancer test case 58 | True 59 | constant("Please type a test contract here...") 60 | 0 61 | True 62 | 5 63 | True 64 | True 65 | 5.0 66 | InCSharpStatement 67 | True 68 | 5.0 69 | InCSharpExpression 70 | test 71 | True 72 | "$contract$".Test(() => 73 | { 74 | $SELECTION$ 75 | }); 76 | True 77 | True 78 | Unit Tests 79 | cs 80 | UnitTest 81 | False 82 | Contract Unit Test 83 | True 84 | getAlphaNumericFileNameWithoutExtension() 85 | -1 86 | 2 87 | True 88 | fileheader() 89 | 0 90 | True 91 | fileDefaultNamespace() 92 | -1 93 | 1 94 | True 95 | True 96 | InCSharpProjectFile 97 | True 98 | $HEADER$using MSTest.Extensions.Contracts; 99 | namespace $NAMESPACE$ 100 | { 101 | public class $CLASS$ 102 | { 103 | [MSTest.Extensions.Contracts.ContractTestCase] 104 | public void Test1() 105 | { 106 | "$SELSTART$Please type a test contract here...$SELEND$".Test(() => 107 | { 108 | // Arrange 109 | 110 | // Action 111 | 112 | // Assert 113 | }); 114 | } 115 | } 116 | } 117 | True 118 | True 119 | Unit Tests 120 | Insert a test case 121 | True 122 | constant("Please type a test contract here...") 123 | 0 124 | True 125 | True 126 | 5.0 127 | InCSharpStatement 128 | True 129 | 5.0 130 | InCSharpExpression 131 | test 132 | True 133 | "$contract$".Test(() => 134 | { 135 | // Arrange 136 | $SELSTART$// TODO: Write your test case here...$SELEND$ 137 | 138 | // Action 139 | 140 | // Assert 141 | }); 142 | True 143 | True 144 | Unit Tests 145 | Insert a test method 146 | True 147 | constant("Please type a test contract here...") 148 | 1 149 | True 150 | 0 151 | True 152 | True 153 | 5.0 154 | InCSharpTypeMember 155 | test 156 | True 157 | [MSTest.Extensions.Contracts.ContractTestCase] 158 | public void $TheMethodNameYouWantToTest$() 159 | { 160 | "$contract$".Test(() => 161 | { 162 | // Arrange 163 | $SELSTART$// TODO: Write your test case here...$SELEND$ 164 | 165 | // Action 166 | 167 | // Assert 168 | }); 169 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /docs/jp/README.jp.md 6 | [zh-chs]: /docs/zh-chs/README.zh-chs.md 7 | [zh-cht]: /docs/zh-cht/README.zh-cht.md 8 | 9 | ![.NET Build & Test](https://github.com/dotnet-campus/CUnit/workflows/.NET%20Build%20&%20Test/badge.svg) ![NuGet Push](https://github.com/dotnet-campus/CUnit/workflows/NuGet%20Push/badge.svg) 10 | 11 | 12 | | Name | NuGet| 13 | |--|--| 14 | |MSTestEnhancer|[![](https://img.shields.io/nuget/v/MSTestEnhancer.svg)](https://www.nuget.org/packages/MSTestEnhancer)| 15 | |dotnetCampus.UITest.WPF|[![](https://img.shields.io/nuget/v/dotnetCampus.UITest.WPF.svg)](https://www.nuget.org/packages/dotnetCampus.UITest.WPF)| 16 | 17 | # CUnit 18 | 19 | Don't you think that naming is very very hard? Especially naming for unit test method? Read this article for more data of naming: [Don’t go into programming if you don’t have a good thesaurus - ITworld](https://www.itworld.com/article/2833265/cloud-computing/don-t-go-into-programming-if-you-don-t-have-a-good-thesaurus.html). 20 | 21 | CUnit (MSTestEnhancer) helps you to write unit tests without naming any method. 22 | 23 | CUnit is a contract-style unit test extension for MSTestv2. You can write method contract descriptions instead of writing confusing test method name when writing unit tests. 24 | 25 | --- 26 | 27 | ## Getting Started with CUnit 28 | 29 | You can write unit test like this: 30 | 31 | ```csharp 32 | [TestClass] 33 | public class DemoTest 34 | { 35 | [ContractTestCase] 36 | public void Foo() 37 | { 38 | "When A happened, result A'.".Test(() => 39 | { 40 | // Arrange 41 | // Action 42 | // Assert 43 | }); 44 | 45 | "But when B happened, result B'".Test(() => 46 | { 47 | // Arrange 48 | // Action 49 | // Assert 50 | }); 51 | } 52 | } 53 | ``` 54 | 55 | Then you'll see this kind of test result in testing explorer window: 56 | 57 | ![Unit test result](/docs/images/unit-test-result-of-demo.png) 58 | 59 | For more usages, please visit: 60 | 61 | - [English](/README.md) 62 | - [日本語](/docs/jp/README.md) 63 | - [简体中文](/docs/zh-chs/README.md) 64 | - [繁體中文](/docs/zh-cht/README.md) 65 | 66 | ### Contributing Guide 67 | 68 | There are many ways to contribute to MSTestEnhancer 69 | 70 | - [Submit issues](https://github.com/dotnet-campus/MSTestEnhancer/issues) and help verify fixes as they are checked in. 71 | - Review the [documentation changes](https://github.com/dotnet-campus/MSTestEnhancer/pulls). 72 | - [How to Contribute](How%20to%20Contribute.md) 73 | 74 | ## License 75 | 76 | MSTestEnhancer is licensed under the [MIT license](/LICENSE) 77 | -------------------------------------------------------------------------------- /build/GenericGenerator/GenericGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | bin\ 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /build/GenericGenerator/GenericTypeGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace GenericGenerator 5 | { 6 | public class GenericTypeGenerator 7 | { 8 | private const string ToolName = "https://github.easiwin.io/mstest-enhancer"; 9 | private const string ToolVersion = "1.0"; 10 | 11 | private static readonly string GeneratedHeader = 12 | $@"//------------------------------------------------------------------------------ 13 | // 14 | // This code was generated by a tool. 15 | // Runtime Version:{Environment.Version.ToString(4)} 16 | // 17 | // Changes to this file may cause incorrect behavior and will be lost if 18 | // the code is regenerated. 19 | // 20 | //------------------------------------------------------------------------------ 21 | 22 | #define GENERATED_CODE 23 | "; 24 | 25 | private static readonly string GeneratedFooter = 26 | $@""; 27 | 28 | private static readonly string Generatedattribute = 29 | $"[System.CodeDom.Compiler.GeneratedCode(\"{ToolName}\", \"{ToolVersion}\")]"; 30 | 31 | private readonly string _genericTemplate; 32 | 33 | public GenericTypeGenerator(string genericTemplate) 34 | { 35 | _genericTemplate = genericTemplate; 36 | } 37 | 38 | public string Generate(int genericCount) 39 | { 40 | var content = _genericTemplate 41 | // 替换泛型。 42 | .Replace("{ForT(t)}", FromTemplate("{0}", "{ForT(t{n})}", ", ", genericCount)) 43 | .Replace("ForT(t)", FromTemplate("{0}", "ForT(t{n})", ", ", genericCount)) 44 | .Replace(" T[] ", FromTemplate(" ({0})[] ", "T{n}", ", ", genericCount)) 45 | .Replace("", FromTemplate("<{0}>", "out T{n}", ", ", genericCount)) 46 | .Replace("Task", FromTemplate("Task<({0})>", "T{n}", ", ", genericCount)) 47 | .Replace("Func", FromTemplate("Func<{0}, Task>", "T{n}", ", ", genericCount)) 48 | .Replace(" T, Task>", FromTemplate(" {0}, Task>", "T{n}", ", ", genericCount)) 49 | .Replace("(T, bool", FromTemplate("({0}, bool", "T{n}", ", ", genericCount)) 50 | .Replace("var (t, ", FromTemplate("var ({0}, ", "t{n}", ", ", genericCount)) 51 | .Replace("var t in", FromTemplate("var ({0}) in", "t{n}", ", ", genericCount)) 52 | .Replace(", t)", FromTemplate(", {0})", "t{n}", ", ", genericCount)) 53 | .Replace("return (t, ", FromTemplate("return ({0}, ", "t{n}", ", ", genericCount)) 54 | .Replace("({t})", FromTemplate("({0})", "{t{n}}", ", ", genericCount)) 55 | .Replace("", FromTemplate("<{0}>", "T{n}", ", ", genericCount)) 56 | .Replace("{T}", FromTemplate("{{{0}}}", "T{n}", ", ", genericCount)) 57 | .Replace("(T value)", FromTemplate("(({0}) value)", "T{n}", ", ", genericCount)) 58 | .Replace("(T t)", FromTemplate("({0})", "T{n} t{n}", ", ", genericCount)) 59 | .Replace("(t)", FromTemplate("({0})", "t{n}", ", ", genericCount)) 60 | .Replace("var t =", FromTemplate("var ({0}) =", "t{n}", ", ", genericCount)) 61 | .Replace(" t =>", FromTemplate(" ({0}) =>", "t{n}", ", ", genericCount)) 62 | .Replace(" T ", FromTemplate(" ({0}) ", "T{n}", ", ", genericCount)) 63 | .Replace(" t;", FromTemplate(" ({0});", "t{n}", ", ", genericCount)) 64 | // 生成 [GeneratedCode]。 65 | .Replace(" public interface ", $" {Generatedattribute}{Environment.NewLine} public interface ") 66 | .Replace(" public class ", $" {Generatedattribute}{Environment.NewLine} public class ") 67 | .Replace(" public sealed class ", $" {Generatedattribute}{Environment.NewLine} public sealed class "); 68 | return GeneratedHeader + Environment.NewLine + content.Trim() + Environment.NewLine + GeneratedFooter; 69 | } 70 | 71 | private static string FromTemplate(string template, string part, string seperator, int count) 72 | { 73 | return string.Format(template, 74 | string.Join(seperator, Enumerable.Range(1, count).Select(x => part.Replace("{n}", x.ToString())))); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /build/GenericGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace GenericGenerator 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | foreach (var argument in args) 12 | { 13 | GenerateGenericTypes(argument, 8); 14 | } 15 | } 16 | 17 | private static void GenerateGenericTypes(string file, int count) 18 | { 19 | // 读取原始文件并创建泛型代码生成器。 20 | var template = File.ReadAllText(file, Encoding.UTF8); 21 | var generator = new GenericTypeGenerator(template); 22 | 23 | // 根据泛型个数生成目标文件路径和文件内容。 24 | var format = GetIndexedFileNameFormat(file); 25 | (string targetFileName, string targetFileContent)[] contents = Enumerable.Range(2, count - 1).Select(i => 26 | (string.Format(format, i.ToString().PadLeft(2, '0')), generator.Generate(i)) 27 | ).ToArray(); 28 | 29 | // 写入目标文件。 30 | foreach (var writer in contents) 31 | { 32 | File.WriteAllText(writer.targetFileName, writer.targetFileContent); 33 | } 34 | } 35 | 36 | private static string GetIndexedFileNameFormat(string fileName) 37 | { 38 | var directory = Path.GetDirectoryName(fileName); 39 | var name = Path.GetFileNameWithoutExtension(fileName); 40 | if (name.EndsWith(".01")) 41 | { 42 | name = name.Substring(0, name.Length - 3); 43 | } 44 | if (name.EndsWith("1")) 45 | { 46 | name = name.Substring(0, name.Length - 1); 47 | } 48 | 49 | return Path.Combine(directory, name + ".{0}.cs"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /build/Version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.0.1 4 | 5 | -------------------------------------------------------------------------------- /build/code-style.ruleset: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace dotnetCampus.UITest.WPF.Demo 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/FooTest.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | using System.Windows.Threading; 5 | 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | using MSTest.Extensions.Contracts; 9 | using MSTest.Extensions.Utils; 10 | 11 | namespace dotnetCampus.UITest.WPF.Demo 12 | { 13 | [TestClass] 14 | public class FooTest 15 | { 16 | [AssemblyInitialize] 17 | public static void InitializeApplication(TestContext testContext) 18 | { 19 | UITestManager.InitializeApplication(() => new App()); 20 | } 21 | 22 | [UIContractTestCase] 23 | public void TestAsyncLoad() 24 | { 25 | "Waiting with async Loaded, then it do not lock UI Thread.".Test(async () => 26 | { 27 | var mainWindow = new MainWindow(); 28 | var taskCompletionSource = new TaskCompletionSource(); 29 | mainWindow.Loaded += (sender, args) => taskCompletionSource.SetResult(); 30 | await mainWindow.Dispatcher.InvokeAsync(mainWindow.Show); 31 | await taskCompletionSource.Task; 32 | }); 33 | } 34 | 35 | [UIContractTestCase] 36 | public void TestMainWindow() 37 | { 38 | "Test Open MainWindow, MainWindow be opened".Test(() => 39 | { 40 | Assert.AreEqual(Application.Current.Dispatcher, Dispatcher.CurrentDispatcher); 41 | var mainWindow = new MainWindow(); 42 | bool isMainWindowLoaded = false; 43 | mainWindow.Loaded += (sender, args) => isMainWindowLoaded = true; 44 | mainWindow.Show(); 45 | Assert.AreEqual(true, isMainWindowLoaded); 46 | }); 47 | 48 | "Test Close MainWindow, MainWindow be closed".Test(() => 49 | { 50 | var window = Application.Current.MainWindow; 51 | Assert.AreEqual(true, window is MainWindow); 52 | bool isMainWindowClosed = false; 53 | Assert.IsNotNull(window); 54 | window.Closed += (sender, args) => isMainWindowClosed = true; 55 | window.Close(); 56 | Assert.AreEqual(true, isMainWindowClosed); 57 | }); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | using System.Windows.Shapes; 15 | 16 | namespace dotnetCampus.UITest.WPF.Demo 17 | { 18 | /// 19 | /// Interaction logic for MainWindow.xaml 20 | /// 21 | public partial class MainWindow : Window 22 | { 23 | public MainWindow() 24 | { 25 | InitializeComponent(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/dotnetCampus.UITest.WPF.Demo/dotnetCampus.UITest.WPF.Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | exe 5 | true 6 | net6.0-windows 7 | enable 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Designer 19 | MSBuild:Compile 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/README.md -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # MSTest Enhancer 2 | 3 | MSTestEnhancer is a MSTest v2 extension to connect unit test and the method that should be tested. You'll find out that all unit test contracts are listed under target methods, and you can see all the result of them directly, no need to translate the obscure method name into what you want to test. 4 | 5 | ## Getting Started 6 | 7 | 1. Install [MSTestEnhancer](https://www.nuget.org/packages/MSTestEnhancer/) from the [nuget.org](https://www.nuget.org/). 8 | 1. Write unit test code in the style listed below. 9 | 10 | ## Recommended Style of Writing Unit Tests 11 | 12 | Assuming that you want to test a class named `TheTestedClass` containing a method named `TheTestedMethod`. Then you can write unit tests like this: 13 | 14 | ```csharp 15 | [TestClass] 16 | public class TheTestedClassTest 17 | { 18 | [ContractTestCase] 19 | public void TheTestedMethod() 20 | { 21 | "When Xxx happens, results in Yyy.".Test(() => 22 | { 23 | // Write test case code here... 24 | }); 25 | 26 | "When Zzz happens, results in Www.".Test(() => 27 | { 28 | // Write test case code here... 29 | }); 30 | } 31 | } 32 | ``` 33 | 34 | Notice that the name of class and method are almost the name of the tested class and tested method. As a result, we don't need to think about anything about naming unit test, nor to read the obscure name of the unit test. 35 | 36 | ![Test results](/docs/images/2018-02-13-13-09-26.png) 37 | 38 | ## Unit Test with Arguments 39 | 40 | Some unit tests need multiple values to verify the contracts, so MSTestEnhancer provides `WithArguments` method to config the arguments. 41 | 42 | ```csharp 43 | "prime number.".Test((int num) => 44 | { 45 | // Write test case code here... 46 | }).WithArguments(2, 3, 5, 7, 11); 47 | 48 | "{0} is not a prime number.".Test((int num) => 49 | { 50 | // Write test case code here... 51 | }).WithArguments(1, 4); 52 | ``` 53 | 54 | You can pass up to 8 parameters into the test case. 55 | 56 | ```csharp 57 | "Contract 1: {0} and {1} are allowed in the contract description.".Test((int a, int b) => 58 | { 59 | // Now, a is 2 and b is 3. 60 | }).WithArguments(2, 3); 61 | 62 | "Contract 2".Test((int a, int b) => 63 | { 64 | // Now the test case will run twice. The first group, a is 2 and b is 3; and the second group, a is 10 and b is 20. 65 | // ValueTuple is supported, too. 66 | }).WithArguments((2, 3), (10, 20)); 67 | ``` 68 | 69 | In this example, the contract description will be replaced to the arguments that you have passed into. 70 | 71 | ## Async Unit Test 72 | 73 | All `Test` extension method support async action so that you can test any async method. 74 | 75 | ## Some Fantastic Feature 76 | 77 | Nested unit test classes are supported by MSTest v2, so you can write an infinite level unit test tree. 78 | -------------------------------------------------------------------------------- /docs/images/2018-02-12-08-54-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-12-08-54-31.png -------------------------------------------------------------------------------- /docs/images/2018-02-13-13-09-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-13-13-09-26.png -------------------------------------------------------------------------------- /docs/images/2018-02-13-14-35-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-13-14-35-29.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-11-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-11-10.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-15-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-15-09.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-32-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-32-59.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-33-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-33-49.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-36-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-36-50.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-38-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-38-16.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-39-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-39-09.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-48-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-48-07.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-50-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-50-18.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-51-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-51-43.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-56-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-56-40.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-11-59-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-11-59-28.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-12-01-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-12-01-29.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-12-16-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-12-16-05.png -------------------------------------------------------------------------------- /docs/images/2018-02-27-12-19-51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/2018-02-27-12-19-51.png -------------------------------------------------------------------------------- /docs/images/unit-test-result-of-demo.jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/unit-test-result-of-demo.jp.png -------------------------------------------------------------------------------- /docs/images/unit-test-result-of-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/unit-test-result-of-demo.png -------------------------------------------------------------------------------- /docs/images/unit-test-result-of-demo.zh-chs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/unit-test-result-of-demo.zh-chs.png -------------------------------------------------------------------------------- /docs/images/unit-test-result-of-demo.zh-cht.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/docs/images/unit-test-result-of-demo.zh-cht.png -------------------------------------------------------------------------------- /docs/jp/README.jp.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /docs/jp/README.jp.md 6 | [zh-chs]: /docs/zh-chs/README.zh-chs.md 7 | [zh-cht]: /docs/zh-cht/README.zh-cht.md 8 | 9 | # MSTestEnhancer 10 | 11 | 命名は非常に難しいと思いませんか? 特にユニットテストメソッドの命名は? 詳細な命名についてはこの記事をお読みください。 12 | 13 | [あなたが良いシソーラスを持っていない場合、プログラミングに参加しないでください - ITworld](https://www.itworld.com/article/2833265/cloud-computing/don-t-go-into-programming-if-you-don-t-have-a-good-thesaurus.html)。 14 | 15 | MSTestEnhancerは、メソッドの名前を付けずに単体テストを記述するのに役立ちます。 16 | 17 | MSTestEnhancerは、MSTestv2の契約スタイルの単体テスト拡張です。 単体テストを書くときに、混乱しているテストメソッド名を記述するのではなく、メソッド契約の記述を書くことができます。 18 | 19 | ## 入門 20 | 21 | 次のようにユニットテストを書くことができます: 22 | 23 | ```csharp 24 | [TestClass] 25 | public class DemoTest 26 | { 27 | [ContractTestCase] 28 | public void Foo() 29 | { 30 | "A条件が満たされると、Xのことが起こるはずです。".Test(() => 31 | { 32 | // Arrange 33 | // Action 34 | // Assert 35 | }); 36 | 37 | "しかし、あなたがB条件を満たすとき、Y事が起こるはずです。".Test(() => 38 | { 39 | // Arrange 40 | // Action 41 | // Assert 42 | }); 43 | } 44 | } 45 | ``` 46 | 47 | 次に、エクスプローラウィンドウのテストでこのようなテスト結果が表示されます。 48 | 49 | ![ユニットテスト結果](/docs/images/unit-test-result-of-demo.jp.png) 50 | -------------------------------------------------------------------------------- /docs/zh-chs/README.md: -------------------------------------------------------------------------------- 1 | # MSTest Enhancer 2 | 3 | 在 MSTestEnhancer 的帮助下,单元测试将更加容易编写、理解和维护。因为它让单元测试方法和被测类本身之间的关系更加紧密。 4 | 5 | ## 如何开始 6 | 7 | 1. 在单元测试项目中安装 NuGet 包:[MSTestEnhancer](https://www.nuget.org/packages/MSTestEnhancer/)。 8 | 1. 现在就可以开始编写下文那种风格的单元测试代码了。 9 | 10 | ## 推荐的单元测试编写方式 11 | 12 | 假设你希望测试一个名为 `被测类名` 的类,其中包含一个名为 `被测方法名` 的方法。你可以这样编写单元测试: 13 | 14 | ```csharp 15 | [TestClass] 16 | public class 被测类名Test 17 | { 18 | [ContractTestCase] 19 | public void 被测方法名() 20 | { 21 | "契约 1(当 Xxx 时,应该发生 Yyy)".Test(() => 22 | { 23 | // 测试用例代码 24 | }); 25 | 26 | "契约 2(但当 Zzz 时,应该发生 Www)".Test(() => 27 | { 28 | // 测试用例代码 29 | }); 30 | } 31 | } 32 | ``` 33 | 34 | 注意到单元测试类的名称和原本的类名称是一一对应的,方法名和原本的方法名是一模一样的;于是,我们不再需要为任何一个单元测试思考命名问题。 35 | 36 | ![单元测试结果页](/docs/images/2018-02-12-08-54-31.png) 37 | 38 | ## 参数化的单元测试 39 | 40 | 有些契约需要更多的值组合来验证正确性,那么可以在契约测试用例的后面添加参数。 41 | 42 | ```csharp 43 | "质数".Test((int num) => 44 | { 45 | // 测试用例代码 46 | }).WithArguments(2, 3, 5, 7, 11); 47 | 48 | "{0} 不是质数".Test((int num) => 49 | { 50 | // 测试用例代码 51 | }).WithArguments(1, 4); 52 | ``` 53 | 54 | 也可以添加多个参数(最多支持 8 个): 55 | 56 | ```csharp 57 | "契约 1,参数中可以带 {0} 和 {1}。".Test((int a, int b) => 58 | { 59 | // 现在,a 会等于 2,b 会等于 3。 60 | }).WithArguments(2, 3); 61 | 62 | "契约 2".Test((int a, int b) => 63 | { 64 | // 现在有两组代码,一组 a=2, b=3;另一组 a=10, b=20。 65 | // 当然也可以传入元组数组。 66 | }).WithArguments((2, 3), (10, 20)); 67 | ``` 68 | 69 | 在显示单元测试结果时,如果契约字符串中含有格式化占位符 `{0}`、`{1}` 等,会被自动替换为参数的值。 70 | 71 | ## 异步的单元测试 72 | 73 | `Test` 方法中传入的每个 `Action` 都支持 `async` 关键字,并会在执行测试用例时等待异步操作结束。 74 | 75 | ## 额外的黑科技 76 | 77 | MSTest v2 支持嵌套类型的单元测试。也就是说,我们可以利用这一点做出近乎无限层级的单元测试树出来。 78 | -------------------------------------------------------------------------------- /docs/zh-chs/README.zh-chs.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /docs/jp/README.jp.md 6 | [zh-chs]: /docs/zh-chs/README.zh-chs.md 7 | [zh-cht]: /docs/zh-cht/README.zh-cht.md 8 | 9 | # MSTestEnhancer 10 | 11 | 有没有觉得命名太难?有没有觉得单元测试的命名更难?没错,你不是一个人!看看这个你就知道了:[程序员最头疼的事:命名](http://blog.jobbole.com/50708/#rd?sukey=fc78a68049a14bb285ac0d81ca56806ac10192f4946a780ea3f3dd630804f86056e6fcfe6fcaeddb3dc04830b7e3b3eb) 或它的英文原文 [Don’t go into programming if you don’t have a good thesaurus - ITworld](https://www.itworld.com/article/2833265/cloud-computing/don-t-go-into-programming-if-you-don-t-have-a-good-thesaurus.html)。 12 | 13 | **MSTestEnhancer** 的出现将解决令你头疼的单元测试命名问题——因为,你再也不需要为任何单元测试方法命名了! 14 | 15 | MSTestEnhancer 是 MSTest v2 的一个扩展。使用它,你可以用契约的方式来描述一个又一个的测试用例,这些测试用例将在单元测试运行结束后显示到单元测试控制台或 GUI 窗口中。全过程你完全不需要为任何单元测试方法进行命名——你关注的,是测试用例本身。 16 | 17 | ## 新手入门 18 | 19 | 现在,你的单元测试可以这样写了: 20 | 21 | ```csharp 22 | [TestClass] 23 | public class DemoTest 24 | { 25 | [ContractTestCase] 26 | public void Foo() 27 | { 28 | "当满足 A 条件时,应该发生 A' 事。".Test(() => 29 | { 30 | // Arrange 31 | // Action 32 | // Assert 33 | }); 34 | 35 | "当满足 B 条件时,应该发生 B' 事。".Test(() => 36 | { 37 | // Arrange 38 | // Action 39 | // Assert 40 | }); 41 | } 42 | } 43 | ``` 44 | 45 | 于是,运行单元测试将看到这样的结果视图: 46 | 47 | ![单元测试运行结果](/docs/images/unit-test-result-of-demo.zh-chs.png) 48 | 49 | ## 开源社区需要你的加入 50 | 51 | // 编写中…… 52 | 53 | ### 发现并提出问题 54 | 55 | // 编写中…… 56 | 57 | ### 贡献你的代码 58 | 59 | // 编写中…… 60 | 61 | ## 许可协议 62 | 63 | [MIT 许可](./LICENSE) 64 | -------------------------------------------------------------------------------- /docs/zh-chs/resharper-code-templates.md: -------------------------------------------------------------------------------- 1 | # MSTestEnhancer 风格的 ReSharper 代码片段 2 | 3 | MSTest 有着独特的单元测试编写风格,这意味着目前并没有现成的工具提供这种风格单元测试的快速生成方案。但是你可以使用 ReSharper 手工导入我们预设好的代码片段来自动生成这种风格的代码。 4 | 5 | ## 目前提供的代码片段 6 | 7 | 目前提供了四种代码片段: 8 | 9 | - [生成单元测试类](#生成单元测试文件和类) 10 | - [生成测试方法](#生成单元测试方法) 11 | - [生成测试用例](#生成单元测试用例) 12 | - [转换现有测试到 MSTestEnhancer 风格](#%E8%BD%AC%E6%8D%A2%E7%8E%B0%E6%9C%89%E7%9A%84%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E5%88%B0-mstestenhancer-%E9%A3%8E%E6%A0%BC) 13 | 14 | ### 生成单元测试文件和类 15 | 16 | 如果你使用 Visual Studio(带 ReSharper 插件)开发,那么在项目上或者项目的文件夹上点击右键 → `添加` → `New from Template` → `Contract Unit Test` 即可添加一个 MSTestEnhancer 风格的单元测试类。 17 | 18 | ![文件模板](/docs/images/2018-02-27-11-11-10.png) 19 | ▲ 根据模板创建类 20 | 21 | ![填写文件名](/docs/images/2018-02-27-11-15-09.png) 22 | ▲ 填写文件名(正好也是类名) 23 | 24 | 添加好的类会默认选中那段契约文字,这样可以立刻开始编写或者粘贴契约。 25 | 26 | ![生成的类](/docs/images/2018-02-27-11-32-59.png) 27 | ▲ 生成的类 28 | 29 | ### 生成单元测试方法 30 | 31 | 在类中输入 `test` 会出现 `Insert a test method` 代码片段。敲回车或者 Tab 便能够插入单元测试方法。 32 | 33 | ![Insert a test method 代码片段](/docs/images/2018-02-27-11-36-50.png) 34 | ▲ 输入 test(并不需要完整输入,ReSharper 还是很厉害的) 35 | 36 | ![生成的方法](/docs/images/2018-02-27-11-33-49.png) 37 | ▲ 生成的方法(命名空间会自动加入) 38 | 39 | ### 生成单元测试用例 40 | 41 | 使用 MSTestEnhancer 风格编写单元测试后,一个方法内部可以包含多个单元测试用例了。所以如果在方法内部输入 `test` 会出现 `Insert a test case` 代码片段。敲回车或者 Tab 便能够插入单元测试用例。 42 | 43 | ![Insert a test case 代码片段](/docs/images/2018-02-27-11-38-16.png) 44 | ▲ 输入 test(同样不需要完整输入) 45 | 46 | ![生成的测试用例](/docs/images/2018-02-27-11-39-09.png) 47 | ▲ 生成的测试用例 48 | 49 | ### 转换现有的单元测试用例到 MSTestEnhancer 风格 50 | 51 | 如果项目中已经存在传统风格的单元测试,可以使用此代码片段将测试用例转换成 MSTestEnhancer 风格。 52 | 53 | 1. 选中传统风格的测试用例。 54 | 1. 直接输入 `test`,不用担心覆盖掉刚刚选中的代码。 55 | 1. 敲回车或者 Tab 便会转换刚刚选中的代码到 MSTestEnhancer 风格。 56 | 57 | ![选中](/docs/images/2018-02-27-11-48-07.png) 58 | ▲ 选中测试用例代码(可使用多次 Ctrl + W 快捷键快速选中) 59 | 60 | ![Convert to MSTestEnhancer test case 代码片段](/docs/images/2018-02-27-11-56-40.png) 61 | ▲ 输入 `test` 转换测试用例 62 | 63 | ![转换的测试用例](/docs/images/2018-02-27-11-51-43.png) 64 | ▲ 转换的测试用例 65 | 66 | 步骤 2 也可以替换成菜单操作:按下 Ctrl + Enter,选择 `Surround with...` → `test`。 67 | 68 | ![使用菜单操作](/docs/images/2018-02-27-11-50-18.png) 69 | ▲ 使用菜单操作(当然效率会低很多) 70 | 71 | ## 将代码片段导入到我的 ReSharper 72 | 73 | 你可以在 [MSTestEnhancer/MSTest.Extensions.sln.DotSettings](/MSTest.Extensions.sln.DotSettings) 文件中找到我们预设好的 ReSharper 代码片段。不过这种格式的文件人类可读性较差,我们更推荐你使用导入的方式来使用这些代码片段。 74 | 75 | ### 如何导入到我的 ReSharper 76 | 77 | 在 ReSharper 菜单中选择 `Tools` → `Templates Explorer` 打开模板浏览器。 78 | 79 | ![Templates Explorer](/docs/images/2018-02-27-11-59-28.png) 80 | 81 | 依次选中所有种类的模板标签,点击 `导入`,选择从本仓库下载的 [MSTestEnhancer/MSTest.Extensions.sln.DotSettings](/MSTest.Extensions.sln.DotSettings) 文件即可。 82 | 83 | ![导入](/docs/images/2018-02-27-12-01-29.png) 84 | 85 | 在 File Templates 标签的工具栏中选择漏斗状的图标,保持 `Show Predefined Templates` 为选中状态。然后,将 这样可以将我们刚刚导入的模板设置到快速新建的菜单中(也就是本文一开始就贴出的那张添加文件和类的图)。 86 | 87 | ![Show Predefined Templates](/docs/images/2018-02-27-12-16-05.png) 88 | ▲ 选中 `显示预定义的模板` 89 | 90 | ![拖拽到快速列表中](/docs/images/2018-02-27-12-19-51.png) 91 | ▲ 将导入的模板拖拽到快速列表中 92 | -------------------------------------------------------------------------------- /docs/zh-cht/README.md: -------------------------------------------------------------------------------- 1 | # MSTest Enhancer 2 | 3 | 在 MSTestEnhancer 的幫助下,單元測試將更加容易編寫、理解和維護。因為它讓單元測試方法和被測類本身之間的關係更加緊密。 4 | 5 | ## 如何開始 6 | 7 | 1. 在單元測試項目中安裝 NuGet 包:[MSTestEnhancer](https://www.nuget.org/packages/MSTestEnhancer/)。 8 | 1. 現在就可以開始編寫下文那種風格的單元測試代碼了。 9 | 10 | ## 推薦的單元測試編寫方式 11 | 12 | 假設你希望測試一個名為 `被測類名` 的類,其中包含一個名為 `被測方法名` 的方法。你可以這樣編寫單元測試: 13 | 14 | ```csharp 15 | [TestClass] 16 | public class 被測類名Test 17 | { 18 |     [ContractTestCase] 19 |     public void 被測方法名() 20 |     { 21 |         "契約 1(當 Xxx 時,應該發生 Yyy)".Test(() => 22 |         { 23 |             // 測試用例代碼 24 |         }); 25 |          26 |         "契約 2(但當 Zzz 時,應該發生 Www)".Test(() => 27 |         { 28 |             // 測試用例代碼 29 |         }); 30 |     } 31 | } 32 | ``` 33 | 34 | 注意到單元測試類的名稱和原本的類名稱是一一對應的,方法名和原本的方法名是一模一樣的;於是,我們不再需要為任何一個單元測試思考命名問題。 35 | 36 | ![單元測試結果頁](/docs/images/2018-02-13-14-35-29.png) 37 | 38 | ## 參數化的單元測試 39 | 40 | 有些契約需要更多的值組合來驗證正確性,那麼可以在契約測試用例的後面添加參數。 41 | 42 | ```csharp 43 | "質數".Test((int num) => 44 | { 45 |     // 測試用例代碼 46 | }).WithArguments(2, 3, 5, 7, 11); 47 | 48 | "{0} 不是質數".Test((int num) => 49 | { 50 |     // 測試用例代碼 51 | }).WithArguments(1, 4); 52 | ``` 53 | 54 | 也可以添加多個參數(最多支持 8 個): 55 | 56 | ```csharp 57 | "契約 1,參數中可以帶 {0} 和 {1}。".Test((int a, int b) => 58 | { 59 |     // 現在,a 會等於 2,b 會等於 3。 60 | }).WithArguments(2, 3); 61 | 62 | "契約 2".Test((int a, int b) => 63 | { 64 |     // 現在有兩組代碼,一組 a=2, b=3;另一組 a=10, b=20。 65 |     // 當然也可以傳入元組數組。 66 | }).WithArguments((2, 3), (10, 20)); 67 | ``` 68 | 69 | 在顯示單元測試結果時,如果契約字符串中含有格式化佔位符 `{0}`、`{1}` 等,會被自動替換為參數的值。 70 | 71 | ## 異步的單元測試 72 | 73 | `Test` 方法中傳入的每個 `Action` 都支持 `async` 關鍵字,並會在執行測試用例時等待異步操作結束。 74 | 75 | ## 額外的黑科技 76 | 77 | MSTest v2 支持嵌套類型的單元測試。也就是說,我們可以利用這一點做出近乎無限層級的單元測試樹出來。 78 | -------------------------------------------------------------------------------- /docs/zh-cht/README.zh-cht.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /docs/jp/README.jp.md 6 | [zh-chs]: /docs/zh-chs/README.zh-chs.md 7 | [zh-cht]: /docs/zh-cht/README.zh-cht.md 8 | 9 | # MSTestEnhancer 10 | 11 | 有沒有覺得命名太難?有沒有覺得單元測試的命名更難?沒錯,你不是一個人!看看這個你就知道了:[程序員最頭疼的事:命名](http://blog.jobbole.com/50708/#rd?sukey=fc78a68049a14bb285ac0d81ca56806ac10192f4946a780ea3f3dd630804f86056e6fcfe6fcaeddb3dc04830b7e3b3eb) 或它的英文原文 [Don't go into programming if you don't have a good thesaurus - ITworld](https://www.itworld.com/article/2833265/cloud-computing/don-t-go-into-programming-if-you-don-t-have-a-good-thesaurus.html)。 12 | 13 | **MSTestEnhancer** 的出現將解決令你頭疼的單元測試命名問題——因為,你再也不需要為任何單元測試方法命名了! 14 | 15 | MSTestEnhancer 是 MSTest v2 的一個擴展。使用它,你可以用契約的方式來描述一個又一個的測試用例,這些測試用例將在單元測試運行結束後顯示到單元測試控制台或 GUI 窗口中。全過程你完全不需要為任何單元測試方法進行命名——你關注的,是測試用例本身。 16 | 17 | ## 新手入門 18 | 19 | 現在,你的單元測試可以這樣寫了: 20 | 21 | ```csharp 22 | [TestClass] 23 | public class DemoTest 24 | { 25 | [ContractTestCase] 26 | public void Foo() 27 | { 28 | "當滿足 A 條件時,應該發生 X 事。".Test(() => 29 | { 30 | // Arrange 31 | // Action 32 | // Assert 33 | }); 34 | 35 | "但是當滿足 B 條件時,應該發生 Y 事。".Test(() => 36 | { 37 | // Arrange 38 | // Action 39 | // Assert 40 | }); 41 | } 42 | } 43 | ``` 44 | 45 | 於是,運行單元測試將看到這樣的結果視圖: 46 | 47 | ![單元測試運行結果](/docs/images/unit-test-result-of-demo.zh-cht.png) 48 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/icon.png -------------------------------------------------------------------------------- /src/MSTest.Extensions/AssertExtensions/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace MSTest.Extensions.AssertExtensions 4 | { 5 | /// 6 | /// A set of assertion libraries that support chained syntax 7 | /// 8 | public static class AssertExtensions 9 | { 10 | /// 11 | /// Tests whether the specified object is an instance of the expected 12 | /// type and throws an exception if the expected type is not in the 13 | /// inheritance hierarchy of the object. 14 | /// 15 | /// 16 | /// 17 | /// The object the test expects to be of the specified type. 18 | /// 19 | /// 20 | /// The expected type of . 21 | /// 22 | /// 23 | /// The message to include in the exception when 24 | /// is not an instance of . The message is 25 | /// shown in test results. 26 | /// 27 | /// 28 | /// Thrown if is null or 29 | /// is not in the inheritance hierarchy 30 | /// of . 31 | /// 32 | public static Assert IsInstanceOfType(this Assert assert, object value, string message = "") 33 | { 34 | Assert.IsInstanceOfType(value, typeof(T)); 35 | return assert; 36 | } 37 | 38 | /// 39 | /// Tests whether the specified object is not an instance of the wrong 40 | /// type and throws an exception if the specified type is in the 41 | /// inheritance hierarchy of the object. 42 | /// 43 | /// 44 | /// 45 | /// The object the test expects not to be of the specified type. 46 | /// 47 | /// 48 | /// The type that should not be. 49 | /// 50 | /// 51 | /// The message to include in the exception when 52 | /// is an instance of . The message is shown 53 | /// in test results. 54 | /// 55 | /// 56 | /// Thrown if is not null and 57 | /// is in the inheritance hierarchy 58 | /// of . 59 | /// 60 | public static Assert IsNotInstanceOfType(this Assert assert, object value, string message = "") 61 | { 62 | Assert.IsNotInstanceOfType(value, typeof(T), message, null); 63 | return assert; 64 | } 65 | 66 | /// 67 | /// Tests whether the specified object is null and throws an exception 68 | /// if it is not. 69 | /// 70 | /// 71 | /// 72 | /// The object the test expects to be null. 73 | /// 74 | /// 75 | /// The message to include in the exception when 76 | /// is not null. The message is shown in test results. 77 | /// 78 | /// 79 | /// Thrown if is not null. 80 | /// 81 | public static Assert IsNullT(this Assert assert, [CanBeNull]object value, string message = "") 82 | { 83 | Assert.IsNull(value); 84 | return assert; 85 | } 86 | 87 | /// 88 | /// Tests whether the specified condition is true and throws an exception 89 | /// if the condition is false. 90 | /// 91 | /// 92 | /// 93 | /// The condition the test expects to be true. 94 | /// 95 | /// 96 | /// The message to include in the exception when 97 | /// is false. The message is shown in test results. 98 | /// 99 | /// 100 | /// Thrown if is false. 101 | /// 102 | public static Assert IsTrueT(this Assert assert, bool condition, string message = "") 103 | { 104 | Assert.IsTrue(condition, message); 105 | return assert; 106 | } 107 | 108 | /// 109 | /// Tests whether the specified condition is false and throws an exception 110 | /// if the condition is true. 111 | /// 112 | /// 113 | /// 114 | /// The condition the test expects to be false. 115 | /// 116 | /// 117 | /// The message to include in the exception when 118 | /// is true. The message is shown in test results. 119 | /// 120 | /// 121 | /// Thrown if is true. 122 | /// 123 | public static Assert IsFalseT(this Assert assert, bool condition, string message = "") 124 | { 125 | Assert.IsFalse(condition, message); 126 | return assert; 127 | } 128 | 129 | /// 130 | /// Tests whether the specified object is non-null and throws an exception 131 | /// if it is null. 132 | /// 133 | /// 134 | /// 135 | /// The object the test expects not to be null. 136 | /// 137 | /// 138 | /// The message to include in the exception when 139 | /// is null. The message is shown in test results. 140 | /// 141 | /// 142 | /// Thrown if is null. 143 | /// 144 | public static Assert IsNotNullT(this Assert assert, [CanBeNull]object value, string message = "") 145 | { 146 | Assert.IsNotNull(value, message, null); 147 | return assert; 148 | } 149 | 150 | /// 151 | /// Tests whether the specified objects both refer to the same object and 152 | /// throws an exception if the two inputs do not refer to the same object. 153 | /// 154 | /// 155 | /// 156 | /// The first object to compare. This is the value the test expects. 157 | /// 158 | /// 159 | /// The second object to compare. This is the value produced by the code under test. 160 | /// 161 | /// 162 | /// The message to include in the exception when 163 | /// is not the same as . The message is shown 164 | /// in test results. 165 | /// 166 | /// 167 | /// Thrown if does not refer to the same object 168 | /// as . 169 | /// 170 | public static Assert AreSameT(this Assert assert, object expected, object actual, string message = "") 171 | { 172 | Assert.AreSame(expected, actual, message, null); 173 | return assert; 174 | } 175 | 176 | /// 177 | /// Tests whether the specified objects refer to different objects and 178 | /// throws an exception if the two inputs refer to the same object. 179 | /// 180 | /// 181 | /// 182 | /// The first object to compare. This is the value the test expects not 183 | /// to match . 184 | /// 185 | /// 186 | /// The second object to compare. This is the value produced by the code under test. 187 | /// 188 | /// 189 | /// The message to include in the exception when 190 | /// is the same as . The message is shown in 191 | /// test results. 192 | /// 193 | /// 194 | /// Thrown if refers to the same object 195 | /// as . 196 | /// 197 | public static Assert AreNotSameT(this Assert assert, object notExpected, object actual, string message) 198 | { 199 | Assert.AreNotSame(notExpected, actual, message, null); 200 | return assert; 201 | } 202 | 203 | /// 204 | /// Tests whether the specified values are equal and throws an exception 205 | /// if the two values are not equal. Different numeric types are treated 206 | /// as unequal even if the logical values are equal. 42L is not equal to 42. 207 | /// 208 | /// 209 | /// The type of values to compare. 210 | /// 211 | /// 212 | /// 213 | /// The first value to compare. This is the value the tests expects. 214 | /// 215 | /// 216 | /// The second value to compare. This is the value produced by the code under test. 217 | /// 218 | /// 219 | /// The message to include in the exception when 220 | /// is not equal to . The message is shown in 221 | /// test results. 222 | /// 223 | /// 224 | /// Thrown if is not equal to 225 | /// . 226 | /// 227 | public static Assert AreEqualT(this Assert assert, T expected, T actual, string message = "") 228 | { 229 | Assert.AreEqual(expected, actual, message, null); 230 | return assert; 231 | } 232 | 233 | /// 234 | /// Tests whether the specified values are unequal and throws an exception 235 | /// if the two values are equal. Different numeric types are treated 236 | /// as unequal even if the logical values are equal. 42L is not equal to 42. 237 | /// 238 | /// 239 | /// The type of values to compare. 240 | /// 241 | /// 242 | /// 243 | /// The first value to compare. This is the value the test expects not 244 | /// to match . 245 | /// 246 | /// 247 | /// The second value to compare. This is the value produced by the code under test. 248 | /// 249 | /// 250 | /// The message to include in the exception when 251 | /// is equal to . The message is shown in 252 | /// test results. 253 | /// 254 | /// 255 | /// Thrown if is equal to . 256 | /// 257 | public static Assert AreNotEqualT(this Assert assert, T notExpected, T actual, string message = "") 258 | { 259 | Assert.AreNotEqual(notExpected, actual, message, null); 260 | return assert; 261 | } 262 | 263 | /// 264 | /// Tests whether the specified doubles are equal and throws an exception 265 | /// if they are not equal. 266 | /// 267 | /// 268 | /// 269 | /// The first double to compare. This is the double the tests expects. 270 | /// 271 | /// 272 | /// The second double to compare. This is the double produced by the code under test. 273 | /// 274 | /// 275 | /// The required accuracy. An exception will be thrown only if 276 | /// is different than 277 | /// by more than . 278 | /// 279 | /// 280 | /// The message to include in the exception when 281 | /// is different than by more than 282 | /// . The message is shown in test results. 283 | /// 284 | /// 285 | /// Thrown if is not equal to . 286 | /// 287 | public static Assert AreEqualT(this Assert assert, double expected, double actual, double delta, string message = "") 288 | { 289 | Assert.AreEqual(expected, actual, delta, message, null); 290 | return assert; 291 | } 292 | 293 | /// 294 | /// Tests whether the specified doubles are unequal and throws an exception 295 | /// if they are equal. 296 | /// 297 | /// 298 | /// 299 | /// The first double to compare. This is the double the test expects not to 300 | /// match . 301 | /// 302 | /// 303 | /// The second double to compare. This is the double produced by the code under test. 304 | /// 305 | /// 306 | /// The required accuracy. An exception will be thrown only if 307 | /// is different than 308 | /// by at most . 309 | /// 310 | /// 311 | /// The message to include in the exception when 312 | /// is equal to or different by less than 313 | /// . The message is shown in test results. 314 | /// 315 | /// 316 | /// Thrown if is equal to . 317 | /// 318 | public static Assert AreNotEqualT(this Assert assert, double notExpected, double actual, double delta, string message = "") 319 | { 320 | Assert.AreNotEqual(notExpected, actual, delta, message, null); 321 | return assert; 322 | } 323 | 324 | /// 325 | /// Tests whether the specified strings are equal and throws an exception 326 | /// if they are not equal. The invariant culture is used for the comparison. 327 | /// 328 | /// 329 | /// 330 | /// The first string to compare. This is the string the tests expects. 331 | /// 332 | /// 333 | /// The second string to compare. This is the string produced by the code under test. 334 | /// 335 | /// 336 | /// A Boolean indicating a case-sensitive or insensitive comparison. (true 337 | /// indicates a case-insensitive comparison.) 338 | /// 339 | /// 340 | /// The message to include in the exception when 341 | /// is not equal to . The message is shown in 342 | /// test results. 343 | /// 344 | /// 345 | /// Thrown if is not equal to . 346 | /// 347 | public static Assert AreEqualT(this Assert assert, string expected, string actual, bool ignoreCase, string message = "") 348 | { 349 | Assert.AreEqual(expected, actual, ignoreCase, message, null); 350 | return assert; 351 | } 352 | 353 | /// 354 | /// Tests whether the specified strings are unequal and throws an exception 355 | /// if they are equal. The invariant culture is used for the comparison. 356 | /// 357 | /// 358 | /// 359 | /// The first string to compare. This is the string the test expects not to 360 | /// match . 361 | /// 362 | /// 363 | /// The second string to compare. This is the string produced by the code under test. 364 | /// 365 | /// 366 | /// A Boolean indicating a case-sensitive or insensitive comparison. (true 367 | /// indicates a case-insensitive comparison.) 368 | /// 369 | /// 370 | /// The message to include in the exception when 371 | /// is equal to . The message is shown in 372 | /// test results. 373 | /// 374 | /// 375 | /// Thrown if is equal to . 376 | /// 377 | public static Assert AreNotEqualT(this Assert assert, string notExpected, string actual, bool ignoreCase, string message = "") 378 | { 379 | Assert.AreNotEqual(notExpected, actual, ignoreCase, message, null); 380 | return assert; 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/AssertExtensions/CollectionAssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace MSTest.Extensions.AssertExtensions 6 | { 7 | /// 8 | /// A set of assertion libraries that support chained syntax 9 | /// 10 | public static class CollectionAssertExtensions 11 | { 12 | /// 13 | /// Tests whether the specified collection contains the specified element 14 | /// and throws an exception if the element is not in the collection. 15 | /// 16 | /// 17 | /// 18 | /// The collection in which to search for the element. 19 | /// 20 | /// 21 | /// The element that is expected to be in the collection. 22 | /// 23 | /// 24 | /// The message to include in the exception when 25 | /// is not in . The message is shown in 26 | /// test results. 27 | /// 28 | /// 29 | /// Thrown if is not found in 30 | /// . 31 | /// 32 | public static CollectionAssert ContainsT(this CollectionAssert collectionAssert, ICollection collection, object element, string message = "") 33 | { 34 | CollectionAssert.Contains(collection, element, message, null); 35 | return collectionAssert; 36 | } 37 | 38 | /// 39 | /// Tests whether the specified collection does not contain the specified 40 | /// element and throws an exception if the element is in the collection. 41 | /// 42 | /// 43 | /// 44 | /// The collection in which to search for the element. 45 | /// 46 | /// 47 | /// The element that is expected not to be in the collection. 48 | /// 49 | /// 50 | /// The message to include in the exception when 51 | /// is in . The message is shown in test 52 | /// results. 53 | /// 54 | /// 55 | /// Thrown if is found in 56 | /// . 57 | /// 58 | public static CollectionAssert DoesNotContainT(this CollectionAssert collectionAssert, ICollection collection, object element, string message = "") 59 | { 60 | CollectionAssert.DoesNotContain(collection, element, message, null); 61 | return collectionAssert; 62 | } 63 | 64 | /// 65 | /// Tests whether all items in the specified collection are non-null and throws 66 | /// an exception if any element is null. 67 | /// 68 | /// 69 | /// 70 | /// The collection in which to search for null elements. 71 | /// 72 | /// 73 | /// The message to include in the exception when 74 | /// contains a null element. The message is shown in test results. 75 | /// 76 | /// 77 | /// Thrown if a null element is found in . 78 | /// 79 | public static CollectionAssert AllItemsAreNotNullT(this CollectionAssert collectionAssert, ICollection collection, string message = "") 80 | { 81 | CollectionAssert.AllItemsAreNotNull(collection, message, null); 82 | return collectionAssert; 83 | } 84 | 85 | /// 86 | /// Tests whether all items in the specified collection are unique or not and 87 | /// throws if any two elements in the collection are equal. 88 | /// 89 | /// 90 | /// 91 | /// The collection in which to search for duplicate elements. 92 | /// 93 | /// 94 | /// The message to include in the exception when 95 | /// contains at least one duplicate element. The message is shown in 96 | /// test results. 97 | /// 98 | /// 99 | /// Thrown if a two or more equal elements are found in 100 | /// . 101 | /// 102 | public static CollectionAssert AllItemsAreUniqueT(this CollectionAssert collectionAssert, ICollection collection, string message = "") 103 | { 104 | CollectionAssert.AllItemsAreUnique(collection, message, null); 105 | return collectionAssert; 106 | } 107 | 108 | /// 109 | /// Tests whether one collection is a subset of another collection and 110 | /// throws an exception if any element in the subset is not also in the 111 | /// superset. 112 | /// 113 | /// 114 | /// 115 | /// The collection expected to be a subset of . 116 | /// 117 | /// 118 | /// The collection expected to be a superset of 119 | /// 120 | /// 121 | /// The message to include in the exception when an element in 122 | /// is not found in . 123 | /// The message is shown in test results. 124 | /// 125 | /// 126 | /// Thrown if an element in is not found in 127 | /// . 128 | /// 129 | public static CollectionAssert IsSubsetOfT(this CollectionAssert collectionAssert, ICollection subset, ICollection superset, string message = "") 130 | { 131 | CollectionAssert.IsSubsetOf(subset, superset, message, null); 132 | return collectionAssert; 133 | } 134 | 135 | /// 136 | /// Tests whether one collection is not a subset of another collection and 137 | /// throws an exception if all elements in the subset are also in the 138 | /// superset. 139 | /// 140 | /// 141 | /// 142 | /// The collection expected not to be a subset of . 143 | /// 144 | /// 145 | /// The collection expected not to be a superset of 146 | /// 147 | /// 148 | /// The message to include in the exception when every element in 149 | /// is also found in . 150 | /// The message is shown in test results. 151 | /// 152 | /// 153 | /// Thrown if every element in is also found in 154 | /// . 155 | /// 156 | public static CollectionAssert IsNotSubsetOfT(this CollectionAssert collectionAssert, ICollection subset, ICollection superset, string message = "") 157 | { 158 | CollectionAssert.IsNotSubsetOf(subset, superset, message, null); 159 | return collectionAssert; 160 | } 161 | 162 | /// 163 | /// Tests whether two collections contain the same elements and throws an 164 | /// exception if either collection contains an element not in the other 165 | /// collection. 166 | /// 167 | /// 168 | /// 169 | /// The first collection to compare. This contains the elements the test 170 | /// expects. 171 | /// 172 | /// 173 | /// The second collection to compare. This is the collection produced by 174 | /// the code under test. 175 | /// 176 | /// 177 | /// The message to include in the exception when an element was found 178 | /// in one of the collections but not the other. The message is shown 179 | /// in test results. 180 | /// 181 | /// 182 | /// Thrown if an element was found in one of the collections but not 183 | /// the other. 184 | /// 185 | public static CollectionAssert AreEquivalentT(this CollectionAssert collectionAssert, ICollection expected, ICollection actual, string message = "") 186 | { 187 | CollectionAssert.AreEquivalent(expected, actual, message, null); 188 | return collectionAssert; 189 | } 190 | 191 | /// 192 | /// Tests whether two collections contain the different elements and throws an 193 | /// exception if the two collections contain identical elements without regard 194 | /// to order. 195 | /// 196 | /// 197 | /// 198 | /// The first collection to compare. This contains the elements the test 199 | /// expects to be different than the actual collection. 200 | /// 201 | /// 202 | /// The second collection to compare. This is the collection produced by 203 | /// the code under test. 204 | /// 205 | /// 206 | /// The message to include in the exception when 207 | /// contains the same elements as . The message 208 | /// is shown in test results. 209 | /// 210 | /// 211 | /// Thrown if the two collections contained the same elements, including 212 | /// the same number of duplicate occurrences of each element. 213 | /// 214 | public static CollectionAssert AreNotEquivalentT(this CollectionAssert collectionAssert, ICollection expected, ICollection actual, string message = "") 215 | { 216 | CollectionAssert.AreNotEquivalent(expected, actual, message, null); 217 | return collectionAssert; 218 | } 219 | 220 | /// 221 | /// Tests whether all elements in the specified collection are instances 222 | /// of the expected type and throws an exception if the expected type is 223 | /// not in the inheritance hierarchy of one or more of the elements. 224 | /// 225 | /// 226 | /// 227 | /// The collection containing elements the test expects to be of the 228 | /// specified type. 229 | /// 230 | /// 231 | /// The expected type of each element of . 232 | /// 233 | /// 234 | /// The message to include in the exception when an element in 235 | /// is not an instance of 236 | /// . The message is shown in test results. 237 | /// 238 | /// 239 | /// Thrown if an element in is null or 240 | /// is not in the inheritance hierarchy 241 | /// of an element in . 242 | /// 243 | public static CollectionAssert AllItemsAreInstancesOfTypeT(this CollectionAssert collectionAssert, ICollection collection, Type expectedType, string message = "") 244 | { 245 | CollectionAssert.AllItemsAreInstancesOfType(collection, expectedType, message, null); 246 | return collectionAssert; 247 | } 248 | 249 | /// 250 | /// Tests whether the specified collections are equal and throws an exception 251 | /// if the two collections are not equal. Equality is defined as having the same 252 | /// elements in the same order and quantity. Different references to the same 253 | /// value are considered equal. 254 | /// 255 | /// 256 | /// 257 | /// The first collection to compare. This is the collection the tests expects. 258 | /// 259 | /// 260 | /// The second collection to compare. This is the collection produced by the 261 | /// code under test. 262 | /// 263 | /// 264 | /// The message to include in the exception when 265 | /// is not equal to . The message is shown in 266 | /// test results. 267 | /// 268 | /// 269 | /// Thrown if is not equal to 270 | /// . 271 | /// 272 | public static CollectionAssert AreEqualT(this CollectionAssert collectionAssert, ICollection expected, ICollection actual, string message = "") 273 | { 274 | CollectionAssert.AreEqual(expected, actual, message, null); 275 | return collectionAssert; 276 | } 277 | 278 | /// 279 | /// Tests whether the specified collections are unequal and throws an exception 280 | /// if the two collections are equal. Equality is defined as having the same 281 | /// elements in the same order and quantity. Different references to the same 282 | /// value are considered equal. 283 | /// 284 | /// 285 | /// 286 | /// The first collection to compare. This is the collection the tests expects 287 | /// not to match . 288 | /// 289 | /// 290 | /// The second collection to compare. This is the collection produced by the 291 | /// code under test. 292 | /// 293 | /// 294 | /// The message to include in the exception when 295 | /// is equal to . The message is shown in 296 | /// test results. 297 | /// 298 | /// 299 | /// Thrown if is equal to . 300 | /// 301 | public static CollectionAssert AreNotEqualT(this CollectionAssert collectionAssert, ICollection notExpected, ICollection actual, string message = "") 302 | { 303 | CollectionAssert.AreNotEqual(notExpected, actual, message, null); 304 | return collectionAssert; 305 | } 306 | 307 | /// 308 | /// Tests whether the specified collections are equal and throws an exception 309 | /// if the two collections are not equal. Equality is defined as having the same 310 | /// elements in the same order and quantity. Different references to the same 311 | /// value are considered equal. 312 | /// 313 | /// 314 | /// 315 | /// The first collection to compare. This is the collection the tests expects. 316 | /// 317 | /// 318 | /// The second collection to compare. This is the collection produced by the 319 | /// code under test. 320 | /// 321 | /// 322 | /// The compare implementation to use when comparing elements of the collection. 323 | /// 324 | /// 325 | /// The message to include in the exception when 326 | /// is not equal to . The message is shown in 327 | /// test results. 328 | /// 329 | /// 330 | /// Thrown if is not equal to 331 | /// . 332 | /// 333 | public static CollectionAssert AreEqualT(this CollectionAssert collectionAssert, ICollection expected, ICollection actual, IComparer comparer, string message = "") 334 | { 335 | CollectionAssert.AreEqual(expected, actual, comparer, message, null); 336 | return collectionAssert; 337 | } 338 | 339 | /// 340 | /// Tests whether the specified collections are unequal and throws an exception 341 | /// if the two collections are equal. Equality is defined as having the same 342 | /// elements in the same order and quantity. Different references to the same 343 | /// value are considered equal. 344 | /// 345 | /// 346 | /// 347 | /// The first collection to compare. This is the collection the tests expects 348 | /// not to match . 349 | /// 350 | /// 351 | /// The second collection to compare. This is the collection produced by the 352 | /// code under test. 353 | /// 354 | /// 355 | /// The compare implementation to use when comparing elements of the collection. 356 | /// 357 | /// 358 | /// The message to include in the exception when 359 | /// is equal to . The message is shown in 360 | /// test results. 361 | /// 362 | /// 363 | /// Thrown if is equal to . 364 | /// 365 | public static CollectionAssert AreNotEqualT(this CollectionAssert collectionAssert, ICollection notExpected, ICollection actual, IComparer comparer, string message = "") 366 | { 367 | CollectionAssert.AreNotEqual(notExpected, actual, comparer, message, null); 368 | return collectionAssert; 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/AssertExtensions/StringAssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace MSTest.Extensions.AssertExtensions 5 | { 6 | /// 7 | /// A set of assertion libraries that support chained syntax 8 | /// 9 | public static class StringAssertExtensions 10 | { 11 | /// 12 | /// Tests whether the specified string contains the specified substring 13 | /// and throws an exception if the substring does not occur within the 14 | /// test string. 15 | /// 16 | /// 17 | /// 18 | /// The string that is expected to contain . 19 | /// 20 | /// 21 | /// The string expected to occur within . 22 | /// 23 | /// 24 | /// The message to include in the exception when 25 | /// is not in . The message is shown in 26 | /// test results. 27 | /// 28 | /// 29 | /// Thrown if is not found in 30 | /// . 31 | /// 32 | public static StringAssert ContainsT(this StringAssert stringAssert, string value, string substring, string message = "") 33 | { 34 | StringAssert.Contains(value, substring, message, null); 35 | return stringAssert; 36 | } 37 | 38 | /// 39 | /// Tests whether the specified string begins with the specified substring 40 | /// and throws an exception if the test string does not start with the 41 | /// substring. 42 | /// 43 | /// 44 | /// 45 | /// The string that is expected to begin with . 46 | /// 47 | /// 48 | /// The string expected to be a prefix of . 49 | /// 50 | /// 51 | /// The message to include in the exception when 52 | /// does not begin with . The message is 53 | /// shown in test results. 54 | /// 55 | /// 56 | /// Thrown if does not begin with 57 | /// . 58 | /// 59 | public static StringAssert StartsWithT(this StringAssert stringAssert, string value, string substring, string message = "") 60 | { 61 | StringAssert.StartsWith(value, substring, message, null); 62 | return stringAssert; 63 | } 64 | 65 | /// 66 | /// Tests whether the specified string ends with the specified substring 67 | /// and throws an exception if the test string does not end with the 68 | /// substring. 69 | /// 70 | /// 71 | /// 72 | /// The string that is expected to end with . 73 | /// 74 | /// 75 | /// The string expected to be a suffix of . 76 | /// 77 | /// 78 | /// The message to include in the exception when 79 | /// does not end with . The message is 80 | /// shown in test results. 81 | /// 82 | /// 83 | /// Thrown if does not end with 84 | /// . 85 | /// 86 | public static StringAssert EndsWithT(this StringAssert stringAssert, string value, string substring, string message = "") 87 | { 88 | StringAssert.EndsWith(value, substring, message, null); 89 | return stringAssert; 90 | } 91 | 92 | /// 93 | /// Tests whether the specified string matches a regular expression and 94 | /// throws an exception if the string does not match the expression. 95 | /// 96 | /// 97 | /// 98 | /// The string that is expected to match . 99 | /// 100 | /// 101 | /// The regular expression that is 102 | /// expected to match. 103 | /// 104 | /// 105 | /// The message to include in the exception when 106 | /// does not match . The message is shown in 107 | /// test results. 108 | /// 109 | /// 110 | /// Thrown if does not match 111 | /// . 112 | /// 113 | public static StringAssert MatchesT(this StringAssert stringAssert, string value, Regex pattern, string message = "") 114 | { 115 | StringAssert.Matches(value, pattern, message, null); 116 | return stringAssert; 117 | } 118 | 119 | /// 120 | /// Tests whether the specified string does not match a regular expression 121 | /// and throws an exception if the string matches the expression. 122 | /// 123 | /// 124 | /// 125 | /// The string that is expected not to match . 126 | /// 127 | /// 128 | /// The regular expression that is 129 | /// expected to not match. 130 | /// 131 | /// 132 | /// The message to include in the exception when 133 | /// matches . The message is shown in test 134 | /// results. 135 | /// 136 | /// 137 | /// Thrown if matches . 138 | /// 139 | public static StringAssert DoesNotMatchT(this StringAssert stringAssert, string value, Regex pattern, string message = "") 140 | { 141 | StringAssert.DoesNotMatch(value, pattern, message, null); 142 | return stringAssert; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Contracts/ContractTest.01.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Threading.Tasks; 4 | using dotnetCampus.Runtime.CompilerServices; 5 | 6 | namespace MSTest.Extensions.Contracts 7 | { 8 | [GenerateGenericFromThis(From = 2, To = 4)] 9 | public static partial class ContractTest 10 | { 11 | /// 12 | /// Create a test case for the specified . 13 | /// 14 | /// The description of a test contract. 15 | /// The action which is used to test the contract. 16 | [System.Diagnostics.Contracts.Pure, NotNull, PublicAPI] 17 | public static ContractTestContext Test([NotNull] this string contract, [NotNull] Action testCase) 18 | { 19 | if (contract == null) throw new ArgumentNullException(nameof(contract)); 20 | if (testCase == null) throw new ArgumentNullException(nameof(testCase)); 21 | 22 | Contract.EndContractBlock(); 23 | 24 | return new ContractTestContext(contract, testCase); 25 | } 26 | 27 | /// 28 | /// Create an async test case for the specified . 29 | /// 30 | /// The description of a test contract. 31 | /// The async action which is used to test the contract. 32 | [System.Diagnostics.Contracts.Pure, NotNull, PublicAPI] 33 | public static ContractTestContext Test([NotNull] this string contract, [NotNull] Func testCase) 34 | { 35 | if (contract == null) throw new ArgumentNullException(nameof(contract)); 36 | if (testCase == null) throw new ArgumentNullException(nameof(testCase)); 37 | 38 | Contract.EndContractBlock(); 39 | 40 | return new ContractTestContext(contract, testCase); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Contracts/ContractTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Threading.Tasks; 4 | using MSTest.Extensions.Core; 5 | 6 | namespace MSTest.Extensions.Contracts 7 | { 8 | /// 9 | /// Contains methods to write contract based unit test code. 10 | /// 11 | public static partial class ContractTest 12 | { 13 | #region Style 1: "Contract".Test(() => { Test Case Code }) 14 | 15 | /// 16 | /// Create a test case for the specified . 17 | /// 18 | /// The description of a test contract. 19 | /// The action of the which is used to test the contract. 20 | [PublicAPI] 21 | public static void Test([NotNull] this string contract, [NotNull] Action testCase) 22 | { 23 | if (contract == null) throw new ArgumentNullException(nameof(contract)); 24 | if (testCase == null) throw new ArgumentNullException(nameof(testCase)); 25 | Contract.EndContractBlock(); 26 | 27 | Method.Current.Add(new ContractTestCase(contract, testCase)); 28 | } 29 | 30 | /// 31 | /// Create an async test case for the specified . 32 | /// 33 | /// The description of a test contract. 34 | /// The async action of the which is used to test the contract. 35 | [PublicAPI] 36 | public static void Test([NotNull] this string contract, [NotNull] Func testCase) 37 | { 38 | if (contract == null) throw new ArgumentNullException(nameof(contract)); 39 | if (testCase == null) throw new ArgumentNullException(nameof(testCase)); 40 | Contract.EndContractBlock(); 41 | 42 | Method.Current.Add(new ContractTestCase(contract, testCase)); 43 | } 44 | 45 | #endregion 46 | 47 | #region Style 2: await "Contract" Test Case Code 48 | 49 | ///// 50 | ///// Treat a string as test case contract description, and then treat the code below await as test case action. 51 | ///// 52 | ///// The description of a test contract. 53 | ///// 54 | //[EditorBrowsable(EditorBrowsableState.Never)] 55 | //public static IAwaiter GetAwaiter(this string contract) 56 | //{ 57 | // var result = new TestCaseAwaitable(contract); 58 | // Method.Current.Add(result); 59 | // return result; 60 | //} 61 | 62 | #endregion 63 | 64 | /// 65 | /// Gets all test case information that is collected or will be collected from the test method. 66 | /// 67 | [NotNull] 68 | internal static TestCaseIndexer Method { get; } = new TestCaseIndexer(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Contracts/ContractTestCaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Diagnostics.Contracts; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using MSTest.Extensions.Core; 10 | using MSTest.Extensions.Utils; 11 | 12 | // ## How it works? 13 | // 14 | // 1. When the MSTestv2 discover a method which is marked with a `TestMethodAttribute`, 15 | // it will search all `Attributes` to find out the one which derived from `ITestDataSource`. 16 | // 1. The first instance (#1) of `ContractTestCaseAttribute` will be created because it derived from `ITestDataSource`. 17 | // 1. `GetMethod` of #1 is called: 18 | // - Invoke the target unit test method. 19 | // - Collect all test cases that are created during the target unit test method invoking. 20 | // - Return an empty array with length equals to test case count to the MSTestv2 framework. 21 | // - *In this case, `Execute` would be called the same times to that array length.* 22 | // 1. The second instance (#2) of `ContractTestCaseAttribute` will be created because it derived from `TestMethodAttribute`. 23 | // 1. `Execute` of #2 and `GetDisplayName` of #1 will be called alternately by the MSTestv2 framework: 24 | // - When `Execute` is called, fetch a test case and run it to get the test result. 25 | // - When `GetDisplayName` is called, fetch a test case and get the contract description string from it. 26 | 27 | namespace MSTest.Extensions.Contracts 28 | { 29 | /// 30 | /// Enable the unit test writing style of `"contract string".Test(TestCaseAction)`. 31 | /// 32 | [PublicAPI] 33 | public class ContractTestCaseAttribute : TestMethodAttribute, ITestDataSource 34 | { 35 | #region Instance derived from TestMethodAttribute 36 | 37 | /// 38 | [NotNull] 39 | public override TestResult[] Execute([NotNull] ITestMethod testMethod) 40 | { 41 | if (_testMethodProxy is null) 42 | { 43 | _testMethodProxy = CreateTestMethodProxy(testMethod); 44 | } 45 | 46 | var result = _testMethodProxy.Invoke(null); 47 | return new[] { result }; 48 | } 49 | 50 | [NotNull] 51 | internal Task ExecuteAsync([NotNull] ITestMethod testMethod) 52 | { 53 | _testMethodProxy ??= CreateTestMethodProxy(testMethod); 54 | 55 | return _testMethodProxy.InvokeAsync(); 56 | } 57 | 58 | [NotNull] 59 | private protected virtual TestMethodProxy CreateTestMethodProxy([NotNull] ITestMethod testMethod) 60 | { 61 | return new TestMethodProxy(testMethod); 62 | } 63 | 64 | #endregion 65 | 66 | #region Instance derived from ITestDataSource 67 | 68 | /// 69 | /// When a unit test method is preparing to run, This method will be called 70 | /// to fetch all the test cases of target unit test method. 71 | /// 72 | /// Target unit test method. 73 | /// 74 | /// The parameter array which will be passed into the target unit test method. 75 | /// We don't need any parameter, so we return an all null array with length equals to test case count. 76 | /// 77 | [NotNull] 78 | public IEnumerable GetData([NotNull] MethodInfo methodInfo) 79 | { 80 | Contract.EndContractBlock(); 81 | 82 | // Collect all test cases from the target unit test method. 83 | Collect(methodInfo); 84 | 85 | var cases = ContractTest.Method[methodInfo]; 86 | VerifyContracts(cases); 87 | 88 | return Enumerable.Range(0, cases.Count).Select(x => (object[]) null); 89 | } 90 | 91 | /// 92 | /// Each time after is called, this method will be called 93 | /// to retrieve the display name of the test case. 94 | /// 95 | /// Target unit test method. 96 | /// The parameter list which was returned by . 97 | /// The display name of this test case. 98 | [NotNull] 99 | public string GetDisplayName([NotNull] MethodInfo methodInfo, [NotNull] object[] data) 100 | { 101 | return ContractTest.Method[methodInfo][_testCaseIndex++].DisplayName; 102 | } 103 | 104 | #endregion 105 | 106 | #region Collect and verify test cases 107 | 108 | [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 109 | [SuppressMessage("ReSharper", "PossibleNullReferenceException")] 110 | private static void Collect([NotNull] MethodInfo methodInfo) 111 | { 112 | var type = methodInfo.DeclaringType; 113 | Contract.Requires(type != null, 114 | "The method must be declared in a type. If this exception happened, there might be a bug in MSTest v2."); 115 | 116 | var testInstance = Activator.CreateInstance(type); 117 | var testCaseList = ContractTest.Method[methodInfo]; 118 | try 119 | { 120 | // Invoke target test method to collect all test cases. 121 | methodInfo.Invoke(testInstance, null); 122 | } 123 | catch (TargetInvocationException ex) 124 | { 125 | // An exception has occurred during the test cases collecting. 126 | // We should create a new test result to report this exception instead of reporting the collected ones. 127 | var exception = ex.InnerException; 128 | Contract.Assume(exception != null); 129 | 130 | testCaseList.Clear(); 131 | testCaseList.Add(new ReadonlyTestCase(exception, exception.GetType().FullName)); 132 | } 133 | catch (Exception ex) 134 | { 135 | // An unexpected exception has occurred, but we don't know why it happens. 136 | // We should Add a new test result to report this exception. 137 | testCaseList.Add(new ReadonlyTestCase(ex, ex.GetType().FullName)); 138 | } 139 | 140 | if (!testCaseList.Any()) 141 | { 142 | testCaseList.Add(new ReadonlyTestCase(UnitTestOutcome.Inconclusive, 143 | @"No test found", 144 | @"A unit test method should contain at least one test case. 145 | Try to call Test extension method to collect one. 146 | 147 | ```csharp 148 | ""Test contract description"".Test(() => 149 | { 150 | // Arrange 151 | // Action 152 | // Assert 153 | }); 154 | ``` 155 | 156 | If you only need to write a normal test method, use `TestMethodAttribute` instead of `ContractTestCaseAttribute`.")); 157 | } 158 | } 159 | 160 | /// 161 | /// Find out the test cases which have the same contract string, add a special exception to it. 162 | /// 163 | /// The test cases of a single test method. 164 | private void VerifyContracts([NotNull] IList cases) 165 | { 166 | var caseContractSet = new HashSet(); 167 | var duplicatedCases = new HashSet(); 168 | var duplicatedContracts = new HashSet(); 169 | 170 | foreach (var @case in cases) 171 | { 172 | if (caseContractSet.Contains(@case.DisplayName)) 173 | { 174 | duplicatedCases.Add(@case); 175 | duplicatedContracts.Add(@case.DisplayName); 176 | } 177 | else 178 | { 179 | caseContractSet.Add(@case.DisplayName); 180 | } 181 | } 182 | 183 | foreach (var @case in duplicatedCases) 184 | { 185 | cases.Remove(@case); 186 | } 187 | 188 | foreach (var contract in duplicatedContracts) 189 | { 190 | cases.Add(new ReadonlyTestCase(UnitTestOutcome.Error, contract, 191 | $@"Duplicated Contract String 192 | 193 | Two or more test cases have the same contract string which is ""{contract}"". 194 | 1. Please check whether you have created two test cases which have the same contract string. Notice that different formatted string may become the same string when the placeholders are filled with arguments. 195 | 2. If you are using formatted strings but all the value strings are the same(by calling argument.ToString()), please avoid using the formatted contract string. ")); 196 | } 197 | 198 | // throw new NotImplementedException(); 199 | } 200 | 201 | #endregion 202 | 203 | /// 204 | /// Gets or increment the current test case index. 205 | /// and should increment it separately 206 | /// because they are not in the same instance. 207 | /// 208 | private int _testCaseIndex; 209 | 210 | /// 211 | /// the proxy of ITestMethod(TestMethodInfo in fact) 212 | /// overwrite the invoke method 213 | /// only one instance in one ContractTestCaseAttribute 214 | /// 215 | private TestMethodProxy _testMethodProxy; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Contracts/ContractTestConfiguration.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/src/MSTest.Extensions/Contracts/ContractTestConfiguration.cs -------------------------------------------------------------------------------- /src/MSTest.Extensions/Contracts/ContractTestContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using MSTest.Extensions.Core; 6 | using dotnetCampus.Runtime.CompilerServices; 7 | #if GENERATED_CODE 8 | using CanBeNullWhenTIsSingle = MSTest.Extensions.NotNullAttribute; 9 | #else 10 | using CanBeNullWhenTIsSingle = MSTest.Extensions.CanBeNullAttribute; 11 | #endif 12 | 13 | namespace MSTest.Extensions.Contracts 14 | { 15 | /// 16 | /// Provides builder for a particular contracted test case. 17 | /// 18 | [GenerateGenericFromThis(From = 2, To = 4)] 19 | public class ContractTestContext 20 | { 21 | /// 22 | /// Create a new instance with contract description and testing action. 23 | /// 24 | /// The contract description string for this test case. 25 | /// The actual action to execute the test case. 26 | public ContractTestContext([NotNull] string contract, [NotNull] Action testCase) 27 | { 28 | _contract = contract ?? throw new ArgumentNullException(nameof(contract)); 29 | if (testCase == null) throw new ArgumentNullException(nameof(testCase)); 30 | #if NET45 31 | #pragma warning disable CS1998 // Async function without await expression 32 | _testCase = async t => testCase(t); 33 | #pragma warning restore CS1998 // Async function without await expression 34 | #else 35 | _testCase = t => 36 | { 37 | testCase(t); 38 | return Task.CompletedTask; 39 | }; 40 | #endif 41 | } 42 | 43 | /// 44 | /// Create a new instance with contract description and async testing action. 45 | /// 46 | /// The contract description string for this test case. 47 | /// The actual async action to execute the test case. 48 | public ContractTestContext([NotNull] string contract, [NotNull] Func testCase) 49 | { 50 | _contract = contract ?? throw new ArgumentNullException(nameof(contract)); 51 | _testCase = testCase ?? throw new ArgumentNullException(nameof(testCase)); 52 | } 53 | 54 | /// 55 | /// When the test case is executed, pass the argument(s) to the test case action. 56 | /// 57 | /// The argument(s) that will be passed into the test case action. 58 | /// The instance itself. 59 | /// 60 | /// Note that we only verify the argument in runtime. In this case, we have the power to pass an array instead of writing them all in a method parameter list. 61 | /// 62 | [NotNull, PublicAPI] 63 | public ContractTestContext WithArguments([CanBeNullWhenTIsSingle] params T[] ts) 64 | { 65 | if (ts == null) 66 | { 67 | #if GENERATED_CODE 68 | throw new ArgumentNullException(nameof(ts)); 69 | #else 70 | ts = new T[] { default }; 71 | #endif 72 | } 73 | if (ts.Length < 1) 74 | { 75 | throw new ArgumentException( 76 | $"At least one argument should be passed into test case {_contract}", nameof(ts)); 77 | } 78 | Contract.EndContractBlock(); 79 | 80 | #if !GENERATED_CODE 81 | #endif 82 | // Check if every argument will be formatted into the contract. 83 | var allFormatted = true; 84 | for (var i = 0; i < ts.Length; i++) 85 | { 86 | allFormatted = _contract.Contains($"{{{i}}}"); 87 | if (!allFormatted) break; 88 | } 89 | 90 | foreach (var t in ts) 91 | { 92 | // If any argument is not formatted, post the argument value at the end of the contract string. 93 | var contract = string.Format(_contract, ForT(t)); 94 | if (!allFormatted) 95 | { 96 | contract = contract + $"({ForT(t)})"; 97 | } 98 | 99 | // Add an argument test case to the test case list. 100 | ContractTest.Method.Current.Add(new ContractTestCase(contract, () => _testCase(t))); 101 | } 102 | 103 | return this; 104 | } 105 | 106 | /// 107 | /// For null value, the formatted string is "Null". 108 | /// 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | private string ForT([CanBeNull] TInput value) 111 | { 112 | return value == null ? "Null" : value.ToString(); 113 | } 114 | 115 | #if GENERATED_CODE 116 | /// 117 | /// When the test case is executed, pass the arguments to the test case action. 118 | /// 119 | /// The instance itself. 120 | [NotNull, PublicAPI] 121 | public ContractTestContext WithArguments(T t) 122 | { 123 | ContractTest.Method.Current.Add(new ContractTestCase( 124 | string.Format(_contract, t), () => _testCase(t))); 125 | return this; 126 | } 127 | #endif 128 | 129 | /// 130 | /// Gets the contract description string for this test case. 131 | /// 132 | [NotNull] private readonly string _contract; 133 | 134 | /// 135 | /// Invoke this action to execute this test case with the argument(s). 136 | /// The returning value of this func will never be null, but it does not mean that it is an async func. 137 | /// 138 | [NotNull] private readonly Func _testCase; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/ContractTestCase.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/src/MSTest.Extensions/Core/ContractTestCase.cs -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/ITestCase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace MSTest.Extensions.Core 4 | { 5 | /// 6 | /// A test case that is discovered from a unit test method. 7 | /// A unit test method may have multiple test cases. 8 | /// 9 | internal interface ITestCase 10 | { 11 | /// 12 | /// Get the display name of this test case. 13 | /// 14 | [NotNull] 15 | string DisplayName { get; } 16 | 17 | /// 18 | /// Get the test result of this test case. This may cause an invoking the action of a test case. 19 | /// 20 | [NotNull] 21 | TestResult Result { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/ReadonlyTestCase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace MSTest.Extensions.Core 6 | { 7 | /// 8 | /// 9 | /// Stores a specific unit test result. 10 | /// 11 | internal class ReadonlyTestCase : ITestCase 12 | { 13 | /// 14 | /// Initialize a new instance of to report a specific exception. 15 | /// 16 | /// The exception that should be reported as a unit test result. 17 | /// The display name that would be displayed to report the exception. 18 | public ReadonlyTestCase([NotNull] Exception exception, [NotNull] string displayName) 19 | { 20 | if (exception == null) throw new ArgumentNullException(nameof(exception)); 21 | DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); 22 | 23 | Contract.EndContractBlock(); 24 | 25 | Result = new TestResult 26 | { 27 | Outcome = UnitTestOutcome.Error, 28 | TestFailureException = exception, 29 | DisplayName = displayName, 30 | }; 31 | } 32 | 33 | /// 34 | /// Initialize a new instance of to report a non-success outcome. 35 | /// 36 | /// 37 | /// The name that will be displayed when the test case. 38 | /// The reason why this test case is not runnable. 39 | internal ReadonlyTestCase(UnitTestOutcome outcome, 40 | [NotNull] string notSuccessTitle, [NotNull] string notSuccessReason) 41 | { 42 | if (outcome == UnitTestOutcome.Passed) 43 | throw new ArgumentException("This constructor only support non-success outcome.", nameof(outcome)); 44 | if (notSuccessReason == null) throw new ArgumentNullException(nameof(notSuccessReason)); 45 | DisplayName = notSuccessTitle ?? throw new ArgumentNullException(nameof(notSuccessTitle)); 46 | 47 | Contract.EndContractBlock(); 48 | 49 | Result = new TestResult 50 | { 51 | Outcome = outcome, 52 | DisplayName = notSuccessTitle, 53 | TestContextMessages = notSuccessReason, 54 | }; 55 | } 56 | 57 | /// 58 | public string DisplayName { get; } 59 | 60 | /// 61 | /// 62 | /// Get the specific unit test result. 63 | /// 64 | public TestResult Result { get; } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/TestCaseIndexer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Diagnostics.Contracts; 6 | using System.Reflection; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using MSTest.Extensions.Contracts; 9 | 10 | namespace MSTest.Extensions.Core 11 | { 12 | /// 13 | /// Contains all test case information of all test unit method which are discovered by . 14 | /// 15 | internal class TestCaseIndexer 16 | { 17 | /// 18 | /// Gets all test cases of a specified method. 19 | /// 20 | /// The target unit test method. 21 | /// 22 | /// The test case list of the specified unit test method. If the discovery is not started, then it returns an empty list. 23 | /// 24 | [NotNull] 25 | internal IList this[[NotNull] MethodInfo method] 26 | { 27 | get 28 | { 29 | if (method == null) throw new ArgumentNullException(nameof(method)); 30 | Contract.EndContractBlock(); 31 | 32 | return this[GetKey(method)]; 33 | } 34 | } 35 | 36 | /// 37 | /// Gets all test cases of current unit test method. This method is found through the stack trace. 38 | /// 39 | [NotNull] 40 | internal IList Current => this[GetCurrentTestMethod()]; 41 | 42 | /// 43 | /// Gets all test cases of a specified method key. 44 | /// 45 | /// A unique string that indicates a unit test method. 46 | /// 47 | /// The test case list of the specified unit test method. If the discovery is not started, then it returns an empty list. 48 | /// 49 | [NotNull] 50 | private IList this[[NotNull] string testKey] 51 | { 52 | get 53 | { 54 | Contract.EndContractBlock(); 55 | 56 | if (!_testCaseDictionary.TryGetValue(testKey, out var list)) 57 | { 58 | list = new List(); 59 | _testCaseDictionary[testKey] = list; 60 | } 61 | 62 | return list; 63 | } 64 | } 65 | 66 | /// 67 | /// Stores all the test cases that discovered or will be discovered from all unit test methods. 68 | /// Key: namespace.class.method. can generate it correctly. 69 | /// Value: All test cases of a specified method. You can get rid of null value by calling Indexer. 70 | /// 71 | [NotNull] private readonly Dictionary> _testCaseDictionary = 72 | new Dictionary>(); 73 | 74 | /// 75 | /// Get the unique string that indicates a unit test method. 76 | /// 77 | /// The unit test method. 78 | /// The unique string that indicates a unit test method. 79 | [NotNull, SuppressMessage("ReSharper", "PossibleNullReferenceException")] 80 | private static string GetKey([NotNull] MemberInfo member) 81 | { 82 | var type = member.DeclaringType; 83 | Contract.Requires(type != null); 84 | Contract.EndContractBlock(); 85 | 86 | return $"{type.FullName}.{member.Name}"; 87 | } 88 | 89 | /// 90 | /// Find the unit test method through current stack trace. 91 | /// 92 | /// The unit test method that found from stack trace. 93 | [NotNull] 94 | private static MethodInfo GetCurrentTestMethod() 95 | { 96 | var stackTrace = new StackTrace(); 97 | for (var i = 0; i < stackTrace.FrameCount; i++) 98 | { 99 | var method = stackTrace.GetFrame(i).GetMethod(); 100 | if (method.GetCustomAttribute() != null) 101 | { 102 | return (MethodInfo) method; 103 | } 104 | } 105 | 106 | throw new InvalidOperationException( 107 | "There is no unit test method in the current stack trace. " + 108 | "This method should only be called directly or indirectly from the unit test method."); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/TestMethodProxy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MSTest.Extensions.Contracts; 7 | using MSTest.Extensions.Utils; 8 | 9 | namespace MSTest.Extensions.Core 10 | { 11 | /// 12 | /// the TestMethod proxy, replace the implement of ITestMethod invoke 13 | /// 14 | public class TestMethodProxy : ITestMethod 15 | { 16 | /// 17 | /// create a TestMethodProxy of an ITestMethod 18 | /// 19 | /// 20 | public TestMethodProxy([NotNull] ITestMethod testMethod) 21 | { 22 | if (testMethod is TestMethodProxy) 23 | { 24 | throw new InvalidOperationException("Can not create a TestMethodProxy of another TestMethodProxy"); 25 | } 26 | 27 | _realSubject = testMethod; 28 | _testCaseIndex = 0; 29 | ReflectionMemberInit(); 30 | } 31 | 32 | /// 33 | /// use the TestMethodInitialize and TestMethodCleanup of MsTest 34 | /// replace the method invoke with ItestCase.Result 35 | /// 36 | /// 37 | /// 38 | [NotNull] 39 | public TestResult Invoke(object[] arguments) 40 | { 41 | TestMethodInitialize(); 42 | var testCases = ContractTest.Method[_realSubject.MethodInfo]; 43 | var testCase = testCases[_testCaseIndex++]; 44 | var result = InvokeCore(testCase); 45 | TestMethodCleanup(); 46 | return result; 47 | } 48 | 49 | [NotNull] 50 | [ItemNotNull] 51 | internal async Task InvokeAsync() 52 | { 53 | TestMethodInitialize(); 54 | var testCases = ContractTest.Method[_realSubject.MethodInfo]; 55 | var testCase = testCases[_testCaseIndex++]; 56 | var result = await InvokeCoreAsync(testCase).ConfigureAwait(false); 57 | TestMethodCleanup(); 58 | return result; 59 | } 60 | 61 | [NotNull] 62 | private protected virtual TestResult InvokeCore([NotNull] ITestCase testCase) 63 | { 64 | var result = testCase.Result; 65 | return result; 66 | } 67 | 68 | [NotNull] 69 | [ItemNotNull] 70 | private protected virtual async Task InvokeCoreAsync([NotNull] ITestCase testCase) 71 | { 72 | TestResult result; 73 | if (testCase is ContractTestCase contractTestCase) 74 | { 75 | result = await contractTestCase.ExecuteAsync().ConfigureAwait(false); 76 | } 77 | else 78 | { 79 | result = testCase.Result; 80 | } 81 | 82 | return result; 83 | } 84 | 85 | /// 86 | /// return real instance implement of GetAllAttributes 87 | /// 88 | /// 89 | /// 90 | public Attribute[] GetAllAttributes(bool inherit) 91 | { 92 | return _realSubject.GetAllAttributes(inherit); 93 | } 94 | 95 | /// 96 | /// return real instance implement of GetAttributes 97 | /// 98 | /// 99 | /// 100 | /// 101 | public TAttributeType[] GetAttributes(bool inherit) where TAttributeType : Attribute 102 | { 103 | return _realSubject.GetAttributes(inherit); 104 | } 105 | 106 | /// 107 | /// return real instance implement of TestMethodName 108 | /// 109 | public string TestMethodName => _realSubject.TestMethodName; 110 | 111 | /// 112 | /// return real instance implement of TestClassName 113 | /// 114 | public string TestClassName => _realSubject.TestClassName; 115 | 116 | /// 117 | /// return real instance implement of ReturnType 118 | /// 119 | public Type ReturnType => _realSubject.ReturnType; 120 | 121 | /// 122 | /// return real instance implement of Arguments 123 | /// 124 | public object[] Arguments => _realSubject.Arguments; 125 | 126 | /// 127 | /// return real instance implement of ParameterTypes 128 | /// 129 | public ParameterInfo[] ParameterTypes => _realSubject.ParameterTypes; 130 | 131 | /// 132 | /// return real instance implement of MethodInfo 133 | /// 134 | public MethodInfo MethodInfo => _realSubject.MethodInfo; 135 | 136 | 137 | /// 138 | /// extract the test TestMethodInitialize of _realSubject and run 139 | /// 140 | private void TestMethodInitialize() 141 | { 142 | ClassInfo.SetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestCleanupMethodsQueue, 143 | new Queue()); 144 | ClassInfo.SetField(MSTestMemberName.TestClassInfoFieldTestCleanupMethod, null); 145 | var testCases = ContractTest.Method[_realSubject.MethodInfo]; 146 | testCases.Clear(); 147 | _realSubject.Invoke(null); 148 | } 149 | 150 | /// 151 | /// extract the test TestMethodCleanup of _realSubject and run 152 | /// 153 | private void TestMethodCleanup() 154 | { 155 | ClassInfo.SetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestCleanupMethodsQueue, 156 | BaseTestCleanupMethodsQueue); 157 | ClassInfo.SetField(MSTestMemberName.TestClassInfoFieldTestCleanupMethod, TestCleanupMethod); 158 | ClassInfo.SetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestInitializeMethodsQueue, 159 | new Queue()); 160 | ClassInfo.SetField(MSTestMemberName.TestClassInfoFieldTestInitializeMethod, null); 161 | _realSubject.Invoke(null); 162 | ClassInfo.SetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestInitializeMethodsQueue, 163 | BaseTestInitializeMethodsQueue); 164 | ClassInfo.SetField(MSTestMemberName.TestClassInfoFieldTestInitializeMethod, TestInitializeMethod); 165 | } 166 | 167 | /// 168 | /// get the nonpublic memeber value of _realsubject var Reflection 169 | /// 170 | private void ReflectionMemberInit() 171 | { 172 | ClassInfo = _realSubject.GetProperty(MSTestMemberName.TestMethodInfoPropertyParent); 173 | TestCleanupMethod = (MethodInfo)ClassInfo 174 | .GetField(MSTestMemberName.TestClassInfoFieldTestCleanupMethod); 175 | TestInitializeMethod = (MethodInfo)ClassInfo 176 | .GetField(MSTestMemberName.TestClassInfoFieldTestInitializeMethod); 177 | BaseTestInitializeMethodsQueue = (Queue)ClassInfo 178 | .GetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestInitializeMethodsQueue); 179 | BaseTestCleanupMethodsQueue = (Queue)ClassInfo 180 | .GetProperty(MSTestMemberName.TestClassInfoPropertyBaseTestCleanupMethodsQueue); 181 | } 182 | 183 | private object ClassInfo { get; set; } 184 | 185 | private Queue BaseTestInitializeMethodsQueue { get; set; } 186 | 187 | private Queue BaseTestCleanupMethodsQueue { get; set; } 188 | 189 | private MethodInfo TestInitializeMethod { get; set; } 190 | 191 | private MethodInfo TestCleanupMethod { get; set; } 192 | private readonly ITestMethod _realSubject; 193 | private int _testCaseIndex; 194 | } 195 | } -------------------------------------------------------------------------------- /src/MSTest.Extensions/Core/ThreadSafeStringWriter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | namespace MSTest.Extensions.Core 8 | { 9 | /// 10 | /// StringWriter which has thread safe ToString(). 11 | /// 12 | internal class ThreadSafeStringWriter : StringWriter 13 | { 14 | private readonly object _lockObject = new object(); 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// 20 | /// The format provider. 21 | /// 22 | public ThreadSafeStringWriter(IFormatProvider formatProvider) 23 | : base(formatProvider) 24 | { 25 | } 26 | 27 | /// 28 | public override string ToString() 29 | { 30 | lock (_lockObject) 31 | { 32 | try 33 | { 34 | return base.ToString(); 35 | } 36 | catch (ObjectDisposedException) 37 | { 38 | return string.Empty; 39 | } 40 | } 41 | } 42 | 43 | /// 44 | public override void Write(char value) 45 | { 46 | lock (_lockObject) 47 | { 48 | InvokeBaseClass(() => base.Write(value)); 49 | } 50 | } 51 | 52 | /// 53 | public override void Write(string value) 54 | { 55 | lock (_lockObject) 56 | { 57 | InvokeBaseClass(() => base.Write(value)); 58 | } 59 | } 60 | 61 | /// 62 | public override void Write(char[] buffer, int index, int count) 63 | { 64 | lock (_lockObject) 65 | { 66 | InvokeBaseClass(() => base.Write(buffer, index, count)); 67 | } 68 | } 69 | 70 | /// 71 | protected override void Dispose(bool disposing) 72 | { 73 | lock (_lockObject) 74 | { 75 | InvokeBaseClass(() => base.Dispose(disposing)); 76 | } 77 | } 78 | 79 | private static void InvokeBaseClass(Action action) 80 | { 81 | try 82 | { 83 | action(); 84 | } 85 | catch (ObjectDisposedException) 86 | { 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/CustomTestManagers/CustomTestManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using MSTest.Extensions.Contracts; 9 | 10 | namespace MSTest.Extensions.CustomTestManagers 11 | { 12 | /// 13 | /// The custom test manager which can run without MSTest 14 | /// 15 | public class CustomTestManager 16 | { 17 | /// 18 | /// Create the custom test manager 19 | /// 20 | public CustomTestManager() 21 | { 22 | Context = new CustomTestManagerRunContext(); 23 | } 24 | 25 | /// 26 | /// Create the custom test manager 27 | /// 28 | /// 29 | public CustomTestManager(CustomTestManagerRunContext context) 30 | { 31 | Context = context; 32 | } 33 | 34 | /// 35 | /// Run all the test from MethodInfo 36 | /// 37 | /// 38 | /// 39 | [NotNull] 40 | [ItemNotNull] 41 | public async Task RunAsync([NotNull] MethodInfo methodInfo) 42 | { 43 | if (methodInfo == null) throw new ArgumentNullException(nameof(methodInfo)); 44 | var exceptionList = new List(); 45 | int count = 0; 46 | TimeSpan duration = TimeSpan.Zero; 47 | 48 | var contractTestCaseAttribute = methodInfo.GetCustomAttribute(); 49 | if (contractTestCaseAttribute != null) 50 | { 51 | // 获取执行次数 52 | foreach (var data in contractTestCaseAttribute.GetData(methodInfo)) 53 | { 54 | count++; 55 | var displayName = contractTestCaseAttribute.GetDisplayName(methodInfo, data); 56 | 57 | try 58 | { 59 | var result = await contractTestCaseAttribute.ExecuteAsync(new FakeTestMethod(methodInfo)) 60 | // 在 UI 中进行测试,期望每次都是返回到相同的线程 61 | .ConfigureAwait(true); 62 | duration += result.Duration; 63 | } 64 | #pragma warning disable CA1031 // 不捕获常规异常类型 65 | catch (Exception e) 66 | #pragma warning restore CA1031 // 不捕获常规异常类型 67 | { 68 | exceptionList.Add(new TestExceptionResult(displayName, e)); 69 | } 70 | } 71 | } 72 | 73 | return new TestManagerRunResult(count, duration, exceptionList); 74 | } 75 | 76 | /// 77 | /// Run all the test from TestClass 78 | /// 79 | /// 80 | [NotNull] 81 | [ItemNotNull] 82 | public async Task RunAsync([NotNull] Type testClass) 83 | { 84 | if (testClass == null) throw new ArgumentNullException(nameof(testClass)); 85 | var exceptionList = new List(); 86 | int count = 0; 87 | 88 | TimeSpan duration = TimeSpan.Zero; 89 | 90 | foreach (var methodInfo in testClass.GetMethods()) 91 | { 92 | var testManagerRunResult = await RunAsync(methodInfo) 93 | // 在 UI 中进行测试,期望每次都是返回到相同的线程 94 | .ConfigureAwait(true); 95 | 96 | count += testManagerRunResult.AllTestCount; 97 | duration += testManagerRunResult.Duration; 98 | exceptionList.AddRange(testManagerRunResult.TestExceptionResultList); 99 | } 100 | 101 | return new TestManagerRunResult(count, duration, exceptionList); 102 | } 103 | 104 | /// 105 | /// Run all the test in assembly 106 | /// 107 | /// 108 | /// 109 | [NotNull] 110 | [ItemNotNull] 111 | public async Task RunAsync([NotNull]Assembly assembly) 112 | { 113 | if (assembly is null) 114 | { 115 | throw new ArgumentNullException(nameof(assembly)); 116 | } 117 | 118 | var exceptionList = new List(); 119 | int count = 0; 120 | 121 | TimeSpan duration = TimeSpan.Zero; 122 | 123 | foreach (var type in assembly.GetTypes()) 124 | { 125 | if (type.GetCustomAttribute() != null) 126 | { 127 | var testManagerRunResult = await RunAsync(type) 128 | // 在 UI 中进行测试,期望每次都是返回到相同的线程 129 | .ConfigureAwait(true); 130 | 131 | count += testManagerRunResult.AllTestCount; 132 | duration += testManagerRunResult.Duration; 133 | exceptionList.AddRange(testManagerRunResult.TestExceptionResultList); 134 | } 135 | } 136 | 137 | return new TestManagerRunResult(count, duration, exceptionList); 138 | } 139 | 140 | private CustomTestManagerRunContext Context { get; } 141 | 142 | class FakeTestMethod : ITestMethod 143 | { 144 | public FakeTestMethod([NotNull] MethodInfo methodInfo, [CanBeNull] object obj = null) 145 | { 146 | MethodInfo = methodInfo; 147 | Obj = obj ?? Activator.CreateInstance(methodInfo.DeclaringType!); 148 | } 149 | 150 | [NotNull] 151 | public TestResult Invoke(object[] arguments) 152 | { 153 | MethodInfo.Invoke(Obj, arguments); 154 | return new TestResult(); 155 | } 156 | 157 | [NotNull] 158 | public Attribute[] GetAllAttributes(bool inherit) 159 | { 160 | return MethodInfo.GetCustomAttributes().ToArray(); 161 | } 162 | 163 | [NotNull] 164 | public TAttributeType[] GetAttributes(bool inherit) where TAttributeType : Attribute 165 | { 166 | return MethodInfo.GetCustomAttributes().OfType().ToArray(); 167 | } 168 | 169 | private object Obj { get; } 170 | [NotNull] 171 | public string TestMethodName => MethodInfo.Name; 172 | [NotNull] 173 | public string TestClassName => MethodInfo.DeclaringType?.Name ?? string.Empty; 174 | [CanBeNull] 175 | public Type ReturnType => MethodInfo.ReturnType; 176 | public object[] Arguments { set; get; } 177 | [CanBeNull] 178 | public ParameterInfo[] ParameterTypes => MethodInfo.GetParameters(); 179 | public MethodInfo MethodInfo { set; get; } 180 | 181 | public FakeTestInfo Parent { get; } = new FakeTestInfo(); 182 | } 183 | 184 | class FakeTestInfo 185 | { 186 | // 命名不能更改,框架内使用反射获取 187 | 188 | private MethodInfo testCleanupMethod; 189 | private MethodInfo testInitializeMethod; 190 | 191 | public Queue BaseTestInitializeMethodsQueue { set; get; } 192 | 193 | public Queue BaseTestCleanupMethodsQueue { set; get; } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/CustomTestManagers/CustomTestManagerRunContext.cs: -------------------------------------------------------------------------------- 1 | namespace MSTest.Extensions.CustomTestManagers 2 | { 3 | /// 4 | /// The context for custom test manager 5 | /// 6 | public class CustomTestManagerRunContext 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/MSTest.Extensions/CustomTestManagers/TestExceptionResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSTest.Extensions.CustomTestManagers 4 | { 5 | /// 6 | /// The test exception result 7 | /// 8 | public class TestExceptionResult 9 | { 10 | internal TestExceptionResult(string displayName, Exception exception) 11 | { 12 | DisplayName = displayName; 13 | Exception = exception; 14 | } 15 | 16 | /// 17 | /// The test display name 18 | /// 19 | public string DisplayName { get; } 20 | 21 | /// 22 | /// The test exception 23 | /// 24 | public Exception Exception { get; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/MSTest.Extensions/CustomTestManagers/TestManagerRunResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace MSTest.Extensions.CustomTestManagers 6 | { 7 | /// 8 | /// The TestManager run result 9 | /// 10 | public class TestManagerRunResult 11 | { 12 | /// 13 | /// Create the TestManager run result 14 | /// 15 | /// 16 | /// 17 | /// 18 | public TestManagerRunResult(int allTestCount, TimeSpan duration, List testExceptionResultList) 19 | { 20 | AllTestCount = allTestCount; 21 | Duration = duration; 22 | TestExceptionResultList = testExceptionResultList; 23 | } 24 | 25 | /// 26 | /// Is all test success 27 | /// 28 | public bool Success => FailTestCount == 0; 29 | 30 | /// 31 | /// The test run duration 32 | /// 33 | public TimeSpan Duration { get; } 34 | 35 | /// 36 | /// The number of all test 37 | /// 38 | public int AllTestCount { get; } 39 | 40 | /// 41 | /// The number of the fail test 42 | /// 43 | public int FailTestCount => TestExceptionResultList.Count; 44 | 45 | /// 46 | /// The number of the success test 47 | /// 48 | public int SuccessTestCount => AllTestCount - FailTestCount; 49 | 50 | /// 51 | /// The test fail exception list 52 | /// 53 | public List TestExceptionResultList { get; } 54 | 55 | /// 56 | [NotNull] 57 | public override string ToString() 58 | { 59 | if (Success) 60 | { 61 | return 62 | $"Passed! - Failed:{FailTestCount,8},Passed:{SuccessTestCount,8},Skipped: 0,Total:{AllTestCount,8},Duration: {Duration.TotalSeconds:0.00} s"; 63 | } 64 | else 65 | { 66 | var stringBuilder = new StringBuilder(); 67 | foreach (var exception in TestExceptionResultList) 68 | { 69 | stringBuilder.AppendLine($"Failed {exception.DisplayName}"); 70 | stringBuilder.AppendLine($"Message:"); 71 | stringBuilder.AppendLine(exception.Exception.ToString()); 72 | stringBuilder.AppendLine(); 73 | } 74 | 75 | stringBuilder.AppendLine($"Failed! - Failed:{FailTestCount,8},Passed:{SuccessTestCount,8},Skipped: 0,Total:{AllTestCount,8},Duration: {Duration.TotalSeconds:0.00} s"); 76 | return stringBuilder.ToString(); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/MSTest.Extensions/MSTest.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net45;netcoreapp3.0;net5.0 5 | latest 6 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 7 | MSTestEnhancer 8 | dotnet-campus 9 | https://github.com/dotnet-campus/CUnit.git 10 | git 11 | true 12 | https://github.com/dotnet-campus/CUnit 13 | Copyright (c) 2018-2023 dotnet职业技术学院 14 | false 15 | MSTestEnhancer helps you to write unit tests without naming any method. You can write method contract descriptions instead of writing confusing test method name when writing unit tests. 16 | Add some assersion extensions. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("MSTest.Extensions.Tests")] 4 | [assembly: InternalsVisibleTo("dotnetCampus.UITest.WPF")] 5 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Utils/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace MSTest.Extensions.Utils 2 | { 3 | /// 4 | /// The member name in tesetfx, used for reflection 5 | /// 6 | public static class MSTestMemberName 7 | { 8 | public const string TestMethodInfoPropertyParent = "Parent"; 9 | public const string TestClassInfoPropertyBaseTestInitializeMethodsQueue = "BaseTestInitializeMethodsQueue"; 10 | public const string TestClassInfoPropertyBaseTestCleanupMethodsQueue = "BaseTestCleanupMethodsQueue"; 11 | public const string TestClassInfoFieldTestInitializeMethod = "testInitializeMethod"; 12 | public const string TestClassInfoFieldTestCleanupMethod = "testCleanupMethod"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MSTest.Extensions/Utils/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace MSTest.Extensions.Utils 4 | { 5 | /// 6 | /// object extension.used for getting/setting both public and nonpublic property or field of an instance 7 | /// 8 | public static class ReflectionExtensions 9 | { 10 | /// 11 | /// used for getting both public and nonpublic field of an instance 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static object GetField([NotNull] this object source, string fieldName) 17 | { 18 | var type = source.GetType(); 19 | var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 20 | return field.GetValue(source); 21 | } 22 | 23 | /// 24 | /// used for getting both public and nonpublic property of an instance 25 | /// 26 | /// 27 | /// 28 | /// 29 | public static object GetProperty([NotNull] this object source, string propertyName) 30 | { 31 | var type = source.GetType(); 32 | var property = type.GetProperty(propertyName, 33 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 34 | return property.GetValue(source); 35 | } 36 | 37 | /// 38 | /// used for setting both public and nonpublic field of an instance 39 | /// 40 | /// 41 | /// 42 | /// 43 | public static void SetField([NotNull] this object target, string fieldName, object value) 44 | { 45 | var type = target.GetType(); 46 | var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 47 | field.SetValue(target, value); 48 | } 49 | 50 | /// 51 | /// used for getting both public and nonpublic property of an instance 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static void SetProperty([NotNull] this object target, string propertyName, object value) 57 | { 58 | var type = target.GetType(); 59 | var property = type.GetProperty(propertyName, 60 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 61 | property.SetValue(target, value); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/dotnetCampus.UITest.WPF/UIContractTestCaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using MSTest.Extensions; 8 | using MSTest.Extensions.Contracts; 9 | using MSTest.Extensions.Core; 10 | 11 | // ReSharper disable InconsistentNaming 12 | 13 | namespace dotnetCampus.UITest.WPF 14 | { 15 | /// 16 | /// A contract based test case that is discovered from a unit test method. 17 | /// 18 | [PublicAPI] 19 | public class UIContractTestCaseAttribute : ContractTestCaseAttribute 20 | { 21 | private protected override TestMethodProxy CreateTestMethodProxy(ITestMethod testMethod) 22 | { 23 | if (!UITestManager.IsInitialized) 24 | { 25 | throw new InvalidOperationException("Must call `UITestManager.InitializeApplication` in the Method with AssemblyInitializeAttribute."); 26 | } 27 | 28 | return new UITestMethodProxy(testMethod); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/dotnetCampus.UITest.WPF/UITestManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading; 4 | using System.Windows; 5 | using System.Windows.Threading; 6 | 7 | // ReSharper disable InconsistentNaming 8 | 9 | namespace dotnetCampus.UITest.WPF 10 | { 11 | public static class UITestManager 12 | { 13 | public static void InitializeApplication(Func applicationCreator) 14 | { 15 | if (_initializing) 16 | { 17 | throw new InvalidOperationException($"Do not call InitializeApplication twice."); 18 | } 19 | 20 | _initializing = true; 21 | 22 | var manualResetEvent = new ManualResetEvent(false); 23 | var thread = new Thread(() => 24 | { 25 | Thread.CurrentThread.Name = "UIThread"; 26 | 27 | var application = applicationCreator(); 28 | var type = application.GetType(); 29 | var resourceAssembly = type.Assembly; 30 | 31 | typeof(Application).GetField("_resourceAssembly", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(null, resourceAssembly); 32 | 33 | var methodInfo = type.GetMethod("InitializeComponent", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 34 | if (methodInfo != null) 35 | { 36 | methodInfo.Invoke(application, new object[0]); 37 | } 38 | 39 | application.Startup += (sender, args) => 40 | { 41 | manualResetEvent.Set(); 42 | }; 43 | 44 | application.Run(); 45 | }); 46 | 47 | thread.SetApartmentState(ApartmentState.STA); 48 | thread.IsBackground = true; 49 | thread.Start(); 50 | 51 | manualResetEvent.WaitOne(); 52 | 53 | IsInitialized = true; 54 | } 55 | 56 | private static bool _initializing; 57 | internal static bool IsInitialized { private set; get; } 58 | } 59 | } -------------------------------------------------------------------------------- /src/dotnetCampus.UITest.WPF/UITestMethodProxy.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Windows; 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | using MSTest.Extensions; 7 | using MSTest.Extensions.Core; 8 | 9 | // ReSharper disable InconsistentNaming 10 | 11 | namespace dotnetCampus.UITest.WPF 12 | { 13 | class UITestMethodProxy : TestMethodProxy 14 | { 15 | public UITestMethodProxy([NotNull] ITestMethod testMethod) : base(testMethod) 16 | { 17 | } 18 | 19 | private protected override TestResult InvokeCore(ITestCase testCase) 20 | { 21 | if (testCase is ContractTestCase contractTestCase) 22 | { 23 | var task = Application.Current.Dispatcher.Invoke(async () => 24 | { 25 | var result = await contractTestCase.ExecuteAsync() 26 | // 如果在执行过程,应用退出了,那在没有加上 ConfigureAwait 设置为 false 那将需要调度回 UI 线程,才能返回 27 | // 由于应用退出了,也就是 UI 线程不会调度的任务 28 | // 因此不会返回,单元测试将会卡住 29 | .ConfigureAwait(false); 30 | return result; 31 | }); 32 | 33 | return task.Result; 34 | } 35 | 36 | return base.InvokeCore(testCase); 37 | } 38 | 39 | private protected override async Task InvokeCoreAsync(ITestCase testCase) 40 | { 41 | return await await Application.Current.Dispatcher.InvokeAsync(async () => await base.InvokeCoreAsync(testCase)); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/dotnetCampus.UITest.WPF/dotnetCampus.UITest.WPF.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net45;netcoreapp3.1;net5.0-windows;net6.0-windows 5 | enable 6 | true 7 | True 8 | The UITest framework for WPF 9 | https://github.com/dotnet-campus/CUnit 10 | Copyright (c) 2021 dotnet职业技术学院 11 | false 12 | https://github.com/dotnet-campus/CUnit 13 | dotnet;nuget;msbuild;UITest;WPF;MSTest;TestFramework 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Contracts/ContractTestCaseAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MSTest.Extensions.Contracts; 5 | 6 | namespace MSTest.Extensions.Tests.Contracts 7 | { 8 | /// 9 | /// All methods in this class are not real unit test. 10 | /// It is only used for you to check the test result in the result view. 11 | /// 12 | [TestClass] 13 | public class ContractTestCaseAttributeTests 14 | { 15 | [ContractTestCase] 16 | public void RunAPassedTestCase() 17 | { 18 | "".Test(() => { }); 19 | "123".Test(() => { }); 20 | "121233".Test(() => { }); 21 | } 22 | 23 | [ContractTestCase] 24 | public void RunATestCaseWithExtraInfo() 25 | { 26 | "Output".Test(() => Console.WriteLine("This is a test message.")); 27 | "Error".Test(() => Console.Error.WriteLine("This is an error message.")); 28 | } 29 | 30 | [ContractTestCase, Ignore] 31 | public void WithArguments_WithSameContractString() 32 | { 33 | "This is the same string.".Test(() => { }); 34 | "This is the same string.".Test(() => { }); 35 | } 36 | 37 | [ContractTestCase] 38 | [MethodImpl(MethodImplOptions.NoInlining)] 39 | public void WithArguments_WithFormatString() 40 | { 41 | "If {0}, then {1}.".Test((string condition, string result) => 42 | { 43 | // Test Case. 44 | }).WithArguments(("A", "A'"), ("B", "B'")); 45 | } 46 | 47 | [ContractTestCase] 48 | [MethodImpl(MethodImplOptions.NoInlining)] 49 | public void WithArguments_WithPartialFormatString() 50 | { 51 | "If {0}, then something...".Test((string condition, string result) => 52 | { 53 | // Test Case. 54 | }).WithArguments(("A", "A'"), ("B", "B'")); 55 | } 56 | 57 | [ContractTestCase] 58 | [MethodImpl(MethodImplOptions.NoInlining)] 59 | public void WithArguments_WithoutFormatString() 60 | { 61 | "If something..., then something others...".Test((string condition, string result) => 62 | { 63 | // Test Case. 64 | }).WithArguments(("A", "A'"), ("B", "B'")); 65 | } 66 | 67 | [TestMethod, Ignore] 68 | public void OriginalAssertButFailed() 69 | { 70 | Assert.AreEqual(5, 6); 71 | } 72 | 73 | [ContractTestCase, Ignore] 74 | public void AssertButFailed() 75 | { 76 | "This test case will always fail.".Test(() => Assert.AreEqual(5, 6)); 77 | } 78 | 79 | [ContractTestCase, Ignore] 80 | public void RunAFailedTestCase() 81 | { 82 | "".Test(() => Assert.Fail()); 83 | } 84 | 85 | [ContractTestCase, Ignore] 86 | public void NoTestCasesIsDefinedInTheTestCaseMethod() 87 | { 88 | } 89 | 90 | [ContractTestCase, Ignore] 91 | public void ThrowExceptionDirectlyInTheTestCaseMethod() 92 | { 93 | throw new InvalidOperationException(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Contracts/ContractTestContextTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MSTest.Extensions.Contracts; 7 | 8 | #pragma warning disable CS1998 9 | 10 | namespace MSTest.Extensions.Tests.Contracts 11 | { 12 | [TestClass] 13 | public class ContractTestContextTests 14 | { 15 | [TestMethod] 16 | [DataRow(null, false, false, DisplayName = "If contract is null but action is not null, exception thrown.")] 17 | [DataRow("", true, false, DisplayName = "If contract is not null but action is null, exception thrown.")] 18 | [DataRow(null, false, true, DisplayName = 19 | "If contract is null but async action is not null, exception thrown.")] 20 | [DataRow("", true, true, DisplayName = "If contract is not null but async action is null, exception thrown.")] 21 | [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 22 | public void Constructor_NullArgument_ArgumentNullExceptionThrown(string contract, bool isActionNull, 23 | bool isAsync) 24 | { 25 | if (isAsync) 26 | { 27 | // Arrange 28 | var action = isActionNull ? (Func) null : async a => { }; 29 | 30 | // Action & Assert 31 | Assert.ThrowsException(() => new ContractTestContext(contract, action)); 32 | } 33 | else 34 | { 35 | // Arrange 36 | var action = isActionNull ? (Action) null : a => { }; 37 | 38 | // Action & Assert 39 | Assert.ThrowsException(() => new ContractTestContext(contract, action)); 40 | } 41 | } 42 | 43 | [TestMethod] 44 | public void WithArgument_NullAsSingleArgument_TestCaseCreated() 45 | { 46 | // Arrange 47 | var context = new ContractTestContext("", a => { }); 48 | 49 | // Action 50 | context.WithArguments(null); 51 | 52 | // Assert 53 | var cases = ContractTest.Method.Current; 54 | Assert.AreEqual(1, cases.Count); 55 | } 56 | 57 | [TestMethod] 58 | public void WithArgument_NullAsSingleArgument_TestCaseContractAppendNull() 59 | { 60 | // Arrange 61 | var context = new ContractTestContext("My money is ", a => { }); 62 | 63 | // Action 64 | context.WithArguments(null); 65 | 66 | // Assert 67 | var cases = ContractTest.Method.Current; 68 | var contract = cases[0].DisplayName; 69 | Assert.AreEqual("My money is (Null)", contract); 70 | } 71 | 72 | [TestMethod] 73 | public void WithArgument_NullAsSingleArgumentForFormattedContract_TestCaseContractFormattedNull() 74 | { 75 | // Arrange 76 | var context = new ContractTestContext("{0} is my money", a => { }); 77 | 78 | // Action 79 | context.WithArguments(null); 80 | 81 | // Assert 82 | var cases = ContractTest.Method.Current; 83 | var contract = cases[0].DisplayName; 84 | Assert.AreEqual("Null is my money", contract); 85 | } 86 | 87 | [TestMethod, SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 88 | public void WithArgument_NullArrayForTwoArguments_ArgumentNullExceptionThrown() 89 | { 90 | // Arrange 91 | var context = new ContractTestContext("", (a,b) => { }); 92 | 93 | // Action & Assert 94 | Assert.ThrowsException(() => context.WithArguments(null)); 95 | } 96 | 97 | [TestMethod] 98 | public void WithArgument_EmptyArray_ArgumentExceptionThrown() 99 | { 100 | // Arrange 101 | var context = new ContractTestContext("", a => { }); 102 | 103 | // Action & Assert 104 | Assert.ThrowsException(() => context.WithArguments()); 105 | } 106 | 107 | [TestMethod] 108 | public void WithArgument_OneArgument_TestCaseCreated() 109 | { 110 | // Arrange 111 | var context = new ContractTestContext("", a => { }); 112 | 113 | // Action 114 | context.WithArguments(0); 115 | 116 | // Assert 117 | var cases = ContractTest.Method.Current; 118 | Assert.AreEqual(1, cases.Count); 119 | } 120 | 121 | [TestMethod] 122 | public void WithArgument_MultipleArgument_TestCaseCreated() 123 | { 124 | // Arrange 125 | var context = new ContractTestContext("", a => { }); 126 | 127 | // Action 128 | context.WithArguments(0, 1); 129 | 130 | // Assert 131 | var cases = ContractTest.Method.Current; 132 | Assert.AreEqual(2, cases.Count); 133 | } 134 | 135 | [TestMethod] 136 | public void WithArgument_GetUnitTestResult_TestCaseExecuted() 137 | { 138 | // Arrange 139 | var executed = false; 140 | var context = new ContractTestContext("", a => executed = true); 141 | 142 | // Action 143 | context.WithArguments(0); 144 | var result = ContractTest.Method.Current.Single().Result; 145 | 146 | // Assert 147 | Assert.IsNotNull(result); 148 | Assert.IsTrue(executed); 149 | } 150 | 151 | [TestMethod] 152 | public void WithArgument_GetAsyncUnitTestResult_TestCaseExecuted() 153 | { 154 | // Arrange 155 | var executed = false; 156 | var context = new ContractTestContext("", async a => 157 | { 158 | await Task.Yield(); 159 | executed = true; 160 | }); 161 | 162 | // Action 163 | context.WithArguments(0); 164 | var result = ContractTest.Method.Current.Single().Result; 165 | 166 | // Assert 167 | Assert.IsNotNull(result); 168 | Assert.IsTrue(executed); 169 | } 170 | } 171 | } 172 | 173 | #pragma warning restore CS1998 174 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Contracts/ContractTestGenericTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MSTest.Extensions.Contracts; 6 | 7 | #pragma warning disable CS1998 8 | 9 | namespace MSTest.Extensions.Tests.Contracts 10 | { 11 | [TestClass] 12 | public class ContractTestGenericTests 13 | { 14 | [TestMethod] 15 | [DataRow(null, false, false, DisplayName = "If contract is null but action is not null, exception thrown.")] 16 | [DataRow("", true, false, DisplayName = "If contract is not null but action is null, exception thrown.")] 17 | [DataRow(null, false, true, DisplayName = "If contract is null but async action is not null, exception thrown.")] 18 | [DataRow("", true, true, DisplayName = "If contract is not null but async action is null, exception thrown.")] 19 | [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 20 | public void Test_NullArgument_ArgumentNullExceptionThrown(string contract, bool isActionNull, bool isAsync) 21 | { 22 | if (isAsync) 23 | { 24 | // Arrange 25 | var action = isActionNull ? (Func)null : async a => { }; 26 | 27 | // Action & Assert 28 | Assert.ThrowsException(() => contract.Test(action)); 29 | } 30 | else 31 | { 32 | // Arrange 33 | var action = isActionNull ? (Action)null : a => { }; 34 | 35 | // Action & Assert 36 | Assert.ThrowsException(() => contract.Test(action)); 37 | } 38 | } 39 | 40 | [TestMethod] 41 | public void Test_GenericAction_ContractTestContextCreated() 42 | { 43 | // Arrange & Action 44 | var context = "".Test((int a) => { }); 45 | 46 | // Assert 47 | Assert.IsNotNull(context); 48 | } 49 | 50 | [TestMethod] 51 | public void Test_AsyncGenericAction_ContractTestContextCreated() 52 | { 53 | // Arrange & Action 54 | var context = "".Test(async (int a) => { }); 55 | 56 | // Assert 57 | Assert.IsNotNull(context); 58 | } 59 | } 60 | } 61 | 62 | #pragma warning restore CS1998 63 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Contracts/ContractTestInitilizeCleanupTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MSTest.Extensions.Contracts; 3 | 4 | namespace MSTest.Extensions.Tests.Contracts 5 | { 6 | [TestClass] 7 | public class ContractTestInitilizeTests 8 | { 9 | private static int _staticField = 0; 10 | [ContractTestCase] 11 | public void TestInitializeAttribute() 12 | { 13 | "the 1st time TestInitialize run 1 time,_staticField==1".Test(() => 14 | { 15 | Assert.AreEqual(_staticField, 1); 16 | }); 17 | 18 | "the 2ed time TestInitialize run 2 times,_staticField==2".Test(() => 19 | { 20 | Assert.AreEqual(_staticField, 2); 21 | }); 22 | 23 | "the 3rd time TestInitialize run 3 times,_staticField==2".Test(() => 24 | { 25 | Assert.AreEqual(_staticField, 3); 26 | }); 27 | } 28 | [TestInitialize] 29 | public void Init() 30 | { 31 | _staticField = _staticField + 1; 32 | } 33 | } 34 | 35 | [TestClass] 36 | public class ContractTestCleanupTests 37 | { 38 | private static int _staticField = 0; 39 | [ContractTestCase] 40 | public void TheMethodNameYouWantToTest() 41 | { 42 | "the 1st time TestCleanup not run,_staticField==1".Test(() => 43 | { 44 | Assert.AreEqual(_staticField, 0); 45 | }); 46 | 47 | "the 2ed time TestCleanup not run 1 time,_staticField==5".Test(() => 48 | { 49 | Assert.AreEqual(_staticField, 5); 50 | }); 51 | 52 | "the 3rd time TestCleanup not run run 2 times,_staticField==10".Test(() => 53 | { 54 | Assert.AreEqual(_staticField, 10); 55 | }); 56 | } 57 | 58 | [TestCleanup] 59 | public void Cleanup() 60 | { 61 | _staticField = _staticField + 5; 62 | } 63 | } 64 | 65 | [TestClass] 66 | public class ContractTestInitilizeCleanupTests 67 | { 68 | private static int _staticField = 0; 69 | [ContractTestCase] 70 | public void TestInitializeAttribute() 71 | { 72 | "the 1st time TestInitialize run 1 time,TestCleanup not run,_staticField==1".Test(() => 73 | { 74 | Assert.AreEqual(_staticField, 1); 75 | }); 76 | 77 | "the 2ed time TestInitialize run 2 times,TestCleanup not run 1 time,_staticField==7".Test(() => 78 | { 79 | Assert.AreEqual(_staticField, 7); 80 | }); 81 | 82 | "the 3rd time TestInitialize run 3 times,TestCleanup not run run 2 times,_staticField==13".Test(() => 83 | { 84 | Assert.AreEqual(_staticField, 13); 85 | }); 86 | } 87 | [TestInitialize] 88 | public void Init() 89 | { 90 | _staticField = _staticField + 1; 91 | } 92 | [TestCleanup] 93 | public void Cleanup() 94 | { 95 | _staticField = _staticField + 5; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Contracts/ContractTestTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MSTest.Extensions.Contracts; 7 | 8 | #pragma warning disable CS1998 9 | 10 | namespace MSTest.Extensions.Tests.Contracts 11 | { 12 | [TestClass] 13 | public class ContractTestTests 14 | { 15 | [TestMethod] 16 | [DataRow(null, false, false, DisplayName = "If contract is null but action is not null, exception thrown.")] 17 | [DataRow("", true, false, DisplayName = "If contract is not null but action is null, exception thrown.")] 18 | [DataRow(null, false, true, DisplayName = 19 | "If contract is null but async action is not null, exception thrown.")] 20 | [DataRow("", true, true, DisplayName = "If contract is not null but async action is null, exception thrown.")] 21 | [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 22 | public void Test_NullArgument_ArgumentNullExceptionThrown(string contract, bool isActionNull, bool isAsync) 23 | { 24 | if (isAsync) 25 | { 26 | // Arrange 27 | var action = isActionNull ? (Func) null : async () => { }; 28 | 29 | // Action & Assert 30 | Assert.ThrowsException(() => { contract.Test(action); }); 31 | } 32 | else 33 | { 34 | // Arrange 35 | var action = isActionNull ? (Action) null : () => { }; 36 | 37 | // Action & Assert 38 | Assert.ThrowsException(() => { contract.Test(action); }); 39 | } 40 | } 41 | 42 | [TestMethod] 43 | public void Test_Action_TestCaseCreatedAndExecuted() 44 | { 45 | // Arrange 46 | const string contract = "Test contract description."; 47 | var executed = false; 48 | 49 | // Action 50 | contract.Test(() => executed = true); 51 | var result = ContractTest.Method.Current.Single().Result; 52 | 53 | // Assert 54 | Assert.AreEqual(result.DisplayName, contract); 55 | Assert.IsTrue(executed); 56 | } 57 | 58 | [TestMethod] 59 | public void Test_AsyncAction_TestCaseCreatedAndExecuted() 60 | { 61 | // Arrange 62 | const string contract = "Test contract description."; 63 | var executed = false; 64 | 65 | // Action 66 | contract.Test(async () => 67 | { 68 | await Task.Yield(); 69 | executed = true; 70 | }); 71 | var result = ContractTest.Method.Current.Single().Result; 72 | 73 | // Assert 74 | Assert.AreEqual(result.DisplayName, contract); 75 | Assert.IsTrue(executed); 76 | } 77 | } 78 | } 79 | 80 | #pragma warning restore CS1998 81 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Core/ContractTestCaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MSTest.Extensions.Core; 7 | 8 | #pragma warning disable CS1998 9 | 10 | namespace MSTest.Extensions.Tests.Core 11 | { 12 | [TestClass] 13 | public class ContractTestCaseTests 14 | { 15 | [TestMethod] 16 | [DataRow(null, false, false, DisplayName = "If contract is null but action is not null, exception thrown.")] 17 | [DataRow("", true, false, DisplayName = "If contract is not null but action is null, exception thrown.")] 18 | [DataRow(null, false, true, DisplayName = "If contract is null but async action is not null, exception thrown.")] 19 | [DataRow("", true, true, DisplayName = "If contract is not null but async action is null, exception thrown.")] 20 | [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 21 | public void Constructor_NullArgument_ArgumentNullExceptionThrown(string contract, bool isActionNull, bool isAsync) 22 | { 23 | if (isAsync) 24 | { 25 | // Arrange 26 | var action = isActionNull ? (Func) null : async () => { }; 27 | 28 | // Action & Assert 29 | Assert.ThrowsException(() => new ContractTestCase(contract, action)); 30 | } 31 | else 32 | { 33 | // Arrange 34 | var action = isActionNull ? (Action) null : () => { }; 35 | 36 | // Action & Assert 37 | Assert.ThrowsException(() => new ContractTestCase(contract, action)); 38 | } 39 | } 40 | 41 | [TestMethod] 42 | public void Execute_NothingDone_Passed() 43 | { 44 | // Arrange 45 | var @case = new ContractTestCase("", () => { }); 46 | 47 | // Action 48 | var result = @case.Result; 49 | 50 | // Assert 51 | Assert.AreEqual(result.Outcome, UnitTestOutcome.Passed); 52 | } 53 | 54 | [TestMethod] 55 | public void Execute_Wait_Passed() 56 | { 57 | // Arrange 58 | var waitTime = TimeSpan.FromMilliseconds(10); 59 | var @case = new ContractTestCase("", () => Thread.Sleep(waitTime)); 60 | 61 | // Action 62 | var result = @case.Result; 63 | 64 | // Assert 65 | Assert.IsTrue(result.Duration >= waitTime); 66 | } 67 | 68 | [TestMethod] 69 | public void Execute_CaseThrowsException_Failed() 70 | { 71 | // Arrange 72 | var @case = new ContractTestCase("", () => throw new InvalidOperationException()); 73 | 74 | // Action 75 | var result = @case.Result; 76 | 77 | // Assert 78 | Assert.AreEqual(result.Outcome, UnitTestOutcome.Failed); 79 | Assert.IsTrue(result.TestFailureException is InvalidOperationException); 80 | } 81 | 82 | [TestMethod] 83 | public void Execute_ConsoleWriteLine_OutputCollected() 84 | { 85 | // Assert 86 | const string output = "This is a test message."; 87 | var @case = new ContractTestCase("", () => Console.Write(output)); 88 | 89 | // Action 90 | var result = @case.Result; 91 | 92 | // Assert 93 | Assert.AreEqual(result.LogOutput, output); 94 | } 95 | 96 | [TestMethod] 97 | public void Execute_ErrorWriteLine_ErrorCollected() 98 | { 99 | // Assert 100 | const string error = "This is a error message."; 101 | var @case = new ContractTestCase("", () => Console.Error.Write(error)); 102 | 103 | // Action 104 | var result = @case.Result; 105 | 106 | // Assert 107 | Assert.AreEqual(result.LogError, error); 108 | } 109 | } 110 | } 111 | 112 | #pragma warning restore CS1998 113 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Core/TestCaseIndexerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MSTest.Extensions.Core; 6 | 7 | #pragma warning disable CS1998 8 | 9 | namespace MSTest.Extensions.Tests.Core 10 | { 11 | [TestClass] 12 | public class TestCaseIndexerTests 13 | { 14 | [TestMethod, SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] 15 | public void ThisMethod_NullArgument_ArgumentNullExceptionThrown() 16 | { 17 | // Arrange 18 | var indexer = new TestCaseIndexer(); 19 | 20 | // Action & Assert 21 | Assert.ThrowsException(() => indexer[null]); 22 | } 23 | 24 | private bool _exceptionOccurredInCurrentProperty; 25 | 26 | [TestInitialize, SuppressMessage("ReSharper", "UnusedVariable")] 27 | public void Current_NotFromTestMethod() 28 | { 29 | var indexer = new TestCaseIndexer(); 30 | // Run in TestInitialize, so that current method isn't running in TestMethod. 31 | try 32 | { 33 | var cases = indexer.Current; 34 | } 35 | catch (InvalidOperationException) 36 | { 37 | // This exception only occurs when Current property is not running in TestMethod. 38 | _exceptionOccurredInCurrentProperty = true; 39 | } 40 | } 41 | 42 | [TestMethod] 43 | public async Task Current_NotFromTestMethod_InvalidOperationExceptionThrown() 44 | { 45 | // Arrange 46 | // Arrange is transfered into Current_NotFromTestMethod. 47 | 48 | // Action & Assert 49 | Assert.IsTrue(_exceptionOccurredInCurrentProperty); 50 | } 51 | } 52 | } 53 | 54 | #pragma warning restore CS1998 55 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/MSTest.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net45; 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/MSTest.Extensions.Tests/Properties/GlobalSuppressions.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet-campus/CUnit/1affc9f7a34f5ddd508845aa974ba36097de0f30/tests/MSTest.Extensions.Tests/Properties/GlobalSuppressions.cs --------------------------------------------------------------------------------