简体   繁体   中英

How can I unit test a blazor component with Http and a Timer?

I've created a Blazor component that periodically collects a price from an api:

@page "/"
@using System.Globalization;
@using System.Net.Http;
@using BitcoinChallenge.Entities;
@using Newtonsoft.Json;
@inject BitcoinChallengeSettings BitcoinSettings;
@inject HttpClient Http;
@inject IJSRuntime JSRuntime;

<div class="card" style="width: 20rem;">
    <h5 class="card-title">Current price of one bitcoin</h5>
    <p>@FormattedPrice</p>
</div>

@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();
            this.startPeriodicRefresh();
        }
        catch (Exception e) {
            await JSRuntime.InvokeAsync<object>("alert", e.ToString());
        }
    }

    private void startPeriodicRefresh() {
        TimeSpan startTimeSpan = TimeSpan.Zero;
        TimeSpan periodTimeSpan = TimeSpan.FromSeconds(BitcoinSettings.RefreshTimeInSeconds);

        var timer = new System.Threading.Timer(async (e) => {
            this.Price = await this.fetchPrice();
        }, null, startTimeSpan, periodTimeSpan);
    }

    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);
        decimal amount = decimal.Parse(bitcoinPriceWrapper.Data.Amount);
        return amount;
    }
}

I'd like to create a test to prove this is working as expected and I've created this test:

using BitcoinChallenge.Entities;
using BitcoinChallengeBlazorApp;
using Microsoft.AspNetCore.Components.Testing;
using Microsoft.JSInterop;
using Moq;
using Nancy.Json;
using RichardSzalay.MockHttp;
using System;
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 Test1() {
            // Arrange
            _ = this.SetMockRuntime();
            _ = this.CreateMockHttpClientAsync();
            _ = this.CreateSettings();


            // Act
            RenderedComponent<Index> componentUnderTest = this.host.AddComponent<Index>();

            // Assert            
            Assert.Equal($"{this.testAmount:n2}", componentUnderTest.Find("p").InnerText);
        }

        public Mock<IJSRuntime> SetMockRuntime() {
            Mock<IJSRuntime> jsRuntimeMock = new Mock<IJSRuntime>();
            this.host.AddService(jsRuntimeMock.Object);
            return jsRuntimeMock;
        }

        public HttpClient CreateMockHttpClientAsync() {
            MockHttpMessageHandler mockHttpMessageHandler = new MockHttpMessageHandler();
            mockHttpMessageHandler.When("https://api.coinbase.com/v2/prices/spot?currency=EUR")
                .Respond(this.CreateMockResponse);

            HttpClient httpClient = new HttpClient(mockHttpMessageHandler);
            this.host.AddService(httpClient);
            return httpClient;
        }

        private Task<HttpResponseMessage> CreateMockResponse() {
            BitcoinPriceWrapper bitcoinPriceWrapper = new BitcoinPriceWrapper();
            bitcoinPriceWrapper.Data = new BitcoinPrice();
            bitcoinPriceWrapper.Data.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 BitcoinChallengeSettings CreateSettings() {
            AppSettings appSettings = new AppSettings {
                RefreshTimeInSeconds = this.testRefreshRate
            };
            BitcoinChallengeSettings bitcoinChallengeSettings = new BitcoinChallengeSettings(appSettings);
            this.host.AddService(bitcoinChallengeSettings);
            return bitcoinChallengeSettings;
        }

    }
}

However, this test fails with:

BitcoinChalllengeBlazorApp.UnitTests.IndexTest.Test1 Source: IndexTest.cs line 23 Duration: 51.5 sec

Message: Assert.Equal() Failure ↓ (pos 0) Expected: 8,448.95 Actual: 0,00 € ↑ (pos 0) Stack Trace: IndexTest.Test1() line 34

From experimenting with Visual Studio's debugger, it seems to me that what's happening is the mocked Http Client and Timer are "conspiring" for an unstable result which seemingly invariably fails.

I can observe, as expected, the expression decimal.Parse(bitcoinPriceWrapper.Data.Amount) is executed multiple times, sometimes resulting in the value "8448.947391885", but sometimes resulting in a value of null or 0.

I haven't observed the pattern for this yet, nor how many times this actually executes before the test finishes, but I have noticed that the Timer never actually waits (to be honest, I probably wouldn't want it to) and the test always fails.

  • How can I fix this test so amount always gets catches a value?
  • How can I prove the HttpClient has received multiple requests?

Thanks.

I figured out how to stablize the Http Client mock.

First, I created a new Handler:

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace BitcoinChalllengeBlazorApp.UnitTests {
    public class MockBitcoinMessageHandler : HttpMessageHandler {
        public static int requestCount = 0;

        private readonly Func<Task<HttpResponseMessage>> createMockResponse;

        public MockBitcoinMessageHandler(Func<Task<HttpResponseMessage>> createMockResponse) {
            this.createMockResponse = createMockResponse;
        }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, 
            CancellationToken cancellationToken
            ) {
            requestCount++;
            return this.createMockResponse();
        }
    }
}

Leveraging this, I modified creation of the HttpClient:

        private MockBitcoinMessageHandler CreateMockHttpClientAsync() {
            MockBitcoinMessageHandler mockHttpMessageHandler = new MockBitcoinMessageHandler(this.CreateMockResponse);
            HttpClient httpClient = new HttpClient(mockHttpMessageHandler);
            this.host.AddService(httpClient);
            return mockHttpMessageHandler;
        }

        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);
        }

Finally, I modified my assertions:

            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);

            Assert.Equal(2, MockBitcoinMessageHandler.requestCount);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM