简体   繁体   English

如何使用 Http 和计时器对 blazor 组件进行单元测试?

[英]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:我创建了一个 Blazor 组件,它定期从 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 BitcoinChallengeBlazorApp.UnitTests.IndexTest.Test1 来源:IndexTest.cs 第 23 行持续时间:51.5 秒

Message: Assert.Equal() Failure ↓ (pos 0) Expected: 8,448.95 Actual: 0,00 € ↑ (pos 0) Stack Trace: IndexTest.Test1() line 34消息:Assert.Equal() 失败↓ (pos 0) 预期:8,448.95 实际:0,00 € ↑ (pos 0) 堆栈跟踪:IndexTest.Test1() 第 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.通过对 Visual Studio 调试器的试验,在我看来,正在发生的事情是被模拟的 Http 客户端和计时器正在“合谋”导致看似总是失败的不稳定结果。

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.正如预期的那样,我可以观察到表达式decimal.Parse(bitcoinPriceWrapper.Data.Amount)多次执行,有时会导致值“8448.947391885”,但有时会导致值 null 或 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.我还没有观察到这种模式,也没有观察到在测试完成之前实际执行了多少次,但我注意到 Timer 从来没有真正等待(老实说,我可能不希望它等待)和测试总是失败。

  • How can I fix this test so amount always gets catches a value?我怎样才能修复这个测试,所以amount总是得到一个值?
  • How can I prove the HttpClient has received multiple requests?如何证明 HttpClient 已收到多个请求?

Thanks.谢谢。

I figured out how to stablize the Http Client mock.我想出了如何稳定 Http 客户端模拟。

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:利用这一点,我修改了 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);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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