AssetManager.API/AssetManager.Tests/Services/MarketDataServiceTests.cs
niannian zheng 2a6512ff48 feat(市场数据): 添加Yahoo财经服务并设为优先数据源
- 新增YahooMarketService实现股票实时价格和历史数据获取
- 更新MarketDataService优先使用Yahoo服务,腾讯财经降级为第二选择
- 添加YahooQuotesApi依赖并更新相关NuGet包版本
- 补充Yahoo服务测试用例
2026-03-17 12:06:47 +08:00

229 lines
7.5 KiB
C#

using AssetManager.Data.Repositories;
using AssetManager.Infrastructure.Services;
using AssetManager.Models.DTOs;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace AssetManager.Tests.Services;
/// <summary>
/// MarketDataService 单元测试
/// </summary>
public class MarketDataServiceTests
{
private readonly Mock<ITencentMarketService> _tencentMock;
private readonly Mock<ITiingoMarketService> _tiingoMock;
private readonly Mock<IYahooMarketService> _yahooService;
private readonly Mock<IOkxMarketService> _okxMock;
private readonly Mock<IMarketDataRepository> _repoMock;
private readonly Mock<ILogger<MarketDataService>> _loggerMock;
private readonly MarketDataService _service;
public MarketDataServiceTests()
{
_tencentMock = new Mock<ITencentMarketService>();
_tiingoMock = new Mock<ITiingoMarketService>();
_yahooService = new Mock<IYahooMarketService>();
_okxMock = new Mock<IOkxMarketService>();
_repoMock = new Mock<IMarketDataRepository>();
_loggerMock = new Mock<ILogger<MarketDataService>>();
_service = new MarketDataService(
_loggerMock.Object,
_tencentMock.Object,
_yahooService.Object,
_tiingoMock.Object,
_okxMock.Object,
_repoMock.Object
);
}
#region GetPriceAsync Tests
[Fact]
public async Task GetPriceAsync_Crypto_ShouldCallOkxService()
{
// Arrange
var symbol = "BTC-USDT";
var expectedPrice = 50000m;
_repoMock.Setup(x => x.GetPriceCacheAsync(symbol, "Crypto"))
.ReturnsAsync((AssetManager.Data.MarketPriceCache?)null);
_okxMock.Setup(x => x.GetCryptoPriceAsync(symbol))
.ReturnsAsync(new MarketPriceResponse
{
Symbol = symbol,
Price = expectedPrice,
PreviousClose = 49000m,
AssetType = "Crypto"
});
_repoMock.Setup(x => x.SavePriceCacheAsync(It.IsAny<AssetManager.Data.MarketPriceCache>()))
.ReturnsAsync(true);
// Act
var result = await _service.GetPriceAsync(symbol, "Crypto");
// Assert
result.Should().NotBeNull();
result.Price.Should().Be(expectedPrice);
_okxMock.Verify(x => x.GetCryptoPriceAsync(symbol), Times.Once);
_tencentMock.Verify(x => x.GetStockPriceAsync(It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task GetPriceAsync_Stock_ShouldCallTencentFirst()
{
// Arrange
var symbol = "AAPL";
var expectedPrice = 150m;
_repoMock.Setup(x => x.GetPriceCacheAsync(symbol, "Stock"))
.ReturnsAsync((AssetManager.Data.MarketPriceCache?)null);
_tencentMock.Setup(x => x.GetStockPriceAsync(symbol))
.ReturnsAsync(new MarketPriceResponse
{
Symbol = symbol,
Price = expectedPrice,
PreviousClose = 148m,
AssetType = "Stock"
});
_repoMock.Setup(x => x.SavePriceCacheAsync(It.IsAny<AssetManager.Data.MarketPriceCache>()))
.ReturnsAsync(true);
// Act
var result = await _service.GetPriceAsync(symbol, "Stock");
// Assert
result.Should().NotBeNull();
result.Price.Should().Be(expectedPrice);
_tencentMock.Verify(x => x.GetStockPriceAsync(symbol), Times.Once);
_tiingoMock.Verify(x => x.GetStockPriceAsync(It.IsAny<string>()), Times.Never);
}
[Fact]
public async Task GetPriceAsync_TencentFails_ShouldFallbackToTiingo()
{
// Arrange
var symbol = "AAPL";
_repoMock.Setup(x => x.GetPriceCacheAsync(symbol, "Stock"))
.ReturnsAsync((AssetManager.Data.MarketPriceCache?)null);
_tencentMock.Setup(x => x.GetStockPriceAsync(symbol))
.ThrowsAsync(new Exception("Tencent API error"));
_tiingoMock.Setup(x => x.GetStockPriceAsync(symbol))
.ReturnsAsync(new MarketPriceResponse
{
Symbol = symbol,
Price = 150m,
PreviousClose = 148m,
AssetType = "Stock"
});
_repoMock.Setup(x => x.SavePriceCacheAsync(It.IsAny<AssetManager.Data.MarketPriceCache>()))
.ReturnsAsync(true);
// Act
var result = await _service.GetPriceAsync(symbol, "Stock");
// Assert
result.Should().NotBeNull();
result.Price.Should().Be(150m);
_tencentMock.Verify(x => x.GetStockPriceAsync(symbol), Times.Once);
_tiingoMock.Verify(x => x.GetStockPriceAsync(symbol), Times.Once);
}
[Fact]
public async Task GetPriceAsync_CacheHit_ShouldNotCallExternalApi()
{
// Arrange
var symbol = "AAPL";
var cachedPrice = new AssetManager.Data.MarketPriceCache
{
Symbol = symbol,
AssetType = "Stock",
Price = 150m,
PreviousClose = 148m,
ExpiredAt = DateTime.Now.AddMinutes(5)
};
_repoMock.Setup(x => x.GetPriceCacheAsync(symbol, "Stock"))
.ReturnsAsync(cachedPrice);
// Act
var result = await _service.GetPriceAsync(symbol, "Stock");
// Assert
result.Should().NotBeNull();
result.Price.Should().Be(150m);
_tencentMock.Verify(x => x.GetStockPriceAsync(It.IsAny<string>()), Times.Never);
_tiingoMock.Verify(x => x.GetStockPriceAsync(It.IsAny<string>()), Times.Never);
}
#endregion
#region GetHistoricalDataAsync Tests
[Fact]
public async Task GetHistoricalDataAsync_Crypto_ShouldCallOkxService()
{
// Arrange
var symbol = "BTC-USDT";
var timeframe = "1d";
var limit = 30;
_repoMock.Setup(x => x.GetKlineCacheAsync(symbol, "Crypto", timeframe, limit))
.ReturnsAsync(new List<AssetManager.Data.MarketKlineCache>());
_okxMock.Setup(x => x.GetCryptoHistoricalDataAsync(symbol, timeframe, limit))
.ReturnsAsync(new List<MarketDataResponse>
{
new() { Symbol = symbol, Close = 50000m, Timestamp = DateTime.Today }
});
_repoMock.Setup(x => x.SaveKlineCacheBatchAsync(It.IsAny<List<AssetManager.Data.MarketKlineCache>>()))
.ReturnsAsync(true);
// Act
var result = await _service.GetHistoricalDataAsync(symbol, "Crypto", timeframe, limit);
// Assert
result.Should().NotBeEmpty();
_okxMock.Verify(x => x.GetCryptoHistoricalDataAsync(symbol, timeframe, limit), Times.Once);
}
[Fact]
public async Task GetHistoricalDataAsync_CacheSufficient_ShouldReturnCached()
{
// Arrange
var symbol = "AAPL";
var timeframe = "1d";
var limit = 2;
var cachedData = new List<AssetManager.Data.MarketKlineCache>
{
new() { Symbol = symbol, Close = 150m, Timestamp = DateTime.Today.AddDays(-1) },
new() { Symbol = symbol, Close = 152m, Timestamp = DateTime.Today }
};
_repoMock.Setup(x => x.GetKlineCacheAsync(symbol, "Stock", timeframe, limit))
.ReturnsAsync(cachedData);
// Act
var result = await _service.GetHistoricalDataAsync(symbol, "Stock", timeframe, limit);
// Assert
result.Should().HaveCount(2);
_tencentMock.Verify(x => x.GetStockHistoricalDataAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()), Times.Never);
}
#endregion
}