Skip to content

Commit 7aea81d

Browse files
authored
Feat: unit testing (#3)
* Extract services. * Add unit testing project. * Using Verify. With custom converter for DevToys UI elements. * 100% code coverage. * Use `global.json` for .NET version. * Run linting during CI. * Extract packages to `Directory.Packages.props`.
1 parent 2723bc9 commit 7aea81d

38 files changed

+4376
-153
lines changed

.config/dotnet-tools.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"isRoot": true,
44
"tools": {
55
"csharpier": {
6-
"version": "0.29.1",
6+
"version": "0.29.2",
77
"commands": [
88
"dotnet-csharpier"
99
],

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ indent_size = 4
2020
indent_style = space
2121
indent_size = 2
2222

23+
# Verify settings.
24+
# https://github.com/VerifyTests/Verify/blob/main/docs/wiz/Windows_VisualStudioWithReSharper_Gui_Xunit_GitHubActions.md#editorconfig-settings
25+
[*.{received,verified}.{txt,xml,json}]
26+
charset = "utf-8-bom"
27+
end_of_line = lf
28+
indent_size = unset
29+
indent_style = unset
30+
insert_final_newline = false
31+
tab_width = unset
32+
trim_trailing_whitespace = false
33+
2334
[*.{cshtml,htm,html,razor}]
2435
indent_style = tab
2536
indent_size = tab

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Verify
2+
# https://github.com/VerifyTests/Verify/blob/main/docs/wiz/Windows_VisualStudioWithReSharper_Gui_Xunit_GitHubActions.md#source-control-settings
3+
*.verified.txt text eol=lf working-tree-encoding=UTF-8
4+
*.verified.xml text eol=lf working-tree-encoding=UTF-8
5+
*.verified.json text eol=lf working-tree-encoding=UTF-8

.github/workflows/ci.yml

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: "CI & CD: Build & Test .NET Solution, Create & Validate & Publish Nuget Package and Create Release"
1+
name: "CI & CD: Build & Test & Lint .NET Solution, Create & Validate & Publish Nuget Package and Create Release"
22

33
on:
44
push:
@@ -17,7 +17,7 @@ env:
1717
NuGetVersion: 0.0.0
1818

1919
jobs:
20-
build_test:
20+
job_build_test:
2121
name: Build & Test
2222
runs-on: ubuntu-latest
2323
steps:
@@ -26,15 +26,41 @@ jobs:
2626
- name: Setup .NET
2727
uses: actions/setup-dotnet@v4
2828
with:
29-
dotnet-version: "8"
29+
global-json-file: "./global.json"
3030

3131
- name: Build solution
3232
run: dotnet build
3333

3434
- name: Test solution
35-
run: dotnet test --no-build
35+
run: dotnet test --no-build --logger GitHubActions
36+
# Logger: https://github.com/Tyrrrz/GitHubActionsTestLogger
3637

37-
analyze_codeql:
38+
- name: Verify results
39+
if: failure()
40+
uses: actions/upload-artifact@v4
41+
with:
42+
name: verify-test-results
43+
path: |
44+
**/*.received.*
45+
46+
job_lint:
47+
name: Lint
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
52+
- name: Setup .NET
53+
uses: actions/setup-dotnet@v4
54+
with:
55+
global-json-file: "./global.json"
56+
57+
- name: Restore .NET tools
58+
run: dotnet tool restore
59+
60+
- name: Linting
61+
run: dotnet csharpier --check .
62+
63+
job_analyze_codeql:
3864
name: Run CodeQL scanning
3965
runs-on: ubuntu-latest
4066
permissions:
@@ -55,7 +81,7 @@ jobs:
5581
- name: Perform CodeQL Analysis
5682
uses: github/codeql-action/analyze@v3
5783

58-
analyze_sonarcloud:
84+
job_analyze_sonarcloud:
5985
name: Run SonarCloud scanning
6086
runs-on: windows-latest
6187
steps:
@@ -66,7 +92,10 @@ jobs:
6692
- name: Setup .NET
6793
uses: actions/setup-dotnet@v4
6894
with:
69-
dotnet-version: "8"
95+
global-json-file: "./global.json"
96+
97+
- name: Install dotnet-coverage
98+
run: dotnet tool install dotnet-coverage --global
7099

71100
- name: Cache SonarCloud packages
72101
uses: actions/cache@v4
@@ -96,16 +125,21 @@ jobs:
96125
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
97126
shell: powershell
98127
run: |
99-
.\.sonar\scanner\dotnet-sonarscanner begin `
100-
/k:"jerone_Jvw.DevToys.SemverCalculator" `
101-
/o:"jerone" `
102-
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
103-
/d:sonar.host.url="https://sonarcloud.io" `
104-
/d:sonar.exclusions="**/Pack/**/*.xml" `
105-
/d:sonar.verbose=true
128+
.\.sonar\scanner\dotnet-sonarscanner begin `
129+
/k:"jerone_Jvw.DevToys.SemverCalculator" `
130+
/o:"jerone" `
131+
/d:sonar.token="${{ secrets.SONAR_TOKEN }}" `
132+
/d:sonar.host.url="https://sonarcloud.io" `
133+
/d:sonar.exclusions="**/Pack/**/*.xml" `
134+
/d:sonar.verbose=true `
135+
/d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml
106136
107137
- name: Build solution
108-
run: dotnet build
138+
run: dotnet build --no-incremental
139+
140+
- name: Test solution
141+
run: dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml"
142+
# https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage/#dotnetcoverage
109143

110144
- name: End SonarCloud analyze
111145
env:
@@ -115,7 +149,7 @@ jobs:
115149
run: |
116150
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
117151
118-
nuget_pack:
152+
job_nuget_pack:
119153
name: Pack NuGet package
120154
runs-on: ubuntu-latest
121155
steps:
@@ -124,7 +158,7 @@ jobs:
124158
- name: Setup .NET
125159
uses: actions/setup-dotnet@v4
126160
with:
127-
dotnet-version: "8"
161+
global-json-file: "./global.json"
128162

129163
- name: Set version variable
130164
if: ${{ github.ref_type == 'tag' }}
@@ -146,15 +180,20 @@ jobs:
146180
name: ${{ env.NuGetArtifactName }}
147181
path: ${{ env.NuGetDirectory }}/*.nupkg
148182

149-
nuget_validate:
183+
job_nuget_validate:
150184
name: Validate NuGet package
151185
runs-on: ubuntu-latest
152-
needs: [nuget_pack]
186+
needs: [job_nuget_pack]
153187
steps:
188+
- uses: actions/checkout@v4
189+
with:
190+
sparse-checkout: "global.json" # Only need this file for this job.
191+
sparse-checkout-cone-mode: false
192+
154193
- name: Setup .NET
155194
uses: actions/setup-dotnet@v4
156195
with:
157-
dotnet-version: "8"
196+
global-json-file: "./global.json"
158197

159198
- name: Install nuget validator
160199
run: dotnet tool install Meziantou.Framework.NuGetPackageValidation.Tool --global
@@ -168,16 +207,28 @@ jobs:
168207
shell: pwsh
169208
run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg")
170209

171-
nuget_publish:
210+
job_nuget_publish:
172211
name: Publish NuGet package
173212
runs-on: ubuntu-latest
174-
needs: [nuget_validate, build_test, analyze_codeql, analyze_sonarcloud]
213+
needs:
214+
[
215+
job_build_test,
216+
job_lint,
217+
job_nuget_validate,
218+
job_analyze_codeql,
219+
job_analyze_sonarcloud,
220+
]
175221
if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
176222
steps:
223+
- uses: actions/checkout@v4
224+
with:
225+
sparse-checkout: "global.json" # Only need this file for this job.
226+
sparse-checkout-cone-mode: false
227+
177228
- name: Setup .NET
178229
uses: actions/setup-dotnet@v4
179230
with:
180-
dotnet-version: "8"
231+
global-json-file: "./global.json"
181232

182233
- uses: actions/download-artifact@v4
183234
with:
@@ -187,10 +238,10 @@ jobs:
187238
- name: Publish NuGet package
188239
run: dotnet nuget push ${{ env.NuGetDirectory }}/*.nupkg -k ${{ secrets.NUGET_APIKEY }} -s https://api.nuget.org/v3/index.json
189240

190-
release:
241+
job_release:
191242
name: Create release on GitHub
192243
runs-on: ubuntu-latest
193-
needs: [nuget_publish]
244+
needs: [job_nuget_publish]
194245
permissions:
195246
contents: write # Needed to create a release.
196247
steps:

Directory.Packages.props

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project>
2+
<PropertyGroup>
3+
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4+
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
8+
<PackageVersion Include="CSharpier.MsBuild" Version="0.29.2" />
9+
<PackageVersion Include="DevToys.Api" Version="2.0.5-preview" />
10+
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.25" />
11+
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
12+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
13+
<PackageVersion Include="Moq" Version="4.20.72" />
14+
<PackageVersion Include="Moq.Contrib.HttpClient" Version="1.4.0" />
15+
<PackageVersion Include="Semver" Version="2.3.0" />
16+
<PackageVersion Include="Verify.Xunit" Version="26.6.0" />
17+
<PackageVersion Include="xunit" Version="2.9.1" />
18+
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
19+
</ItemGroup>
20+
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Ignore Verify files.
2+
# https://github.com/VerifyTests/Verify/blob/main/docs/wiz/Windows_VisualStudioWithReSharper_Gui_Xunit_GitHubActions.md#includesexcludes
3+
*.received.*
4+
*.received/
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
[assembly: SuppressMessage(
4+
"Performance",
5+
"CA1869",
6+
Justification = "Do not cache JsonSerializerOptions in tests."
7+
)]
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Reflection;
2+
using DevToys.Api;
3+
4+
namespace Jvw.DevToys.SemverCalculator.Tests.Converters;
5+
6+
/// <summary>
7+
/// DevToys element converter used for Verify tool.
8+
/// </summary>
9+
public class DevToysElementConverter : WriteOnlyJsonConverter<IUIElement>
10+
{
11+
public override void Write(VerifyJsonWriter writer, IUIElement element)
12+
{
13+
writer.WriteStartObject();
14+
15+
var type = element.GetType();
16+
17+
// Prepend type name.
18+
writer.WriteMember(element, type.Name, "$type");
19+
20+
var props = type.GetTypeInfo().GetProperties(BindingFlags.Instance | BindingFlags.Public);
21+
foreach (var prop in props)
22+
{
23+
var name = prop.Name;
24+
var val = prop.GetValue(element);
25+
26+
// Replace button `OnClickAction` value with a placeholder, instead of delegate details.
27+
if (IsButtonClickAction(element, name, val))
28+
{
29+
writer.WriteMember(element, "{has click action}", prop.Name);
30+
}
31+
// Replace info-bar `OnCloseAction` value with a placeholder, instead of delegate details.
32+
else if (IsInfoBarCloseAction(element, name, val))
33+
{
34+
writer.WriteMember(element, "{has close action}", prop.Name);
35+
}
36+
else
37+
{
38+
writer.WritePropertyName(name);
39+
writer.Serializer.Serialize(writer, val);
40+
}
41+
}
42+
43+
writer.WriteEndObject();
44+
}
45+
46+
/// <summary>
47+
/// Detect if property is `OnClickAction` from a button.
48+
/// </summary>
49+
/// <param name="element">Element.</param>
50+
/// <param name="name">Property name.</param>
51+
/// <param name="val">Property value.</param>
52+
/// <returns>Whether property is `OnClickAction` from a button.</returns>
53+
private static bool IsButtonClickAction(IUIElement element, string name, object? val)
54+
{
55+
return element is IUIButton && name == nameof(IUIButton.OnClickAction) && val is not null;
56+
}
57+
58+
/// <summary>
59+
/// Detect if property is `OnCloseAction` from an info-bar.
60+
/// </summary>
61+
/// <param name="element">Element.</param>
62+
/// <param name="name">Property name.</param>
63+
/// <param name="val">Property value.</param>
64+
/// <returns>Whether property is `OnCloseAction` from an info-bar.</returns>
65+
private static bool IsInfoBarCloseAction(IUIElement element, string name, object? val)
66+
{
67+
return element is IUIInfoBar && name == nameof(IUIInfoBar.OnCloseAction) && val is not null;
68+
}
69+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
8+
9+
<IsPackable>false</IsPackable>
10+
<IsTestProject>true</IsTestProject>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="coverlet.collector">
15+
<PrivateAssets>all</PrivateAssets>
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
</PackageReference>
18+
<PackageReference Include="CSharpier.MsBuild">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
22+
<PackageReference Include="GitHubActionsTestLogger">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
27+
<PackageReference Include="Moq" />
28+
<PackageReference Include="Moq.Contrib.HttpClient" />
29+
<PackageReference Include="Verify.Xunit" />
30+
<PackageReference Include="xunit" />
31+
<PackageReference Include="xunit.runner.visualstudio">
32+
<PrivateAssets>all</PrivateAssets>
33+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
34+
</PackageReference>
35+
</ItemGroup>
36+
37+
<ItemGroup>
38+
<ProjectReference Include="..\Jvw.DevToys.SemverCalculator\Jvw.DevToys.SemverCalculator.csproj" />
39+
</ItemGroup>
40+
41+
<ItemGroup>
42+
<Using Include="Xunit" />
43+
</ItemGroup>
44+
45+
</Project>

0 commit comments

Comments
 (0)