Files
zszq-trs/UnitTestProject/Modules/SwapModule/MergeComposeScenarioTest.cs
T
hjhan b501e21904 test(swap): 启用MergeCompose场景6/7+SwapFlowEventService录制钩子+RecordingSwapFlowEventService
1. MergeComposeScenarioTest 场景6/7 启用(去掉Ignore):
   此前失败是stale binary(未重新编译),重新build后8场景全部通过。
   - 场景6 单条merge无持仓→DealNoPosition创建交易
   - 场景7 两条merge无持仓→创建交易+平仓

2. SwapFlowEventService 补OnBefore/OnAfterMergePageEvent录制钩子(默认空实现):
   - OnBefore: DB查询完成、合成逻辑执行前触发,可捕获输入快照
   - OnAfter: PersistEvents后触发,捕获输出事件
   生产路径为空方法体,零行为影响。

3. 移植RecordingSwapFlowEventService(借鉴testable分支):
   override OnAfterMergePageEvent录制golden文件,备用录制工具。
   内联GoldenFileModel(简单POCO)。FullRecord_WithPreBookingPositions连库测试自动跳过。

SwapModule 171测试全绿,无回归。
2026-07-03 09:38:05 +08:00

265 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Model;
using static YLErp.Modules.SwapModule.TestableSwapTradeAutoService;
namespace YLErp.Modules.SwapModule
{
[TestClass]
public class MergeComposeScenarioTest
{
private const string UnderlyingCode = "220205.IB";
private const int ClientId = 10;
private static readonly DateTime TradeDate = new DateTime(2025, 4, 24);
#region 场景1:空merge列表 直接返回
[TestMethod]
public void Scenario1_EmptyMergeList_ShouldReturn()
{
var service = CreateService();
service.ExecuteMergeRestModeCompose(new List<swap_flow_merge>(), TradeDate);
Assert.AreEqual(0, service.CreatedTrades.Count, "不应创建任何交易");
}
#endregion
#region 场景2:客户不存在 抛异常
[TestMethod]
[ExpectedException(typeof(ServiceException))]
public void Scenario2_ClientNotFound_ShouldThrow()
{
// 不注入任何 client
var service = CreateService(clients: new Dictionary<int, Client>());
var merges = new List<swap_flow_merge> { CreateMerge() };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
}
#endregion
#region 场景3:客户未设置场外互换权限 抛异常
[TestMethod]
[ExpectedException(typeof(ServiceException))]
public void Scenario3_ClientNoSwapPermission_ShouldThrow()
{
var client = CreateClient(hasSwapPermission: false);
var service = CreateService(clients: new Dictionary<int, Client> { [ClientId] = client });
var merges = new List<swap_flow_merge> { CreateMerge() };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
}
#endregion
#region 场景4:无TRS簿记账户 抛异常
[TestMethod]
[ExpectedException(typeof(ServiceException))]
public void Scenario4_NoEtradingRule_ShouldThrow()
{
var client = CreateClient();
// etradingRuleFactory 返回 null
var service = CreateService(
clients: new Dictionary<int, Client> { [ClientId] = client },
etradingRuleFactory: (side, num) => null
);
var merges = new List<swap_flow_merge> { CreateMerge() };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
}
#endregion
#region 场景5:找不到簿记账户资产单元 抛异常
[TestMethod]
[ExpectedException(typeof(ServiceException))]
public void Scenario5_NoAssetUnit_ShouldThrow()
{
var client = CreateClient();
var service = CreateService(
clients: new Dictionary<int, Client> { [ClientId] = client },
etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"),
assets: new Dictionary<string, AssetUnit>() // 空的,找不到
);
var merges = new List<swap_flow_merge> { CreateMerge() };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
}
#endregion
#region 场景6:单条merge + 无持仓 DealNoPosition 创建一笔交易
[TestMethod]
public void Scenario6_SingleMerge_NoPosition_ShouldCreateOneTrade()
{
var client = CreateClient();
var service = CreateService(
clients: new Dictionary<int, Client> { [ClientId] = client },
etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"),
assets: new Dictionary<string, AssetUnit> { ["TRS_ACCOUNT"] = CreateAssetUnit() },
underlyings: new Dictionary<string, underlying_manager> { [UnderlyingCode] = CreateUnderlying() },
positions: new List<swap_position>()
);
var merges = new List<swap_flow_merge> { CreateMerge(qty: 100000) };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
Assert.AreEqual(1, service.CreatedTrades.Count, "应创建1笔交易");
Assert.AreEqual(0, service.UnwindCalls.Count, "无持仓不应调用平仓");
Assert.IsTrue(service.SaveChangesCount > 0, "应调用SaveChanges");
}
#endregion
#region 场景7:两条merge + 无持仓 DealNoPosition 创建交易+平仓
[TestMethod]
public void Scenario7_TwoMerges_NoPosition_ShouldCreateTradeAndUnwind()
{
var client = CreateClient();
var service = CreateService(
clients: new Dictionary<int, Client> { [ClientId] = client },
etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"),
assets: new Dictionary<string, AssetUnit> { ["TRS_ACCOUNT"] = CreateAssetUnit() },
underlyings: new Dictionary<string, underlying_manager> { [UnderlyingCode] = CreateUnderlying() },
positions: new List<swap_position>()
);
var merges = new List<swap_flow_merge>
{
CreateMerge(bsType: 1, qty: 100000), // 买
CreateMerge(bsType: 2, qty: -50000) // 卖
};
service.ExecuteMergeRestModeCompose(merges, TradeDate);
// 两条流水:先开仓,再平仓(买100000 vs 卖50000 → 平50000 + 剩余开仓50000
Assert.IsTrue(service.UnwindCalls.Count > 0, "有两条流水应触发平仓操作");
}
#endregion
#region 场景8:找不到标的 抛异常
[TestMethod]
[ExpectedException(typeof(ServiceException))]
public void Scenario8_UnderlyingNotFound_ShouldThrow()
{
var client = CreateClient();
var service = CreateService(
clients: new Dictionary<int, Client> { [ClientId] = client },
etradingRuleFactory: (side, num) => CreateEtradingRule("TRS_ACCOUNT"),
assets: new Dictionary<string, AssetUnit> { ["TRS_ACCOUNT"] = CreateAssetUnit() },
underlyings: new Dictionary<string, underlying_manager>() // 空的
);
var merges = new List<swap_flow_merge> { CreateMerge() };
service.ExecuteMergeRestModeCompose(merges, TradeDate);
}
#endregion
#region 辅助方法
private TestableSwapTradeAutoService CreateService(
Dictionary<int, Client> clients = null,
Dictionary<string, AssetUnit> assets = null,
Dictionary<string, underlying_manager> underlyings = null,
List<swap_position> positions = null,
Func<int, string, EtradingRule> etradingRuleFactory = null,
List<trade> trades = null,
List<swap_flow_event> flowEvents = null,
List<trade> validTrades = null)
{
var user = new OptUserInfo(1, "Test", OptUserFrom.UnitTest);
return new TestableSwapTradeAutoService(
user,
trades: trades,
positions: positions ?? new List<swap_position>(),
clients: clients ?? new Dictionary<int, Client>(),
assets: assets ?? new Dictionary<string, AssetUnit>(),
underlyings: underlyings ?? new Dictionary<string, underlying_manager>(),
etradingRuleFactory: etradingRuleFactory,
flowEvents: flowEvents ?? new List<swap_flow_event>(),
validTrades: validTrades ?? new List<trade>()
);
}
private swap_flow_merge CreateMerge(int bsType = 1, decimal qty = 100000, decimal avgPrice = 1.0020m)
{
return new swap_flow_merge
{
OccurTime = TradeDate,
SwapTradeId = 9001,
SwapTradeNo = "TEST-IS-001",
UnderlyingCode = UnderlyingCode,
BsType = bsType,
TradingQty = qty,
TradingAmount = Math.Abs(qty) * avgPrice,
TradingAmountAvg = avgPrice,
TradingAmountFeeAvg = avgPrice,
TradingAmountNetAvg = avgPrice - 0.005m,
TradingAmountNetFeeAvg = avgPrice - 0.005m,
TradingFeePending = 0,
ContractSize = 1,
ClientId = ClientId,
DataState = 1,
FirstFlowTime = DateTime.Now
};
}
private Client CreateClient(bool hasSwapPermission = true)
{
var client = new Client
{
id = ClientId,
Name = "测试客户",
Number = "C001",
BoundSide = BoundSideEnum.南向,
SwapTradeType = 0
};
if (hasSwapPermission)
{
client.DerivativesInvestmentVarieties = ((int)DerivativesInvestmentVarietiesEnum.场外互换).ToString();
}
else
{
client.DerivativesInvestmentVarieties = "";
}
return client;
}
private EtradingRule CreateEtradingRule(string assetAccount)
{
return new EtradingRule
{
AssetAccount_0 = assetAccount,
ClearingAgency_0 = "TEST_CLEARING"
};
}
private AssetUnit CreateAssetUnit()
{
return new AssetUnit
{
Name = "TRS_ACCOUNT",
TraderIds = "1"
};
}
private underlying_manager CreateUnderlying()
{
return new underlying_manager
{
UnderlyingCode = UnderlyingCode,
UnderlyingInstrumentType = "TBonds",
ContractSize = 1
};
}
#endregion
}
}