Skip to content

Commit

Permalink
Better support for testing internal types (#146)
Browse files Browse the repository at this point in the history
* Abstracted test logic into TestBase class 
* Added types to better support testing internal scoped types
  • Loading branch information
roryprimrose authored Feb 17, 2024
1 parent a75e016 commit 431b378
Show file tree
Hide file tree
Showing 30 changed files with 919 additions and 499 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

- name: Generate coverage report
# run: reportgenerator -reports:**/coverage.cobertura.xml -targetdir:Report -reporttypes:HtmlInline_AzurePipelines;Cobertura
uses: danielpalme/ReportGenerator-GitHub-Action@5.1.25
uses: danielpalme/ReportGenerator-GitHub-Action@5.2.0
with:
reports: "**/coverage*cobertura.xml"
targetdir: "Report"
Expand Down
8 changes: 4 additions & 4 deletions Examples/AbstractType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
using FluentAssertions;
using NSubstitute;
using Xunit;

public abstract class AbstractType
{
public abstract string GetCustomValue(Guid id);

public string GetValue(Guid id)
{
return GetCustomValue(id);
}

public abstract string GetCustomValue(Guid id);
}

public class AbstractTypeTests : TestsSubstituteOf<AbstractType>
Expand All @@ -22,7 +22,7 @@ public void CanUseServiceOnMethod()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

SUT.GetCustomValue(id).Returns(expected);

var actual = SUT.GetValue(id);
Expand Down
8 changes: 4 additions & 4 deletions Examples/Examples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Divergic.Logging.Xunit" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1">
<PackageReference Include="Divergic.Logging.Xunit" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
31 changes: 12 additions & 19 deletions Examples/InternalScopedTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,28 @@
using NSubstitute;
using Xunit;

// As InternalClass and IInternalScope in this example are declared as internal in a different project, we need to do three things:
// 1. The project that declares the internal types to be tested needs to include [assembly:InternalsVisibleTo("<THIS_PROJECT>")]
// so that this test project can reference the types
// 2. Create an internal class declared once in the test project that can be the proxy to the Test<T> that points to an internal type
// 3. xUnit test classes need to be public and therefore cannot inherit from Test<T> where T is declared as internal.
// We can get around this by using the TestProxy class that is then declared as a field in the test class rather than the test class inheriting from Test<T>
// This idea comes from https://pedro.digitaldias.com/?p=494

// As per #2 above, declare this once in your test project
internal class TestProxy<T> : Tests<T> where T : class
{
}

public class InternalScopedTypes
// As InternalClass and IInternalScope in this example are declared as internal in a different project, we need to:
// 1. The project that declares the internal types to be tested needs to include [assembly:InternalsVisibleTo("<APPLICATION_PROJECT>")] in a .cs file
// or <InternalsVisibleTo Include="<APPLICATION_PROJECT>" /> in the .csproj file
// so that the test project can reference the types
// 2. xUnit test classes need to be public and therefore cannot inherit from Test<T> where T is declared as internal. Instead, it can inherit from TestsInternal
// and then create its own `public TypeToTest SUT => GetSUT<TypeToTest>();` property to access the internal type which is what Tests<T> already does for public types.

public class InternalScopedTypes : TestsInternal
{
// As per #3 above, declare the proxy instance that the unit test methods can reference
private readonly TestProxy<InternalClass> _proxy = new TestProxy<InternalClass>();

[Fact]
public void CanCreateAndUseInternalTypes()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

// Proxy is used here in a public xUnit test class in order to run against the Test<T> that points to an internal type
_proxy.Service<IInternalScope>().GetValue(id).Returns(expected);
Service<IInternalScope>().GetValue(id).Returns(expected);

var actual = _proxy.SUT.GetValue(id);
var actual = SUT.GetValue(id);

actual.Should().Be(expected);
}

private InternalClass SUT => GetSUT<InternalClass>();
}
6 changes: 3 additions & 3 deletions Examples/InternalTypePublicInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
// Tests<T> points to the public interface
public class InternalTypePublicInterface : Tests<IPublicInterface>
{
// The actual SUT type to create is identified by the TargetType property
protected override Type TargetType => typeof(InternalClassWithPublicInterface);

[Fact]
public void CanCreateAndUseInternalTypes()
{
Expand All @@ -25,4 +22,7 @@ public void CanCreateAndUseInternalTypes()

actual.Should().Be(expected);
}

// The actual SUT type to create is identified by the TargetType property
protected override Type TargetType => typeof(InternalClassWithPublicInterface);
}
8 changes: 4 additions & 4 deletions Neovolve.Streamline.NSubstitute.UnitTests/AbstractType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ namespace Neovolve.Streamline.NSubstitute.UnitTests;

public abstract class AbstractType
{
public string GetValue(Guid id)
public virtual string GetCustomValue(Guid id)
{
return GetCustomValue(id);
return id.ToString();
}

public virtual string GetCustomValue(Guid id)
public string GetValue(Guid id)
{
return id.ToString();
return GetCustomValue(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
79 changes: 79 additions & 0 deletions Neovolve.Streamline.NSubstitute.UnitTests/TestsInternalTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using System;
using FluentAssertions;
using global::NSubstitute;
using Xunit;

public class TestsInternalTests
{
[Fact]
public void CanCreateMissingServices()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsInternalWrapper<TypeWithDependency>();

wrapper.Service<ITargetService>().GetValue(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}

[Fact]
public void CanCreateSUTWithVirtualMembers()
{
// In this test we are proving that using Tests<T> returns an instance of T rather than a substitute of T
var expected = Guid.NewGuid().ToString();
var id = Guid.NewGuid();

var wrapper = new TestsInternalWrapper<TargetWithVirtual>();

wrapper.Service<ITargetService>().GetValue(id).Returns(expected);

wrapper.SUT.Should().NotBeNull();
wrapper.SUT.Service.Should().NotBeNull();

var actual = wrapper.SUT.RunTest(id);

actual.Should().Be(expected);
}

[Fact]
public void CanCreateWithConstructorProvidedService()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var service = Substitute.For<ITargetService>();

service.GetValue(id).Returns(expected);

var wrapper = new TestsInternalWrapper<TypeWithDependency>(service);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}

private class TargetWithVirtual
{
// ReSharper disable once UnusedMember.Global
// ReSharper disable once UnusedMember.Local
public TargetWithVirtual(ITargetService service)
{
Service = service;
}

public virtual string RunTest(Guid id)
{
var service = Service ?? throw new InvalidOperationException("No service defined");

return service.GetValue(id);
}

public ITargetService? Service { get; }
}
}
12 changes: 12 additions & 0 deletions Neovolve.Streamline.NSubstitute.UnitTests/TestsInternalWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using global::NSubstitute;

internal class TestsInternalWrapper<T> : TestsInternal where T : class
{
public TestsInternalWrapper(params object[] values) : base(values)
{
}

public T SUT => GetSUT<T>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using System;
using FluentAssertions;
using global::NSubstitute;
using Xunit;

public class TestsPartOfInternalTests
{
[Fact]
public void CanPartialMockSUT()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsPartOfInternalWrapper<TypeWithVirtual>();

wrapper.SUT.GetValueEx(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using global::NSubstitute;

internal class TestsPartOfInternalWrapper<T> : TestsPartOfInternal where T : class
{
public TestsPartOfInternalWrapper(params object[] values) : base(values)
{
}

public T SUT => GetSUT<T>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Neovolve.Streamline.NSubstitute.UnitTests;

public class TestsPartOfTests
{

[Fact]
public void CanPartialMockSUT()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using System;
using FluentAssertions;
using global::NSubstitute;
using Xunit;

public class TestsSubstituteOfInternalTests
{
[Fact]
public void CanSubstituteAbstractTypeWithoutParameters()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsSubstituteOfInternalWrapper<AbstractType>();

wrapper.SUT.GetCustomValue(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}

[Fact]
public void CanSubstituteAbstractTypeWithParameters()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsSubstituteOfInternalWrapper<AbstractTypeWithDependency>();

wrapper.SUT.GetValueFromService(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}

[Fact]
public void CanSubstituteTypeWithDefaultConstructor()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsSubstituteOfInternalWrapper<TypeWithDefaultConstructor>();

wrapper.SUT.GetCustomValue(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}

[Fact]
public void CanSubstituteVirtualMethod()
{
var id = Guid.NewGuid();
var expected = Guid.NewGuid().ToString();

var wrapper = new TestsSubstituteOfInternalWrapper<TypeWithDependency>();

wrapper.SUT.GetValueFromService(id).Returns(expected);

var actual = wrapper.SUT.GetValue(id);

actual.Should().Be(expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Neovolve.Streamline.NSubstitute.UnitTests;

using global::NSubstitute;

internal class TestsSubstituteOfInternalWrapper<T> : TestsSubstituteOfInternal where T : class
{
public TestsSubstituteOfInternalWrapper(params object[] values) : base(values)
{
}

public T SUT => GetSUT<T>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ namespace Neovolve.Streamline.NSubstitute.UnitTests;

public class TypeWithDefaultConstructor
{
public string GetValue(Guid id)
public virtual string GetCustomValue(Guid id)
{
return GetCustomValue(id);
return id.ToString();
}

public virtual string GetCustomValue(Guid id)
public string GetValue(Guid id)
{
return id.ToString();
return GetCustomValue(id);
}
}
Loading

0 comments on commit 431b378

Please sign in to comment.