Skip to content

Commit

Permalink
Unit tests added
Browse files Browse the repository at this point in the history
  • Loading branch information
hwndmaster committed Jun 20, 2021
1 parent 864b18f commit 24b630e
Show file tree
Hide file tree
Showing 14 changed files with 531 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*.user
*.userosscache
*.sln.docstates
experiments.dib

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
Expand Down
96 changes: 96 additions & 0 deletions PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using AutoFixture;
using Genius.PriceChecker.Core.Messages;
using Genius.PriceChecker.Core.Models;
using Genius.PriceChecker.Core.Repositories;
using Genius.PriceChecker.Core.Services;
using Genius.PriceChecker.Infrastructure.Events;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace Genius.PriceChecker.Core.Tests.Repositories
{
public class SettingsRepositoryTests
{
private readonly Fixture _fixture = new();
private readonly Mock<IEventBus> _eventBusMock = new();
private readonly Mock<IPersister> _persisterMock = new();

[Fact]
public void Constructor__Previous_settings_exist__Loaded()
{
// Arrange
var settings = _fixture.Create<Settings>();

// Act
var sut = CreateSystemUnderTest(settings);

// Verify
Assert.Equal(settings, sut.Get());
}

[Fact]
public void Constructor__Previous_settings_dont_exist__Loaded_default()
{
// Arrange
Settings settings = null;

// Act
var sut = CreateSystemUnderTest(settings);

// Verify
var result = sut.Get();
Assert.False(result.AutoRefreshEnabled);
Assert.Equal(1440, result.AutoRefreshMinutes);
}

[Fact]
public void Get__returns_currently_loaded_settings()
{
// Arrange
var settings = _fixture.Create<Settings>();
var sut = CreateSystemUnderTest(settings);

// Act
var result = sut.Get();

// Verify
Assert.Equal(settings, result);
}

[Fact]
public void Store__Argument_not_provided__throws_exception()
{
// Arrange
var sut = CreateSystemUnderTest();

// Act & Verify
Assert.Throws<ArgumentNullException>(() => sut.Store(null));
}

[Fact]
public void Store__Replaces_existing_settings_and_updates_cache_and_fires_event()
{
// Arrange
var sut = CreateSystemUnderTest();
var newSettings = _fixture.Create<Settings>();

// Act
sut.Store(newSettings);

// Verify
Assert.Equal(newSettings, sut.Get());
_persisterMock.Verify(x => x.Store(It.IsAny<string>(), newSettings));
_eventBusMock.Verify(x => x.Publish(It.Is<SettingsUpdatedEvent>(e => e.Settings == newSettings)), Times.Once);
}

private SettingsRepository CreateSystemUnderTest(Settings settings = null)
{
_persisterMock.Setup(x => x.Load<Settings>(It.IsAny<string>()))
.Returns(settings);
return new SettingsRepository(_eventBusMock.Object, _persisterMock.Object,
Mock.Of<ILogger<SettingsRepository>>());
}
}
}
151 changes: 151 additions & 0 deletions PriceChecker.Core.Tests/Services/PriceSeekerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoFixture;
using Genius.PriceChecker.Core.Models;
using Genius.PriceChecker.Core.Services;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;

namespace Genius.PriceChecker.Core.Tests.Services
{
public class PriceSeekerTests
{
private readonly Fixture _fixture = new();
private readonly Mock<ITrickyHttpClient> _httpMock = new();
private readonly Mock<IIoService> _ioMock = new();
private readonly Mock<ILogger<PriceSeeker>> _loggerMock = new();

private readonly PriceSeeker _sut;

public PriceSeekerTests()
{
_fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2));

_sut = new PriceSeeker(_httpMock.Object, _ioMock.Object, _loggerMock.Object);
}

[Fact]
public async Task SeekAsync__Happy_flow_scenario()
{
// Arrange
var product = CreateSampleProduct();

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Equal(product.Sources.Length, result.Length);
_ioMock.Verify(x => x.WriteTextToFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning, Times.Never());
TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error, Times.Never());
}

[Fact]
public async Task SeekAsync__Content_wasnt_downloaded__Returns_null()
{
// Arrange
var product = CreateSampleProduct(sourcesCount: 1);
_httpMock.Setup(x => x.DownloadContent(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((string)null);

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Empty(result);
}

[Fact]
public async Task SeekAsync__Content_not_matched_the_pattern__Returns_null_and_dumps_file()
{
// Arrange
var product = CreateSampleProduct(sourcesCount: 1);
var contentNotMatchingAnything = _fixture.Create<string>();
_httpMock.Setup(x => x.DownloadContent(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(contentNotMatchingAnything);

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Empty(result);
_ioMock.Verify(x => x.WriteTextToFile(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error);
}

[Fact]
public async Task SeekAsync__DecimalDelimiter_isnt_default__Considered_in_price_parse()
{
// Arrange
var product = CreateSampleProduct(sourcesCount: 1, delimiter: ';');

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Single(result);
Assert.NotEqual(0, result[0].Price);
Assert.NotEqual((int)result[0].Price, result[0].Price); // Check if it is decimal
}

[Fact]
public async Task SeekAsync__Price_is_invalid__Returns_null()
{
// Arrange
var product = CreateSampleProduct(sourcesCount: 1);
var contentPriceInvalid = "`0`";
_httpMock.Setup(x => x.DownloadContent(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(contentPriceInvalid);

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Empty(result);
TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning);
}

[Fact]
public async Task SeekAsync__Price_isnt_convertible__Returns_null()
{
// Arrange
var product = CreateSampleProduct(sourcesCount: 1);
var contentPriceInvalid = "`not-a-number`";
_httpMock.Setup(x => x.DownloadContent(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(contentPriceInvalid);

// Act
var result = await _sut.SeekAsync(product, new CancellationToken());

// Verify
Assert.Empty(result);
TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error);
}

private Product CreateSampleProduct(int sourcesCount = 3, char delimiter = '.')
{
var product = _fixture.Build<Product>()
.With(x => x.Sources, _fixture.CreateMany<ProductSource>(sourcesCount).ToArray())
.Create();
foreach (var productSource in product.Sources)
{
productSource.Agent = _fixture.Build<Agent>()
.With(x => x.Url, _fixture.Create<string>() + "{0}")
.With(x => x.PricePattern, $@"`(?<price>[\d\{delimiter}]+)`")
.With(x => x.DecimalDelimiter, delimiter)
.Create();

var priceDec = _fixture.Create<int>();
var priceFlt = _fixture.Create<int>();
var content = $"{_fixture.Create<string>()}`{priceDec}{delimiter}{priceFlt}`{_fixture.Create<string>()}";

_httpMock.Setup(x => x.DownloadContent(
It.Is<string>(url => url == string.Format(productSource.Agent.Url, productSource.AgentArgument)), It.IsAny<CancellationToken>()))
.ReturnsAsync(content);
}
return product;
}
}
}
20 changes: 20 additions & 0 deletions PriceChecker.Core.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using Microsoft.Extensions.Logging;
using Moq;

namespace Genius.PriceChecker.Core.Tests
{
public static class TestHelpers
{
public static void VerifyLogger<T>(Mock<ILogger<T>> loggerMock, LogLevel logLevel, Times? times = null)
{
if (times == null)
times = Times.Once();
loggerMock.Verify(x => x.Log(logLevel,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>) It.IsAny<object>()), times.Value);
}
}
}
1 change: 1 addition & 0 deletions PriceChecker.Core/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static void Configure(IServiceCollection services)
services.AddSingleton<ISettingsRepository, SettingsRepository>();

// Services
services.AddSingleton<IIoService, IoService>();
services.AddTransient<IPersister, Persister>();
services.AddTransient<IPriceSeeker, PriceSeeker>();
services.AddTransient<IProductStatusProvider, ProductStatusProvider>();
Expand Down
26 changes: 26 additions & 0 deletions PriceChecker.Core/Services/IoService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;

namespace Genius.PriceChecker.Core.Services
{
public interface IIoService
{
bool FileExists(string path);
string ReadTextFromFile(string path);
void WriteTextToFile(string path, string content);
}

[ExcludeFromCodeCoverage]
public class IoService : IIoService
{
public bool FileExists(string path)
=> File.Exists(path);

public string ReadTextFromFile(string path)
=> File.ReadAllText(path);

public void WriteTextToFile(string path, string content)
=> File.WriteAllText(path, content, Encoding.UTF8);
}
}
19 changes: 9 additions & 10 deletions PriceChecker.Core/Services/Persister.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
using System.Threading;

namespace Genius.PriceChecker.Core.Services
{
public interface IPersister
public interface IPersister
{
T Load<T>(string filePath);
T[] LoadCollection<T>(string filePath);
void Store(string filePath, object data);
}

[ExcludeFromCodeCoverage]
internal sealed class Persister : IPersister
{
private readonly IIoService _io;
private readonly JsonSerializerOptions _jsonOptions;
private static ReaderWriterLockSlim _locker = new();

public Persister()
public Persister(IIoService io)
{
_io = io;
_jsonOptions = new JsonSerializerOptions {
PropertyNameCaseInsensitive = true,
WriteIndented = true
Expand All @@ -31,11 +30,11 @@ public T Load<T>(string filePath)
_locker.EnterReadLock();
try
{
if (!File.Exists(filePath))
if (!_io.FileExists(filePath))
{
return default(T);
}
var content = File.ReadAllText(filePath);
var content = _io.ReadTextFromFile(filePath);
return JsonSerializer.Deserialize<T>(content, _jsonOptions);
}
finally
Expand All @@ -49,11 +48,11 @@ public T[] LoadCollection<T>(string filePath)
_locker.EnterReadLock();
try
{
if (!File.Exists(filePath))
if (!_io.FileExists(filePath))
{
return new T[0];
}
var content = File.ReadAllText(filePath);
var content = _io.ReadTextFromFile(filePath);
return JsonSerializer.Deserialize<T[]>(content, _jsonOptions);
}
finally
Expand All @@ -68,7 +67,7 @@ public void Store(string filePath, object data)
try
{
var json = JsonSerializer.Serialize(data, _jsonOptions);
File.WriteAllText(filePath, json);
_io.WriteTextToFile(filePath, json);
}
finally
{
Expand Down
Loading

0 comments on commit 24b630e

Please sign in to comment.