I have the following component:
@using BitcoinChallengeBlazorApp.Services;
@using BitcoinChallenge.Entities;
@using System.Globalization;
@using System.Net.Http;
@using Newtonsoft.Json;
@inject HttpClient Http;
@inject IJSRuntime JSRuntime;
@inject PeriodicTimer PeriodTimer;
<p>@FormattedPrice</p>
@code{
private const string BitcoinPriceProvider = "https://api.coinbase.com/v2/prices/spot?currency=EUR";
private static readonly CultureInfo cultureInfo = new CultureInfo("de-DE", false);
protected decimal Price { get; set; }
protected string FormattedPrice => this.Price.ToString("c", cultureInfo);
protected override async Task OnInitializedAsync() {
try {
this.Price = await this.fetchPrice();
_ = PeriodTimer.Start(async (e) => {
await InvokeAsync(async () => {
this.Price = await this.fetchPrice();
this.StateHasChanged();
});
});
}
catch (Exception e) {
await JSRuntime.InvokeAsync<object>("alert", e.ToString());
}
}
private async Task<decimal> fetchPrice() {
HttpResponseMessage priceResponse = await Http.GetAsync(BitcoinPriceProvider);
priceResponse.EnsureSuccessStatusCode();
string responseBody = await priceResponse.Content.ReadAsStringAsync();
BitcoinPriceWrapper bitcoinPriceWrapper = JsonConvert.DeserializeObject<BitcoinPriceWrapper>(responseBody);
return decimal.Parse(bitcoinPriceWrapper.Data.Amount);
}
}
Assuming BitcoinSettings.RefreshTimeInSeconds
is set to 10, fetchPrice()
should occur every 10 seconds.
The Timer is now wrapped in this class:
using System;
using System.Threading;
namespace BitcoinChallengeBlazorApp.Services {
public class PeriodicTimer {
public int RefreshTimeInSeconds { get; private set; }
public PeriodicTimer(AppSettings appSettings) {
this.RefreshTimeInSeconds = appSettings.RefreshTimeInSeconds;
}
public Timer Start(TimerCallback callback) {
TimeSpan startTimeSpan = TimeSpan.Zero;
TimeSpan periodTimeSpan = TimeSpan.FromSeconds(this.RefreshTimeInSeconds);
return new Timer(callback, null, startTimeSpan, periodTimeSpan);
}
}
}
Which is injected by Program.cs
:
using BitcoinChallengeBlazorApp.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace BitcoinChallengeBlazorApp {
public class Program {
public static async Task Main(string[] args) {
WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
_ = builder.Services.AddTransient(
serviceProvide => new HttpClient {
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
}
);
_ = builder.Services.AddSingleton<PeriodicTimer>(
new PeriodicTimer(builder.Configuration.Get<AppSettings>())
);
await builder.Build()
.RunAsync();
}
}
}
How can I prove it in a test?
I tried making a test like this:
using BitcoinChallenge.Entities;
using BitcoinChallengeBlazorApp;
using BitcoinChallengeBlazorApp.Services;
using Microsoft.AspNetCore.Components.Testing;
using Microsoft.JSInterop;
using Moq;
using Nancy.Json;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Xunit;
using Index = BitcoinChallengeBlazorApp.Pages.Index;
namespace BitcoinChalllengeBlazorApp.UnitTests {
public class IndexTest {
readonly TestHost host = new TestHost();
readonly decimal testAmount = 8448.947391885M;
readonly int testRefreshRate = 10;
[Fact]
public void ItFetchesPriceAndSetsTheValue() {
// Arrange
_ = this.SetMockRuntime();
_ = this.CreateMockHttpClientAsync();
_ = this.CreateTimer();
// Act
RenderedComponent<Index> componentUnderTest = this.host.AddComponent<Index>();
// Assert
string displayAmount = componentUnderTest.Find("p").InnerText.Split(new char[] { ' ' })[0];
NumberStyles style = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands;
CultureInfo provider = new CultureInfo("de-DE");
decimal resultAmount = decimal.Parse(displayAmount, style, provider);
decimal expectedAmount = decimal.Parse($"{this.testAmount:n2}");
Assert.Equal(expectedAmount, resultAmount);
System.Threading.Thread.Sleep(60000)
Assert.Equal(6, MockBitcoinMessageHandler.requestCount);
}
public Mock<IJSRuntime> SetMockRuntime() {
Mock<IJSRuntime> jsRuntimeMock = new Mock<IJSRuntime>();
this.host.AddService(jsRuntimeMock.Object);
return jsRuntimeMock;
}
private HttpClient CreateMockHttpClientAsync() {
MockBitcoinMessageHandler mockHttpMessageHandler = new MockBitcoinMessageHandler(this.CreateMockResponse);
HttpClient httpClient = new HttpClient(mockHttpMessageHandler);
this.host.AddService(httpClient);
return httpClient;
}
private Task<HttpResponseMessage> CreateMockResponse() {
BitcoinPriceWrapper bitcoinPriceWrapper = new BitcoinPriceWrapper {
Data = new BitcoinPrice {
Amount = this.testAmount.ToString()
}
};
HttpResponseMessage mockResponse = new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StringContent(new JavaScriptSerializer().Serialize(bitcoinPriceWrapper))
};
mockResponse.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return Task.FromResult(mockResponse);
}
private PeriodicTimer CreateTimer() {
AppSettings appSettings = new AppSettings {
RefreshTimeInSeconds = this.testRefreshRate
};
PeriodicTimer timer = new PeriodicTimer(appSettings);
this.host.AddService(timer);
return timer;
}
}
}
But while System.Threading.Thread.Sleep(60000)
caused the test to pause, it also prevented the background operation from continuing to execute, so the test failed.
How can I make this work?