Nano Hash - криптовалюты, майнинг, программирование

Как я могу выполнить модульное тестирование компонента Blazor с помощью Http и таймера?

Я создал компонент 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;
    }
}

Я хотел бы создать тест, чтобы доказать, что это работает должным образом, и я создал этот тест:

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

    }
}

Однако этот тест не проходит с:

BitcoinChalllengeBlazorApp.UnitTests.IndexTest.Test1 Источник: IndexTest.cs, строка 23 Продолжительность: 51,5 сек.

Сообщение: Assert.Equal () Failure ↓ (pos 0) Ожидается: 8,448.95 Actual: 0,00 € ↑ (pos 0) Трассировка стека: IndexTest.Test1 () строка 34

Поэкспериментировав с отладчиком Visual Studio, мне кажется, что происходит сговор с поддельным Http-клиентом и таймером для получения нестабильного результата, который, по-видимому, неизменно дает сбой.

Как и ожидалось, я могу наблюдать, как выражение decimal.Parse(bitcoinPriceWrapper.Data.Amount) выполняется несколько раз, иногда приводя к значению 8448.947391885, но иногда к значению null или 0.

Я еще не наблюдал закономерности для этого, а также того, сколько раз это действительно выполняется до завершения теста, но я заметил, что таймер на самом деле никогда не ждет (честно говоря, я, вероятно, не хотел бы этого) и тест всегда терпит неудачу.

  • Как я могу исправить этот тест, чтобы amount всегда получал значение?
  • Как я могу доказать, что HttpClient получил несколько запросов?

Спасибо.


Ответы:


1

Я понял, как стабилизировать макет Http Client.

Сначала я создал новый обработчик:

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

Используя это, я изменил создание 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);
        }

Наконец, я изменил свои утверждения:

            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);
23.06.2020
Новые материалы

Кластеризация: более глубокий взгляд
Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

Как написать эффективное резюме
Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

Частный метод Python: улучшение инкапсуляции и безопасности
Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

Как я автоматизирую тестирование с помощью Jest
Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..