Skip to content

Commit

Permalink
Replaced Moq with NSubstitute
Browse files Browse the repository at this point in the history
  • Loading branch information
Norhaven committed Jan 29, 2024
1 parent e5b2021 commit a4cf071
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 63 deletions.
8 changes: 4 additions & 4 deletions GrowthBook.Tests/ApiTests/ApiUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Threading.Tasks;
using GrowthBook.Api;
using Microsoft.Extensions.Logging;
using Moq;
using NSubstitute;

namespace GrowthBook.Tests.ApiTests;

Expand All @@ -16,15 +16,15 @@ public abstract class ApiUnitTest<T> : UnitTest
protected const string SecondFeatureId = nameof(SecondFeatureId);

protected readonly ILogger<T> _logger;
protected readonly Mock<IGrowthBookFeatureCache> _cache;
protected readonly IGrowthBookFeatureCache _cache;
protected readonly Feature _firstFeature;
protected readonly Feature _secondFeature;
protected readonly Dictionary<string, Feature> _availableFeatures;

public ApiUnitTest()
{
_logger = Mock.Of<ILogger<T>>();
_cache = StrictMockOf<IGrowthBookFeatureCache>();
_logger = Substitute.For<ILogger<T>>();
_cache = Substitute.For<IGrowthBookFeatureCache>();

_firstFeature = new() { DefaultValue = 1 };
_secondFeature = new() { DefaultValue = 2 };
Expand Down
21 changes: 10 additions & 11 deletions GrowthBook.Tests/ApiTests/FeatureRefreshWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
using FluentAssertions;
using GrowthBook.Api;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json;
using NSubstitute;
using Xunit;

namespace GrowthBook.Tests.ApiTests;
Expand Down Expand Up @@ -112,7 +112,7 @@ public FeatureRefreshWorkerTests()
_httpClientFactory = new TestHttpClientFactory();
_httpClientFactory.ResponseContent = _availableFeatures;
_httpClientFactory.StreamResponseContent = _availableFeatures.Take(1).ToDictionary(x => x.Key, x => x.Value);
_worker = new(_logger, _httpClientFactory, _config, _cache.Object);
_worker = new(_logger, _httpClientFactory, _config, _cache);
}

[Fact]
Expand All @@ -131,15 +131,14 @@ public async Task HttpRequestWithSuccessStatusThatPrefersApiCallWillGetFeaturesF
_config.PreferServerSentEvents = false;

_cache
.Setup(x => x.RefreshWith(It.IsAny<IDictionary<string, Feature>>(), It.IsAny<CancellationToken?>()))
.Returns(Task.CompletedTask)
.Verifiable();
.RefreshWith(Arg.Any<IDictionary<string, Feature>>(), Arg.Any<CancellationToken?>())
.Returns(Task.CompletedTask);

var features = await _worker.RefreshCacheFromApi();

features.Should().BeEquivalentTo(_availableFeatures);

Mock.Verify(_cache);
await _cache.Received(1).RefreshWith(Arg.Any<IDictionary<string, Feature>>(), Arg.Any<CancellationToken?>());
}

[Fact]
Expand All @@ -157,13 +156,13 @@ public async Task HttpResponseWithServerSentEventSupportWillStartBackgroundListe
var resetEvent = new AutoResetEvent(false);

_cache
.Setup(x => x.RefreshWith(It.IsAny<IDictionary<string, Feature>>(), It.IsAny<CancellationToken?>()))
.Callback((IDictionary<string, Feature> x, CancellationToken? _) =>
.RefreshWith(Arg.Any<IDictionary<string, Feature>>(), Arg.Any<CancellationToken?>())
.Returns(Task.CompletedTask)
.AndDoes(x =>
{
cachedResults.Enqueue(x);
cachedResults.Enqueue((IDictionary<string, Feature>)x[0]);
resetEvent.Set();
})
.Returns(Task.CompletedTask);
});

var features = await _worker.RefreshCacheFromApi();

Expand Down
70 changes: 26 additions & 44 deletions GrowthBook.Tests/ApiTests/FeatureRepositoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,40 @@
using System.Threading.Tasks;
using GrowthBook.Api;
using Microsoft.Extensions.Logging;
using Moq;
using NSubstitute;
using Xunit;

namespace GrowthBook.Tests.ApiTests;

public class FeatureRepositoryTests : ApiUnitTest<FeatureRepository>
{
private readonly Mock<IGrowthBookFeatureRefreshWorker> _backgroundWorker;
private readonly IGrowthBookFeatureRefreshWorker _backgroundWorker;
private readonly FeatureRepository _featureRepository;

public FeatureRepositoryTests()
{
_backgroundWorker = StrictMockOf<IGrowthBookFeatureRefreshWorker>();
_featureRepository = new(_logger, _cache.Object, _backgroundWorker.Object);
_backgroundWorker = Substitute.For<IGrowthBookFeatureRefreshWorker>();
_featureRepository = new(_logger, _cache, _backgroundWorker);
}

[Fact]
public void CancellingRepositoryWillCancelBackgroundWorker()
{
_backgroundWorker
.Setup(x => x.Cancel())
.Verifiable();

_featureRepository.Cancel();

_backgroundWorker.Verify(x => x.Cancel(), Times.Once, "Cancelling the background worker did not succeed");
_backgroundWorker.Received(1).Cancel();
}

[Theory]
[InlineData(false, null)]
[InlineData(false, false)]
public async Task GettingFeaturesWhenApiCallIsUnnecessaryWillGetFromCache(bool isCacheExpired, bool? isForcedRefresh)
{
_cache
.SetupGet(x => x.IsCacheExpired)
.Returns(isCacheExpired)
.Verifiable();
_cache.IsCacheExpired.Returns(isCacheExpired);

_cache
.Setup(x => x.GetFeatures(It.IsAny<CancellationToken?>()))
.ReturnsAsync(_availableFeatures)
.Verifiable();
.GetFeatures(Arg.Any<CancellationToken?>())
.Returns(_availableFeatures);

var options = isForcedRefresh switch
{
Expand All @@ -57,7 +49,7 @@ public async Task GettingFeaturesWhenApiCallIsUnnecessaryWillGetFromCache(bool i

var features = await _featureRepository.GetFeatures(options);

Mock.Verify(_cache);
await _cache.Received(1).GetFeatures(Arg.Any<CancellationToken?>());
}

[Theory]
Expand All @@ -67,25 +59,17 @@ public async Task GettingFeaturesWhenApiCallIsUnnecessaryWillGetFromCache(bool i
[InlineData(true, true)]
public async Task GettingFeaturesWhenApiCallIsRequiredWithoutWaitingForRetrievalWillGetFromCache(bool isCacheExpired, bool? isForcedRefresh)
{
_cache
.SetupGet(x => x.IsCacheExpired)
.Returns(isCacheExpired)
.Verifiable();
_cache.IsCacheExpired.Returns(isCacheExpired);

_cache
.SetupGet(x => x.FeatureCount)
.Returns(_availableFeatures.Count)
.Verifiable();
_cache.FeatureCount.Returns(_availableFeatures.Count);

_cache
.Setup(x => x.GetFeatures(It.IsAny<CancellationToken?>()))
.ReturnsAsync(_availableFeatures)
.Verifiable();
.GetFeatures(Arg.Any<CancellationToken?>())
.Returns(_availableFeatures);

_backgroundWorker
.Setup(x => x.RefreshCacheFromApi(It.IsAny<CancellationToken?>()))
.ReturnsAsync(_availableFeatures)
.Verifiable();
.RefreshCacheFromApi(Arg.Any<CancellationToken?>())
.Returns(_availableFeatures);

var options = isForcedRefresh switch
{
Expand All @@ -95,7 +79,10 @@ public async Task GettingFeaturesWhenApiCallIsRequiredWithoutWaitingForRetrieval

var features = await _featureRepository.GetFeatures(options);

Mock.Verify(_cache, _backgroundWorker);
_ = _cache.Received(2).IsCacheExpired;
_ = _cache.Received(1).FeatureCount;
_ = _cache.Received(1).GetFeatures(Arg.Any<CancellationToken?>());
_ = _backgroundWorker.Received(1).RefreshCacheFromApi(Arg.Any<CancellationToken?>());
}

[Theory]
Expand All @@ -105,20 +92,13 @@ public async Task GettingFeaturesWhenApiCallIsRequiredWithoutWaitingForRetrieval
[InlineData(true, true)]
public async Task GettingFeaturesWhenApiCallIsRequiredWithWaitingForRetrievalWillGetFromApiCallInsteadOfCache(bool isCacheEmpty, bool? isForcedWait)
{
_cache
.SetupGet(x => x.IsCacheExpired)
.Returns(true)
.Verifiable();
_cache.IsCacheExpired.Returns(true);

_cache
.SetupGet(x => x.FeatureCount)
.Returns(isCacheEmpty ? 0 : 1)
.Verifiable();
_cache.FeatureCount.Returns(isCacheEmpty ? 0 : 1);

_backgroundWorker
.Setup(x => x.RefreshCacheFromApi(It.IsAny<CancellationToken?>()))
.ReturnsAsync(_availableFeatures)
.Verifiable();
.RefreshCacheFromApi(Arg.Any<CancellationToken?>())
.Returns(_availableFeatures);

var options = isForcedWait switch
{
Expand All @@ -128,6 +108,8 @@ public async Task GettingFeaturesWhenApiCallIsRequiredWithWaitingForRetrievalWil

var features = await _featureRepository.GetFeatures(options);

Mock.Verify(_cache, _backgroundWorker);
_ = _cache.Received(2).IsCacheExpired;
_ = _cache.Received(2).FeatureCount;
_ = _backgroundWorker.Received(1).RefreshCacheFromApi(Arg.Any<CancellationToken?>());
}
}
2 changes: 1 addition & 1 deletion GrowthBook.Tests/GrowthBook.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
Expand Down
3 changes: 0 additions & 3 deletions GrowthBook.Tests/UnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Text;
using System.Threading.Tasks;
using GrowthBook.Tests.Json;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
Expand Down Expand Up @@ -227,6 +226,4 @@ public static IEnumerable<object[]> GetMappedTestsInCategory(Type categoryType)

return instance;
}

protected Mock<T> StrictMockOf<T>() where T : class => new Mock<T>(MockBehavior.Strict);
}

0 comments on commit a4cf071

Please sign in to comment.