0

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?

Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • Make the method `async` and include `await Task.Delay(60000)` to do the pause – Flydog57 Jun 23 '20 at 23:06
  • @Flydog57, Cheers for the suggestion, but this didn't seem to work any better than `Sleep`.... I'm still only getting two requests after a long delay.. – Brian Kessler Jun 23 '20 at 23:43
  • 1
    For a **unit-test** you have to mock external dependencies (here `System.Threading.Timer`) and then you can perform this test within milliseconds. And even more you have to inject external dependecies to have a unit-testable class – Sir Rufo Jun 24 '20 at 00:21
  • @SirRufo, That makes sense.... I will try it tomorrow. Thanks. :-) – Brian Kessler Jun 24 '20 at 00:27
  • @SirRufo, actually, by the morning light it didn't make so much sense as then I need to write a complicated mock which can keep track of the time and execute the incoming function or at least create a decorating for the existing function so I can spy on it, but I don't know how I might speed it up..... – Brian Kessler Jun 24 '20 at 05:15
  • What exactly do you want to test? You don't have to debug the timer. Checking if it all works is more of an integration test. – H H Jun 24 '20 at 05:19
  • I want to test to test that if I have the refresh value set to 5 and I leave it running for one minute, fetch executes 12 times, but if I have the refresh value set to 10 and leave it running for one mnute it only executes 6 times. I am not testing the timer: I am testing that the component is correctly making use of the timer. – Brian Kessler Jun 24 '20 at 05:23

0 Answers0