xUnit测试ET框架项目
我之前搞java的单元测试,测试类是在一个项目中而且测试,测试也不需要借助许多初始化数据。
第一次使用xUnit而且,测试ET项目,ET项目有许多初始化【可能游戏都是这样的】,由于服务端分为了许多项目,测试也是单独一个项目,而且大多测试方法都需要一些游戏初始化配置数据,这和之前不一样,不过大致都一样的,都是三个要素:准备、执行、断言,ET项目测试主要是准备阶段比较麻烦。
首先:测试方法可以使用,要待测试项目的类及方法
- 创建xUnit测试项目,
- 依赖要测试的项目,这里依赖ET框架的服务端Server项目
刚开始可能读取配置文件解析失败【我碰到了这个问题】,xUnit单元测试读取配置文件失败问题解决:
因为新建的测试项目xUnit使用的.Net Core版本是3.1,而引入的ET项目是2.1,对应C#版本是7.3的
其次:开始测试前准备测试数据
xUnit测试ET框架项目测试环境准备:xUnit测试方法前会执行构造,所以测试环境及初始化数据在构造中准备
- 测试类的构造用于初始化测试环境,准备测试方法所需测试数据,即执行ET的main()方法。
- main()方法中创建测试登录的用户玩家,并在玩家身上挂载相应组件,如果数据库没有则创建并保存到数据库【因为有的初始化方法StartSystem等是从数据库查询的玩家从而获取组件数据】,并将玩家组件添加到Session的组件SessionUserComponent上供测试方法中从session获取创建的玩家数据,
- 如果有将查询出来用户并挂到session的组件SessionUserComponent上供测试方法中从session获取查询出来的玩家数据,从而获取用户组件数据。
- 创建后执行100次 EventSystem.Update();用于执行初始化AwakeSystem和StartSystem中的初始化方法,main()一般构造中用于测试方法前初始化,所以不必太多次循环造成不必要的测试时间,但太少又可能太快任务还未加到任务队列中【例如循环5次就不会执行StartSysTem的Start()方法】,100差不多
- 然后初始化测试类中的组件字段,一般是要测试的组件从userInfo获取,或Game.Scene获取。
- 测试异步方法,官方说推荐使用async ETTask而不是用async void,测试需要异步方法,异步方法调用照样使用await,亲测,在ET框架下,async void、async ETVoid、async ETTask,都可以使用异步保存,然后向下走,官方异步单元测试:https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2014/november/async-programming-unit-testing-asynchronous-code
玩家背包组件测试:
准备数据:将背包组件存到一个用户身上。挂载后运行EventSystem.update事件,才会执行StartSystem。
测试异步方法一直运行不结束情况:搞了我好久最后终于让我找到了解决办法
如果遇到测试方法一直运行不结束,可能是由于异步await方法未请求到结果,可在将单线程注释掉即可解决
//注释掉就会多线程 //SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);
【期间我试了好多方式但是一直偶尔不结束偶尔结束 不稳定,】例如:
- await方法方法未请求到结果,然后测试方法就结束的猜测,解决办法是在await后再次循环运行EventSystem.update,但这种方式还是不稳定
- await之后的代码使用了await之前的变量,导致await后的代码不能执行,因为await之后打印的输出都未打印
- 等等
- ……许多其他错误的猜测,这虽然多走了许多弯路,但是经验不就是这么来的嘛,现在不走可能以后也会走,和人生一样,我以前不管学习、工作、还是生活上都走了不少弯路,让人痛悔不已,说多了都是泪,扯远了,
- 为了大家遇到这种问题少浪费时间在这上面,我把我的解决方法写到这里,真正原因还未清楚【此解决办法暂时用着还可以】
最后:执行测试和断言
我的测试xUnit代码:
测试类:
public class PlayerItemsComponentTest { PlayerItemsComponent self; ItemsManagerComponent itemsManagerComponent;//用于 public ITestOutputHelper Output; public PlayerItemsComponentTest(ITestOutputHelper tempOutput) { AppProgramHelper.Main2(new string[] { "--appId=1", "--appType=AllServer", "--config=../Config/StartConfig/LocalAllServer.txt" }); LobbyManagerComponent lobbyManagerComponent = Game.Scene.GetComponent<LobbyManagerComponent>(); itemsManagerComponent = lobbyManagerComponent.GetComponent<ItemsManagerComponent>(); Output = tempOutput; self = AppProgramHelper.GetSession().GetComponent<SessionUserComponent>()?.userInfo?.GetComponent<PlayerItemsComponent>(); //System.Diagnostics.Stopwatch stopwatch = new Stopwatch();//Stopwatch //stopwatch.Start(); //耗时方法...() //TimeSpan timespan = stopwatch.Elapsed; // 获取当前实例测量得出的总时间 //double milliseconds = timespan.TotalMilliseconds; // 总毫秒数 //Output.WriteLine($"构造耗时:{milliseconds}毫秒"); } [Fact] //只保存一次到数据库,数据库中不存在userInfo 才运行此测试方法。 public async ETTask PrepareTestDataAsync() { try { self.Add(2); self.Add(3); self.Add(4); self.Add(5); self.Add(6); self.Add(7); self.Add(9); for (int i = 0; i < 30; i++) { self.Add(11); } self.Add(12); Output.WriteLine("测试await前,会不会打印输出"); Session session = AppProgramHelper.GetSession(); ETModel.UserInfo userInfo = session.GetComponent<SessionUserComponent>()?.userInfo; await AppProgramHelper.SaveEntityAsync(userInfo); await AppProgramHelper.SaveEntityAsync(session); Output.WriteLine("测试await后,会不会打印输出");//也可以打印出来,断点执行更容易通过,运行会一直测试不结束。 } catch (Exception e) { Output.WriteLine(e.Message + e.StackTrace); } } …… [Fact] public void ConvertTest() { ItemInfo itemInfo = itemsManagerComponent.getItemById(11); ItemInfo ConvertedItem = itemsManagerComponent.getItemById(itemInfo.CompositeObject); int oldCount = self.GetItemCountById(ConvertedItem.Id); self.Convert(itemInfo); ETModel.UserInfo userInfo = AppProgramHelper.GetSession().GetComponent<SessionUserComponent>()?.userInfo; if (userInfo != null) { AppProgramHelper.SaveEntityAsync(userInfo); } Assert.Equal(oldCount+1, self.GetItemCountById(ConvertedItem.Id)); } }
测试项目构造调用的初始化【准备测试数据】:Main()
internal static class AppProgramHelper { public static void Main2(string[] args) { //TaskStatus // 异步方法全部会回掉到主线程,即Task.Run()开启的任务,和Task.Delay()开启的任务。 //如果不设置同步上下文,你会发现打印出来当前线程就不是主线程了,设置后,线程id一致全是1。 //SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance); try { Game.EventSystem.Add(DLLType.Model, typeof(Game).Assembly); Game.EventSystem.Add(DLLType.Hotfix, DllHelper.GetHotfixAssembly()); //命令行参数。配置的有四个服务器组件的ip+port信息和appId等信息。 Options options = Game.Scene.AddComponent<OptionComponent, string[]>(args).Options; //StartConfig属于Entity里边有其他属性哦ID跟components,配置的有 outerConfig,innerConfig,clientConfig,HttpConfig 组件信息。 //初始化StartConfigComponent组件中的各种服务器配置的是AllServer:DBConfig,RealmConfig,LocationConfig,MapConfigs,GateConfigs, //启动配置 StartConfig startConfig = Game.Scene.AddComponent<StartConfigComponent, string, int>(options.Config, options.AppId).StartConfig; StartConfigComponent startConfigComponent = Game.Scene.GetComponent<StartConfigComponent>(); if (!options.AppType.Is(startConfig.AppType)) { Log.Error("命令行参数apptype与配置不一致"); return; } IdGenerater.AppId = options.AppId; LogManager.Configuration.Variables["appType"] = $"{startConfig.AppType}"; LogManager.Configuration.Variables["appId"] = $"{startConfig.AppId}"; LogManager.Configuration.Variables["appTypeFormat"] = $"{startConfig.AppType,-8}"; LogManager.Configuration.Variables["appIdFormat"] = $"{startConfig.AppId:0000}"; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"游龙方舟ServerStart........................ AppId:{startConfig.AppId} AppType:{startConfig.AppType}"); Game.Scene.AddComponent<TimerComponent>(); Game.Scene.AddComponent<OpcodeTypeComponent>(); Game.Scene.AddComponent<MessageDispatcherComponent>(); // 根据不同的AppType添加不同的组件 OuterConfig outerConfig = startConfig.GetComponent<OuterConfig>(); InnerConfig innerConfig = startConfig.GetComponent<InnerConfig>(); ClientConfig clientConfig = startConfig.GetComponent<ClientConfig>(); switch (startConfig.AppType) { case AppType.Manager: Game.Scene.AddComponent<AppManagerComponent>(); Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); Game.Scene.AddComponent<NetOuterComponent, string>(outerConfig.Address); break; case AppType.Realm: Game.Scene.AddComponent<MailboxDispatcherComponent>(); Game.Scene.AddComponent<ActorMessageDispatcherComponent>(); Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); Game.Scene.AddComponent<NetOuterComponent, string>(outerConfig.Address); Game.Scene.AddComponent<LocationProxyComponent>(); Game.Scene.AddComponent<RealmGateAddressComponent>(); Game.Scene.AddComponent<DBProxyComponent>(); break; case AppType.DB: Game.Scene.AddComponent<DBComponent>(); Game.Scene.AddComponent<NetInnerComponent, IPEndPoint>(innerConfig.IPEndPoint); break; case AppType.Gate: Game.Scene.AddComponent<PlayerComponent>(); Game.Scene.AddComponent<MailboxDispatcherComponent>(); Game.Scene.AddComponent<ActorMessageDispatcherComponent>(); Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); Game.Scene.AddComponent<NetOuterComponent, string>(outerConfig.Address); Game.Scene.AddComponent<LocationProxyComponent>(); Game.Scene.AddComponent<ActorMessageSenderComponent>(); Game.Scene.AddComponent<ActorLocationSenderComponent>(); Game.Scene.AddComponent<GateSessionKeyComponent>(); break; case AppType.Location: Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); Game.Scene.AddComponent<LocationComponent>(); break; case AppType.Map: Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); Game.Scene.AddComponent<UnitComponent>();//多个Unit。 // 访问location server的组件 Game.Scene.AddComponent<LocationProxyComponent>(); Game.Scene.AddComponent<ActorMessageSenderComponent>(); Game.Scene.AddComponent<ActorLocationSenderComponent>(); Game.Scene.AddComponent<MailboxDispatcherComponent>(); Game.Scene.AddComponent<ActorMessageDispatcherComponent>(); Game.Scene.AddComponent<PathfindingComponent>(); break; case AppType.AllServer: // 发送普通actor消息 Game.Scene.AddComponent<ActorMessageSenderComponent>(); // 发送location actor消息 Game.Scene.AddComponent<ActorLocationSenderComponent>(); Game.Scene.AddComponent<DBComponent>(); Game.Scene.AddComponent<DBProxyComponent>(); // location server需要的组件 Game.Scene.AddComponent<LocationComponent>(); // 访问location server的组件 Game.Scene.AddComponent<LocationProxyComponent>(); // 这两个组件是处理actor消息使用的 Game.Scene.AddComponent<MailboxDispatcherComponent>(); Game.Scene.AddComponent<ActorMessageDispatcherComponent>(); // 内网消息组件 Game.Scene.AddComponent<NetInnerComponent, string>(innerConfig.Address); // 外网消息组件 Game.Scene.AddComponent<NetOuterComponent, string>(outerConfig.Address); // manager server组件,用来管理其它进程使用 Game.Scene.AddComponent<AppManagerComponent>(); Game.Scene.AddComponent<RealmGateAddressComponent>(); Game.Scene.AddComponent<GateSessionKeyComponent>(); // 配置管理 Game.Scene.AddComponent<ConfigComponent>(); //控制台组件 Game.Scene.AddComponent<ConsoleComponent>(); /**************游龙方舟项目***************/ // 添加计时器组件 Game.Scene.AddComponent<TimeComponent>(); //验证服务组件 Game.Scene.AddComponent<EducationalGateSessionKeyCpt>(); //用户管理组件 Game.Scene.AddComponent<PlayerManagerComponent>(); //玩家在线组件 Game.Scene.AddComponent<OnlineComponent>(); Game.Scene.RemoveComponent<OnlineComponent>(); //大厅管理组件 Game.Scene.AddComponent<LobbyManagerComponent>(); //数据库定时存储数据组件 Game.Scene.AddComponent<DataBaseSaveComponet>(); break; case AppType.Benchmark: Game.Scene.AddComponent<NetOuterComponent>(); Game.Scene.AddComponent<BenchmarkComponent, string>(clientConfig.Address); break; case AppType.BenchmarkWebsocketServer: Game.Scene.AddComponent<NetOuterComponent, string>(outerConfig.Address); break; case AppType.BenchmarkWebsocketClient: Game.Scene.AddComponent<NetOuterComponent>(); Game.Scene.AddComponent<WebSocketBenchmarkComponent, string>(clientConfig.Address); break; default: throw new Exception($"命令行参数没有设置正确的AppType: {startConfig.AppType}"); } //Console.WriteLine("Game.EventSystem:" + Game.EventSystem); //Test.testCallMsg();//测试发送消息 //Test.TestETTaskAsync(); //Log.Info("测试ETasync/await完成"); //TestBytesUtils.TestBytes(); CreateUserInfo(3);//id=2的用户登录游戏。3:我的时装测试 //先循环一次才会执行StartSystem事件。循环100次,把之前需要运行的线程全部执行 int i = 0; while (i < 100)//此方法一般构造中用于初始化,所以不必太多次循环,但太少又获取不到新添加的任务队列,100差不多 {//循环一次就相当于一帧。 i++; try { Thread.Sleep(1); OneThreadSynchronizationContext.Instance.Update(); Game.EventSystem.Update(); } catch (Exception e) { Log.Error(e); } } //TestBytesUtils.testMongoDB(); //TestBytesUtils.testAddPlayerItems(); //TestBytesUtils.testDictionaryOrderBy(); //TestBytesUtils.testHashSet(); //TestBytesUtils.testExcept(); } catch (Exception e) { Log.Error(e); } } public static void WhileTrue(int times) { int i = 0; while (i < times) {//循环一次就相当于一帧。 i++; try { Thread.Sleep(1); OneThreadSynchronizationContext.Instance.Update(); Game.EventSystem.Update(); } catch (Exception e) { Log.Error(e); } } } //创建登录游戏的用户,并保存到session的组件SessionUserComponent上。 public static async ETTask CreateUserInfo(long userId) { Session realmSession = GetSession(); ETModel.DBProxyComponent dbProxy = Game.Scene.GetComponent<ETModel.DBProxyComponent>(); ETModel.UserInfo userInfo = await dbProxy.Query<ETModel.UserInfo>(userId); if (userInfo == null) { userInfo = UserInfoFactory.Create(userId, realmSession);//这样创建,可以从组件SessionUserComponent【挂在session上】获取【可能还没保存进数据库】。 } else { userInfo.Account="二龙戏珠"; realmSession.AddComponent<SessionUserComponent>().userInfo = userInfo; realmSession.GetComponent<SessionUserComponent>().sessionId = realmSession.Id; } } public static Session GetSession() { StartConfigComponent config = Game.Scene.GetComponent<StartConfigComponent>(); IPEndPoint realmIPEndPoint = config.RealmConfig.GetComponent<InnerConfig>().IPEndPoint; Session realmSession = Game.Scene.GetComponent<NetInnerComponent>().Get(realmIPEndPoint); return realmSession; } public static async ETTask SaveEntityAsync(ComponentWithId componentWithId) { ETModel.DBProxyComponent dbProxy = Game.Scene.GetComponent<ETModel.DBProxyComponent>(); await dbProxy.Save(componentWithId); } }
本文作者:好Wu赖
本文链接:https://www.cnblogs.com/erlongxizhu-03/p/12891254.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现