Skip to content

Commit

Permalink
Code coverage improvements (#1357)
Browse files Browse the repository at this point in the history
* Code coverage: exclude obsolete members, prevent usage of stale coverage results in local build

* Add tests to increase coverage

* Add justification comment for coverage exclusion
  • Loading branch information
bkoelman authored Oct 22, 2023
1 parent ed1ff0d commit 9830302
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 3 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,13 @@ jobs:
dotnet build --no-restore --configuration Release /p:VersionSuffix=$env:PACKAGE_VERSION_SUFFIX
- name: Test
run: |
dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;summary.includeSkippedTests=true" -- RunConfiguration.CollectSourceInformation=true DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DeterministicReport=true
dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;summary.includeSkippedTests=true"
- name: Upload coverage to codecov.io
if: matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
verbose: true
- name: Generate packages
shell: pwsh
run: |
Expand Down
5 changes: 4 additions & 1 deletion Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ Write-Host "$(pwsh --version)"
Write-Host "Active .NET SDK: $(dotnet --version)"
Write-Host "Using version suffix: $versionSuffix"

Remove-Item -Recurse -Force artifacts -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force * -Include coverage.cobertura.xml

dotnet tool restore
VerifySuccessExitCode

dotnet build --configuration Release /p:VersionSuffix=$versionSuffix
VerifySuccessExitCode

dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.DeterministicReport=true
dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage"
VerifySuccessExitCode

dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs
Expand Down
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<IsPackable>false</IsPackable>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
<RunSettingsFilePath>$(MSBuildThisFileDirectory)tests.runsettings</RunSettingsFilePath>
<JsonApiDotNetCoreVersionPrefix>5.4.1</JsonApiDotNetCoreVersionPrefix>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions JsonApiDotNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
CodingGuidelines.ruleset = CodingGuidelines.ruleset
CSharpGuidelinesAnalyzer.config = CSharpGuidelinesAnalyzer.config
Directory.Build.props = Directory.Build.props
tests.runsettings = tests.runsettings
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}"
Expand Down
229 changes: 229 additions & 0 deletions test/NoEntityFrameworkTests/TagTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
using System.Net;
using System.Text.Json;
using FluentAssertions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.Extensions.DependencyInjection;
using NoEntityFrameworkExample.Models;
using TestBuildingBlocks;
using Xunit;

namespace NoEntityFrameworkTests;

public sealed class TagTests : IntegrationTest, IClassFixture<NoLoggingWebApplicationFactory<Tag>>
{
private readonly NoLoggingWebApplicationFactory<Tag> _factory;

protected override JsonSerializerOptions SerializerOptions
{
get
{
var options = _factory.Services.GetRequiredService<IJsonApiOptions>();
return options.SerializerOptions;
}
}

public TagTests(NoLoggingWebApplicationFactory<Tag> factory)
{
_factory = factory;
}

[Fact]
public async Task Can_get_primary_resources()
{
// Arrange
const string route = "/api/tags";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(3);

responseDocument.Meta.Should().ContainTotal(3);
}

[Fact]
public async Task Can_filter_in_primary_resources()
{
// Arrange
const string route = "/api/tags?filter=equals(name,'Personal')";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Personal"));

responseDocument.Meta.Should().ContainTotal(1);
}

[Fact]
public async Task Can_filter_in_related_resources()
{
// Arrange
const string route = "/api/tags?filter=has(todoItems,equals(description,'Check emails'))";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Business"));

responseDocument.Meta.Should().ContainTotal(1);
}

[Fact]
public async Task Can_sort_on_attribute_in_primary_resources()
{
// Arrange
const string route = "/api/tags?sort=-id";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(3);
responseDocument.Data.ManyValue[0].Id.Should().Be("3");
responseDocument.Data.ManyValue[1].Id.Should().Be("2");
responseDocument.Data.ManyValue[2].Id.Should().Be("1");
}

[Fact]
public async Task Can_sort_on_count_in_primary_resources()
{
// Arrange
const string route = "/api/tags?sort=-count(todoItems),id";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(3);
responseDocument.Data.ManyValue[0].Id.Should().Be("1");
responseDocument.Data.ManyValue[1].Id.Should().Be("2");
responseDocument.Data.ManyValue[2].Id.Should().Be("3");
}

[Fact]
public async Task Can_paginate_in_primary_resources()
{
// Arrange
const string route = "/api/tags?page[size]=1&page[number]=2&sort=id";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("name").With(value => value.Should().Be("Family"));

responseDocument.Meta.Should().ContainTotal(3);
}

[Fact]
public async Task Can_select_fields_in_primary_resources()
{
// Arrange
const string route = "/api/tags?fields[tags]=todoItems";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldNotBeEmpty();
responseDocument.Data.ManyValue.Should().AllSatisfy(resource => resource.Attributes.Should().BeNull());
responseDocument.Data.ManyValue.Should().AllSatisfy(resource => resource.Relationships.ShouldOnlyContainKeys("todoItems"));
}

[Fact]
public async Task Can_include_in_primary_resources()
{
// Arrange
const string route = "/api/tags?include=todoItems.owner";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.Should().NotBeEmpty();
responseDocument.Included.Should().NotBeEmpty();
}

[Fact]
public async Task Can_get_primary_resource()
{
// Arrange
const string route = "/api/tags/1";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Id.Should().Be("1");
}

[Fact]
public async Task Can_get_secondary_resources()
{
// Arrange
const string route = "/api/tags/1/todoItems?sort=id";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(3);
responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Make homework"));
responseDocument.Data.ManyValue[1].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Book vacation"));
responseDocument.Data.ManyValue[2].Attributes.ShouldContainKey("description").With(value => value.Should().Be("Cook dinner"));

responseDocument.Meta.Should().ContainTotal(3);
}

[Fact]
public async Task Can_get_ToMany_relationship()
{
// Arrange
const string route = "/api/tags/2/relationships/todoItems";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync<Document>(route);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK);

responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Id.Should().Be("3");

responseDocument.Meta.Should().ContainTotal(1);
}

protected override HttpClient CreateClient()
{
return _factory.CreateClient();
}
}
2 changes: 1 addition & 1 deletion test/TestBuildingBlocks/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;

// https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#excluding-from-coverage
// Justification: This assembly contains building blocks for writing tests. It does not contain code that ships.
[assembly: ExcludeFromCodeCoverage]
16 changes: 16 additions & 0 deletions tests.runsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<RunConfiguration>
<CollectSourceInformation>true</CollectSourceInformation>
</RunConfiguration>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<ExcludeByAttribute>ObsoleteAttribute,GeneratedCodeAttribute</ExcludeByAttribute>
<DeterministicReport>true</DeterministicReport>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>

0 comments on commit 9830302

Please sign in to comment.