From 4b490d79f41463a88c91db0b90023bf8d1b28fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=98=95=EC=9D=B8?= Date: Sat, 7 Jun 2025 01:53:51 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Scripts/UVC/Data/DataArray.cs | 52 +- Assets/Scripts/UVC/Data/DataMapper.cs | 39 +- Assets/Scripts/UVC/Data/DataMask.cs | 26 + Assets/Scripts/UVC/Data/DataMask.cs.meta | 2 + Assets/Scripts/UVC/Data/DataObject.cs | 23 +- .../Scripts/UVC/Tests/Data/DataMapperTests.cs | 92 +++- .../UVC/Tests/Data/HttpPipeLineTests.cs | 490 ++++++++++++++++++ .../UVC/Tests/Data/HttpPipeLineTests.cs.meta | 2 + Assets/Scripts/UVC/Tests/Tester.cs | 3 +- 9 files changed, 671 insertions(+), 58 deletions(-) create mode 100644 Assets/Scripts/UVC/Data/DataMask.cs create mode 100644 Assets/Scripts/UVC/Data/DataMask.cs.meta create mode 100644 Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs create mode 100644 Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta diff --git a/Assets/Scripts/UVC/Data/DataArray.cs b/Assets/Scripts/UVC/Data/DataArray.cs index 0e481403..81bf56b5 100644 --- a/Assets/Scripts/UVC/Data/DataArray.cs +++ b/Assets/Scripts/UVC/Data/DataArray.cs @@ -155,26 +155,50 @@ namespace UVC.Data removedList.Clear(); modifiedList.Clear(); - // 현재 DataArray와 비교하여 변경된 항목을 추적 - for (int i = 0; i < Math.Max(this.Count, otherArray.Count); i++) + // Id 기준으로 객체들을 비교하기 위한 사전 생성 + var thisDict = this.ToDictionary(item => item.Id, item => item); + var otherDict = otherArray.ToDictionary(item => item.Id, item => item); + + // 제거된 항목 확인 (현재 배열에는 있지만 other에는 없는 항목) + foreach (var id in thisDict.Keys.Where(id => !otherDict.ContainsKey(id))) { - if (i < this.Count && i < otherArray.Count) + removedList.Add(thisDict[id]); + } + + // 추가된 항목 확인 (other에는 있지만 현재 배열에는 없는 항목) + foreach (var id in otherDict.Keys.Where(id => !thisDict.ContainsKey(id))) + { + addedList.Add(otherDict[id]); + } + + // 수정된 항목 확인 (양쪽 모두에 있지만 내용이 다른 항목) + foreach (var id in thisDict.Keys.Where(id => otherDict.ContainsKey(id))) + { + var thisItem = thisDict[id]; + var otherItem = otherDict[id]; + + if (!thisItem.ToString().Equals(otherItem.ToString())) { - if (!this[i].ToString().Equals(otherArray[i].ToString())) - { - modifiedList.Add(this[i]); - this[i].UpdateDifferent(otherArray[i]); - } + modifiedList.Add(thisItem); + thisItem.UpdateDifferent(otherItem); } - else if (i < this.Count) + } + + // 실제 컬렉션 업데이트 + // 현재 배열에서 제거된 항목들을 제거 + for (int i = this.Count - 1; i >= 0; i--) + { + if (removedList.Contains(this[i])) { - removedList.Add(this[i]); - } - else if (i < otherArray.Count) - { - addedList.Add(otherArray[i]); + this.RemoveAt(i); } } + + // 추가된 항목들을 현재 배열에 추가 + foreach (var item in addedList) + { + this.Add(item); + } } } diff --git a/Assets/Scripts/UVC/Data/DataMapper.cs b/Assets/Scripts/UVC/Data/DataMapper.cs index 2778d38b..92f224b7 100644 --- a/Assets/Scripts/UVC/Data/DataMapper.cs +++ b/Assets/Scripts/UVC/Data/DataMapper.cs @@ -23,11 +23,12 @@ namespace UVC.Data /// }"); /// /// // Mask 객체 (타입 지정용) - /// var maskJson = JObject.Parse(@"{ + /// var maskJson = DataMask.Parse(@"{ /// ""name"": """", /// ""age"": 0, /// ""isActive"": false /// }"); + /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// DataObject result = mapper.Map(sourceJson); @@ -42,7 +43,7 @@ namespace UVC.Data private int maxRecursionDepth = 10; private int currentDepth = 0; - private JObject mask; + private DataMask mask; /// /// DataMapper 클래스의 새 인스턴스를 초기화합니다. @@ -52,7 +53,7 @@ namespace UVC.Data /// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만, /// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다. /// - public DataMapper(JObject mask) + public DataMapper(DataMask mask) { this.mask = mask; } @@ -93,10 +94,11 @@ namespace UVC.Data /// { ""name"": ""이영희"", ""age"": 25 } /// ]"); /// - /// var maskJson = JObject.Parse(@"{ + /// var maskJson = DataMask.Parse(@"{ /// ""name"": """", /// ""age"": 0 /// }"); + /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// DataArray result = mapper.Map(sourceArray); @@ -147,7 +149,7 @@ namespace UVC.Data /// } /// }"); /// - /// var maskJson = JObject.Parse(@"{ + /// var maskJson = DataMask.Parse(@"{ /// ""user"": { /// ""name"": """", /// ""address"": { @@ -156,21 +158,22 @@ namespace UVC.Data /// } /// } /// }"); + /// maskJson.ObjectIdKey = "user"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result는 sourceJson과 동일한 중첩 구조를 유지 /// /// - private DataObject MapObject(JObject sourceObject, JObject maskObject, int depth = 0) + private DataObject MapObject(JObject sourceObject, DataMask maskObject, int depth = 0) { if (depth >= maxRecursionDepth) { // 깊이 제한에 도달하면 간소화된 처리 - return new DataObject(sourceObject); + return new DataObject(sourceObject) { IdKey = maskObject.ObjectIdKey, Name = maskObject.ObjectName }; } - DataObject target = new DataObject(); + DataObject target = new DataObject() { IdKey = maskObject.ObjectIdKey, Name = maskObject.ObjectName }; foreach (var property in sourceObject.Properties()) { if (maskObject.ContainsKey(property.Name)) @@ -181,7 +184,7 @@ namespace UVC.Data // 중첩된 객체 처리 if (sourceValue.Type == JTokenType.Object && maskValue.Type == JTokenType.Object) { - target[property.Name] = MapObject((JObject)sourceValue, (JObject)maskValue, depth + 1); + target[property.Name] = MapObject((JObject)sourceValue, (DataMask)maskValue, depth + 1); } // 중첩된 배열 처리 else if (sourceValue.Type == JTokenType.Array && maskValue.Type == JTokenType.Array) @@ -191,7 +194,12 @@ namespace UVC.Data } else { - MapProperty(property.Name, sourceValue, maskValue, target); + string propertyName = property.Name; + if(maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName)) + { + propertyName = maskObject.NamesForReplace[propertyName]; + } + MapProperty(propertyName, sourceValue, maskValue, target); } } else @@ -265,11 +273,12 @@ namespace UVC.Data /// ] /// }"); /// - /// var maskJson = JObject.Parse(@"{ + /// var maskJson = DataMask.Parse(@"{ /// ""contacts"": [ /// { ""type"": """", ""number"": """" } /// ] /// }"); + /// maskJson.ObjectIdKey = "contacts"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); @@ -296,7 +305,7 @@ namespace UVC.Data if (sourceItem.Type == JTokenType.Object && maskTemplate != null && maskTemplate.Type == JTokenType.Object) { - results[i] = MapObject((JObject)sourceItem, (JObject)maskTemplate); + results[i] = MapObject((JObject)sourceItem, (DataMask)maskTemplate); } else { @@ -351,7 +360,7 @@ namespace UVC.Data if (sourceItem.Type == JTokenType.Object && maskTemplate.Type == JTokenType.Object) { - targetArray.Add(MapObject((JObject)sourceItem, (JObject)maskTemplate)); + targetArray.Add(MapObject((JObject)sourceItem, (DataMask)maskTemplate)); } else if (sourceItem.Type == JTokenType.Array && maskTemplate.Type == JTokenType.Array) { @@ -408,7 +417,7 @@ namespace UVC.Data /// }"); /// /// // Mask 객체 설정 (열거형 포함) - /// var maskJson = new JObject(); + /// var maskJson = new DataMask(); /// maskJson["name"] = ""; /// maskJson["age"] = 0; /// maskJson["height"] = 0.0; @@ -416,6 +425,8 @@ namespace UVC.Data /// maskJson["birthDate"] = JToken.FromObject(DateTime.Now); /// maskJson["status"] = JToken.FromObject(UserStatus.Inactive); /// + /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 + /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result에는 모든 속성이 적절한 타입으로 변환됨 diff --git a/Assets/Scripts/UVC/Data/DataMask.cs b/Assets/Scripts/UVC/Data/DataMask.cs new file mode 100644 index 00000000..5ae555a4 --- /dev/null +++ b/Assets/Scripts/UVC/Data/DataMask.cs @@ -0,0 +1,26 @@ +#nullable enable + +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace UVC.Data +{ + public class DataMask : JObject + { + /// + /// DataObject의 Id에 해당하는 key 문자열 + /// + public string? ObjectIdKey { get; set; } = null; + + /// + /// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다. + /// + public string ObjectName { get; set; } = string.Empty; + + /// + /// 교체 할 이름을 나타내는 딕셔너리입니다. + /// + public Dictionary? NamesForReplace { get; set; } + + } +} diff --git a/Assets/Scripts/UVC/Data/DataMask.cs.meta b/Assets/Scripts/UVC/Data/DataMask.cs.meta new file mode 100644 index 00000000..7a46e9c8 --- /dev/null +++ b/Assets/Scripts/UVC/Data/DataMask.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 505d9da15c2b309419069d612aa06f15 \ No newline at end of file diff --git a/Assets/Scripts/UVC/Data/DataObject.cs b/Assets/Scripts/UVC/Data/DataObject.cs index fe95b854..f160e42a 100644 --- a/Assets/Scripts/UVC/Data/DataObject.cs +++ b/Assets/Scripts/UVC/Data/DataObject.cs @@ -8,8 +8,25 @@ using System.Linq; namespace UVC.Data { - public class DataObject : Dictionary, IDataObject + public class DataObject : SortedDictionary, IDataObject { + + /// + /// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다. + /// + public string Id { get => (IdKey != null && ContainsKey(IdKey)) ? this[IdKey].ToString() : this.First().Value.ToString(); } + + /// + /// Id에 해당하는 key 문자열 + /// + internal string? IdKey { get; set; } = null; + + /// + /// DataObject의 이름을 나타내는 속성입니다. + /// + public string Name { get; internal set; } = string.Empty; + + // 직접적인 변경이 있었던 키를 저장하는 리스트 protected List changedProperies = new List(); @@ -24,7 +41,7 @@ namespace UVC.Data /// /// 기본 생성자입니다. 빈 데이터 객체를 생성합니다. /// - public DataObject() {} + public DataObject() { } /// /// JObject로부터 데이터 객체를 생성합니다. @@ -320,7 +337,7 @@ namespace UVC.Data changedProperies.Clear(); foreach (var keyValue in (DataObject)other) { - if(!this.ContainsKey(keyValue.Key) || !this[keyValue.Key].Equals(keyValue.Value)) + if (!this.ContainsKey(keyValue.Key) || !this[keyValue.Key].Equals(keyValue.Value)) { this[keyValue.Key] = keyValue.Value; changedProperies.Add(keyValue.Key); diff --git a/Assets/Scripts/UVC/Tests/Data/DataMapperTests.cs b/Assets/Scripts/UVC/Tests/Data/DataMapperTests.cs index 6b96877f..241c23df 100644 --- a/Assets/Scripts/UVC/Tests/Data/DataMapperTests.cs +++ b/Assets/Scripts/UVC/Tests/Data/DataMapperTests.cs @@ -1,8 +1,9 @@ -using NUnit.Framework; +using Newtonsoft.Json.Linq; +using NUnit.Framework; using System; -using Newtonsoft.Json.Linq; -using UVC.Data; +using System.Collections.Generic; using UnityEngine; +using UVC.Data; namespace UVC.Tests.Data { @@ -39,6 +40,7 @@ namespace UVC.Tests.Data RunTest(nameof(Map_EmptyMaskArray_CopiesSourceArray), Map_EmptyMaskArray_CopiesSourceArray); RunTest(nameof(Map_ComplexNestedStructure_MapsCorrectly), Map_ComplexNestedStructure_MapsCorrectly); RunTest(nameof(Map_MixedArrayTypes_HandlesCorrectly), Map_MixedArrayTypes_HandlesCorrectly); + RunTest(nameof(Map_WithNamesForReplace_ShouldRenameProperties), Map_WithNamesForReplace_ShouldRenameProperties); Debug.Log("===== DataMapper 테스트 완료 ====="); } @@ -69,8 +71,9 @@ namespace UVC.Tests.Data public void Map_StringProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Name"] = "홍길동"; + mask.ObjectIdKey = "Name"; // Id로 사용할 키 설정 var source = JObject.Parse(@"{ ""Name"": ""김철수"" }"); @@ -82,6 +85,7 @@ namespace UVC.Tests.Data // Assert Assert.IsTrue(result.ContainsKey("Name")); Assert.AreEqual("김철수", result.GetString("Name")); + Assert.AreEqual("김철수", result.Id); } /// @@ -91,8 +95,9 @@ namespace UVC.Tests.Data public void Map_IntProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Age"] = 25; + mask.ObjectIdKey = "Age"; // Id로 사용할 키 설정 var source = JObject.Parse(@"{ ""Age"": 30 }"); @@ -104,6 +109,7 @@ namespace UVC.Tests.Data // Assert Assert.IsTrue(result.ContainsKey("Age")); Assert.AreEqual(30, result.GetInt("Age")); + Assert.AreEqual("30", result.Id); } /// @@ -113,8 +119,9 @@ namespace UVC.Tests.Data public void Map_DoubleProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Height"] = 175.5; + mask.ObjectIdKey = "Height"; // Id로 사용할 키 설정 var source = JObject.Parse(@"{ ""Height"": 180.5 }"); @@ -126,6 +133,7 @@ namespace UVC.Tests.Data // Assert Assert.IsTrue(result.ContainsKey("Height")); Assert.AreEqual(180.5, result.GetDouble("Height")); + Assert.AreEqual("180.5", result.Id); } /// @@ -135,7 +143,7 @@ namespace UVC.Tests.Data public void Map_BoolProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["IsActive"] = false; var source = JObject.Parse(@"{ ""IsActive"": true }"); @@ -148,6 +156,7 @@ namespace UVC.Tests.Data // Assert Assert.IsTrue(result.ContainsKey("IsActive")); Assert.IsTrue(result.GetBool("IsActive")); + Assert.AreEqual("True", result.Id);// Id는 Boolean 값의 문자열 표현으로 설정됨(첫글자 대문자) } /// @@ -157,7 +166,7 @@ namespace UVC.Tests.Data public void Map_DateTimeProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["BirthDate"] = JToken.FromObject(DateTime.Now); var expectedDate = new DateTime(1990, 1, 1); @@ -180,7 +189,7 @@ namespace UVC.Tests.Data public void Map_DataMapProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); var valueMapper = new DataMap(); valueMapper["ON"] = "활성화"; valueMapper["OFF"] = "비활성화"; @@ -205,7 +214,7 @@ namespace UVC.Tests.Data public void Map_DataMapWithUnmappedValue_ReturnsOriginal() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); var valueMapper = new DataMap(); valueMapper["ON"] = "활성화"; valueMapper["OFF"] = "비활성화"; @@ -230,7 +239,7 @@ namespace UVC.Tests.Data public void Map_EnumProperty_MapsCorrectly() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Status"] = JToken.FromObject(UserStatus.Inactive); var source = JObject.Parse(@"{ ""Status"": ""Active"" }"); @@ -252,7 +261,7 @@ namespace UVC.Tests.Data public void Map_AdditionalProperty_AddsToResult() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Name"] = "홍길동"; var source = JObject.Parse(@"{ ""Name"": ""김철수"", ""Email"": ""kim@example.com"" }"); @@ -274,7 +283,7 @@ namespace UVC.Tests.Data public void Map_InvalidDateTimeString_ReturnsNull() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["BirthDate"] = JToken.FromObject(DateTime.Now); var source = JObject.Parse(@"{ ""BirthDate"": ""InvalidDateTime"" }"); @@ -296,7 +305,7 @@ namespace UVC.Tests.Data public void Map_ComplexObject_MapsAllProperties() { // Arrange - var mask = new JObject(); + var mask = new DataMask(); mask["Id"] = 1; mask["Name"] = "홍길동"; mask["IsActive"] = false; @@ -338,12 +347,12 @@ namespace UVC.Tests.Data public void Map_NestedObject_MapsCorrectly() { // Arrange - var mask = new JObject + var mask = new DataMask { - ["User"] = new JObject + ["User"] = new DataMask { ["Name"] = "홍길동", - ["Address"] = new JObject + ["Address"] = new DataMask { ["City"] = "서울", ["ZipCode"] = "12345" @@ -388,11 +397,11 @@ namespace UVC.Tests.Data public void Map_ArrayMapping_MapsCorrectly() { // Arrange - var mask = new JObject + var mask = new DataMask { ["Contacts"] = new JArray { - new JObject + new DataMask { ["Type"] = "mobile", ["Number"] = "010-0000-0000" @@ -437,7 +446,7 @@ namespace UVC.Tests.Data public void Map_EmptyMaskArray_CopiesSourceArray() { // Arrange - var mask = new JObject + var mask = new DataMask { ["Tags"] = new JArray() }; @@ -474,20 +483,20 @@ namespace UVC.Tests.Data public void Map_ComplexNestedStructure_MapsCorrectly() { // Arrange - var mask = new JObject + var mask = new DataMask { - ["Company"] = new JObject + ["Company"] = new DataMask { ["Name"] = "회사명", ["Founded"] = JToken.FromObject(DateTime.Now), ["Departments"] = new JArray { - new JObject + new DataMask { ["Name"] = "부서명", ["Employees"] = new JArray { - new JObject + new DataMask { ["Name"] = "직원명", ["Age"] = 30, @@ -564,14 +573,14 @@ namespace UVC.Tests.Data public void Map_MixedArrayTypes_HandlesCorrectly() { // Arrange - var mask = new JObject + var mask = new DataMask { ["MixedArray"] = new JArray { "문자열", 123, true, - new JObject { ["Key"] = "Value" }, + new DataMask { ["Key"] = "Value" }, new JArray{1, 1} } }; @@ -620,6 +629,37 @@ namespace UVC.Tests.Data Assert.AreEqual(1, nestedArray[0].GetInt("value")); Assert.AreEqual(2, nestedArray[1].GetInt("value")); } + + + /// + /// NamesForReplace 속성이 제대로 구현되었을 때 예상되는 동작을 테스트합니다. + /// + [Test] + public void Map_WithNamesForReplace_ShouldRenameProperties() + { + // Arrange + var mask = new DataMask(); + mask["OldName"] = "홍길동"; + mask["Age"] = 25; + mask.NamesForReplace = new Dictionary { + { "OldName", "NewName" } + }; + + var source = JObject.Parse(@"{ ""OldName"": ""김철수"", ""Age"": 30 }"); + + var mapper = new DataMapper(mask); + + // Act + var result = mapper.Mapping(source); + + // Assert + // NamesForReplace가 제대로 구현되면 이 테스트는 통과해야 함 + Assert.IsTrue(result.ContainsKey("NewName")); + Assert.AreEqual("김철수", result.GetString("NewName")); + Assert.IsFalse(result.ContainsKey("OldName")); + Assert.IsTrue(result.ContainsKey("Age")); + Assert.AreEqual(30, result.GetInt("Age")); + } } /// diff --git a/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs new file mode 100644 index 00000000..2bf19c00 --- /dev/null +++ b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs @@ -0,0 +1,490 @@ +#nullable enable + +using Cysharp.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using UnityEngine; +using UVC.Data; +using UVC.Network; + +namespace UVC.Tests.Data +{ + /// + /// HttpPipeLine 클래스의 테스트를 위한 테스트 클래스입니다. + /// + [TestFixture] + public class HttpPipeLineTests + { + // 테스트에 사용할 HttpPipeLine 인스턴스 + private HttpPipeLine pipeLine; + + /// + /// 각 테스트 실행 전에 호출되는 설정 메서드입니다. + /// + [SetUp] + public void Setup() + { + pipeLine = new HttpPipeLine(); + // 테스트를 위한 DataRepository 초기화 + ClearDataRepository(); + } + + /// + /// 모든 테스트 메서드를 실행하는 메서드입니다. + /// + /// + /// 이 메서드는 클래스의 모든 테스트 메서드를 순차적으로 호출하고 + /// 각 테스트의 성공 또는 실패 여부를 로그로 출력합니다. + /// + public void TestAll() + { + Setup(); + + Debug.Log("===== HttpPipeLine 테스트 시작 ====="); + + RunTest(nameof(Add_NewInfo_AddedSuccessfully), Add_NewInfo_AddedSuccessfully); + RunTest(nameof(Add_ExistingInfo_UpdatesExistingEntry), Add_ExistingInfo_UpdatesExistingEntry); + RunTest(nameof(Remove_ExistingInfo_RemovedSuccessfully), Remove_ExistingInfo_RemovedSuccessfully); + RunTest(nameof(Remove_NonExistingInfo_DoesNothing), Remove_NonExistingInfo_DoesNothing); + RunTest(nameof(Excute_WithNonExistingKey_DoesNothing), Excute_WithNonExistingKey_DoesNothing); + // 비동기 테스트는 RunTest로 실행하기 어려워 별도 처리 필요 + // 간단하게 메시지만 출력 + Debug.Log($"비동기 테스트: {nameof(Excute_WithJObjectResponse_ProcessesDataCorrectly)}와 {nameof(Excute_WithJArrayResponse_ProcessesDataCorrectly)}는 수동으로 실행하세요."); + Debug.Log("===== HttpPipeLine 테스트 완료 ====="); + } + + /// + /// 단일 테스트 메서드를 실행하고 결과를 로그로 출력합니다. + /// + /// 테스트 메서드 이름 + /// 실행할 테스트 메서드 + private void RunTest(string testName, Action testAction) + { + try + { + Debug.Log($"테스트 시작: {testName}"); + testAction(); + Debug.Log($"테스트 성공: {testName}"); + } + catch (Exception ex) + { + Debug.LogError($"테스트 실패: {testName}\n{ex.Message}\n{ex.StackTrace}"); + } + } + + /// + /// 새로운 HttpPipeLineInfo를 추가하는 테스트 + /// + [Test] + public void Add_NewInfo_AddedSuccessfully() + { + // Arrange + var info = new HttpPipeLineInfo("http://test.com"); + + // Act + pipeLine.Add("test", info); + + // Assert - Dictionary에 추가되었는지 확인 (reflection 사용) + var infoList = GetInfoListField(); + Assert.IsTrue(infoList.ContainsKey("test")); + Assert.AreEqual(info, infoList["test"]); + } + + /// + /// 기존에 존재하는 키로 HttpPipeLineInfo를 추가할 때 업데이트 테스트 + /// + [Test] + public void Add_ExistingInfo_UpdatesExistingEntry() + { + // Arrange + var info1 = new HttpPipeLineInfo("http://test1.com"); + var info2 = new HttpPipeLineInfo("http://test2.com"); + pipeLine.Add("test", info1); + + // Act + pipeLine.Add("test", info2); + + // Assert + var infoList = GetInfoListField(); + Assert.IsTrue(infoList.ContainsKey("test")); + Assert.AreEqual(info2, infoList["test"]); + Assert.AreNotEqual(info1, infoList["test"]); + } + + /// + /// 존재하는 HttpPipeLineInfo를 제거하는 테스트 + /// + [Test] + public void Remove_ExistingInfo_RemovedSuccessfully() + { + // Arrange + var info = new HttpPipeLineInfo("http://test.com"); + pipeLine.Add("test", info); + + // Act + pipeLine.Remove("test"); + + // Assert + var infoList = GetInfoListField(); + Assert.IsFalse(infoList.ContainsKey("test")); + } + + /// + /// 존재하지 않는 키에 대한 Remove 호출 테스트 + /// + [Test] + public void Remove_NonExistingInfo_DoesNothing() + { + // Arrange + var info = new HttpPipeLineInfo("http://test.com"); + pipeLine.Add("test", info); + + // Act - 존재하지 않는 키 제거 시도 + pipeLine.Remove("nonexistent"); + + // Assert - 기존 항목은 여전히 존재해야 함 + var infoList = GetInfoListField(); + Assert.IsTrue(infoList.ContainsKey("test")); + Assert.AreEqual(info, infoList["test"]); + } + + /// + /// HttpPipeLine의 private infoList 필드 가져오기 + /// + private Dictionary GetInfoListField() + { + var fieldInfo = typeof(HttpPipeLine).GetField("infoList", + BindingFlags.NonPublic | BindingFlags.Instance); + + return (Dictionary)fieldInfo.GetValue(pipeLine); + } + + /// + /// DataRepository를 테스트를 위해 초기화 (리플렉션 사용) + /// + private void ClearDataRepository() + { + var repositoryType = typeof(DataRepository); + var instanceField = repositoryType.GetField("instance", + BindingFlags.NonPublic | BindingFlags.Static); + + if (instanceField != null) + { + // Lazy 필드 값을 가져옴 + var lazyInstance = instanceField.GetValue(null); + + // Lazy의 Value 속성을 통해 인스턴스 접근 + var instance = lazyInstance.GetType().GetProperty("Value").GetValue(lazyInstance); + + // dataObjects 딕셔너리 필드 가져오기 + var dataObjectsField = repositoryType.GetField("dataObjects", + BindingFlags.NonPublic | BindingFlags.Instance); + + // 딕셔너리 접근하여 Clear 메서드 호출 + var dataObjects = dataObjectsField.GetValue(instance) as Dictionary; + dataObjects.Clear(); + } + } + + /// + /// Excute 메소드에서 JObject 응답을 처리하는 기능 테스트 + /// + [Test] + public async Task Excute_WithJObjectResponse_ProcessesDataCorrectly() + { + // Arrange + bool handlerCalled = false; + DataObject? receivedData = null; + + var mockResponse = @"{""name"": ""테스트"", ""value"": 123}"; + + // DataMask와 DataMapper 설정 + var dataMask = new DataMask(); + dataMask["name"] = "이름"; + dataMask["value"] = 0; + + var dataMapper = new DataMapper(dataMask); + + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo("http://test.com") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCalled = true; + if (data is DataObject dataObject) + { + receivedData = dataObject; + } + }); + + pipeLine.Add("testKey", info); + + try + { + // HttpRequester의 Request 메소드를 대체하는 대신 + // 메소드를 직접 호출하여 테스트에서 통제할 수 있게 함 + // (실제 구현은 Mock 프레임워크를 사용하는 것이 좋음) + + // Execute 메소드 구현은 비동기지만 async void라서 직접 테스트 불가능 + // 대신 private 필드에 접근하여 로직을 검증 + + // Act + // 이 시점에서 실제 HTTP 요청 대신 mockResponse를 반환하도록 설정해야 함 + // 아래는 conceptual pseudo-code (실제 구현 불가) + // MockHttpRequesterHelper.PatchHttpRequester( + // (url, method, body, headers) => UniTask.FromResult(mockResponse)); + + // pipeLine.Excute("testKey"); + + // 대신 메소드 내부 로직을 수동으로 실행 + var key = "testKey"; + if (GetInfoListField().TryGetValue(key, out var pipeLineInfo)) + { + // HttpRequester.Request 호출하는 대신 가짜 응답 사용 + string result = mockResponse; + + IDataObject? dataObject = null; + if (!string.IsNullOrEmpty(result)) + { + result = result.Trim(); + if (result.StartsWith("{")) + { + JObject source = JObject.Parse(result); + if (pipeLineInfo.dataMapper != null) + dataObject = pipeLineInfo.dataMapper.Mapping(source); + } + } + + if (dataObject != null) + { + // AddData 대신 수동으로 처리 + pipeLineInfo.handler?.Invoke(dataObject); + } + } + + // Wait for async operations to complete + await Task.Delay(100); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "핸들러에 전달된 데이터가 null입니다."); + Assert.AreEqual("테스트", receivedData?.GetString("name")); + Assert.AreEqual(123, receivedData?.GetInt("value")); + } + finally + { + // 테스트 후 복원 + // MockHttpRequesterHelper.RestoreHttpRequester(); + pipeLine.Remove("testKey"); + } + } + + /// + /// Excute 메소드에서 JArray 응답을 처리하는 기능 테스트 + /// + [Test] + public async Task Excute_WithJArrayResponse_ProcessesDataCorrectly() + { + // Arrange + bool handlerCalled = false; + IDataObject? receivedData = null; + + var mockResponse = @"[{""name"": ""항목1"", ""value"": 10}, {""name"": ""항목2"", ""value"": 20}]"; + + // 배열용 DataMask 설정 + var dataMask = new DataMask + { + ["0"] = new DataMask + { + ["name"] = "이름", + ["value"] = 0 + } + }; + + var dataMapper = new DataMapper(dataMask); + + // HttpPipeLineInfo 설정 + var info = new HttpPipeLineInfo("http://test.com") + .setDataMapper(dataMapper) + .setHandler((data) => + { + handlerCalled = true; + receivedData = data; + }); + + pipeLine.Add("testArrayKey", info); + + try + { + // Act + var key = "testArrayKey"; + if (GetInfoListField().TryGetValue(key, out var pipeLineInfo)) + { + // 가짜 응답 사용 + string result = mockResponse; + + IDataObject? dataObject = null; + if (!string.IsNullOrEmpty(result)) + { + result = result.Trim(); + if (result.StartsWith("[")) + { + JArray source = JArray.Parse(result); + if (pipeLineInfo.dataMapper != null) + dataObject = pipeLineInfo.dataMapper.Mapping(source); + } + } + + if (dataObject != null) + { + pipeLineInfo.handler?.Invoke(dataObject); + } + } + + // Wait for async operations to complete + await Task.Delay(100); + + // Assert + Assert.IsTrue(handlerCalled, "핸들러가 호출되지 않았습니다."); + Assert.IsNotNull(receivedData, "핸들러에 전달된 데이터가 null입니다."); + Assert.IsTrue(receivedData is DataArray, "결과가 DataArray가 아닙니다."); + + var dataArray = receivedData as DataArray; + Assert.AreEqual(2, dataArray?.Count); + Assert.AreEqual("항목1", dataArray?[0].GetString("name")); + Assert.AreEqual(10, dataArray?[0].GetInt("value")); + Assert.AreEqual("항목2", dataArray?[1].GetString("name")); + Assert.AreEqual(20, dataArray?[1].GetInt("value")); + } + finally + { + pipeLine.Remove("testArrayKey"); + } + } + + /// + /// Excute 메소드에서 존재하지 않는 키를 호출했을 때의 동작 테스트 + /// + [Test] + public void Excute_WithNonExistingKey_DoesNothing() + { + // Arrange - 의도적으로 아무 것도 설정하지 않음 + + // Act & Assert - 예외가 발생하지 않아야 함 + Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey")); + } + } + + /// + /// HTTP 테스트를 위한 모의(Mock) HttpRequester 클래스입니다. + /// 실제 네트워크 요청을 하지 않고 테스트할 수 있게 합니다. + /// + public static class MockHttpRequester + { + // 테스트 응답 설정을 위한 딕셔너리 + private static readonly Dictionary mockResponses = new Dictionary(); + + /// + /// 테스트용 응답을 설정합니다. + /// + public static void SetMockResponse(string url, string response) + { + mockResponses[url] = response; + } + + /// + /// 모든 모의 응답을 제거합니다. + /// + public static void ClearMockResponses() + { + mockResponses.Clear(); + } + + /// + /// HTTP 요청을 모의로 실행하고 미리 설정된 응답을 반환합니다. + /// + public static Task Request(string url, string method, string body = null, + Dictionary headers = null) + { + if (mockResponses.TryGetValue(url, out string response)) + { + return Task.FromResult(response); + } + + // 기본 빈 응답 + return Task.FromResult("{}"); + } + + + + + + } + + /// + /// Excute 메소드 테스트를 위한 모의 HttpRequester + /// + class MockHttpRequesterHelper + { + // HttpRequester.Request 메소드를 대체할 delegate + public delegate UniTask RequestDelegate(string url, string method, string body, Dictionary headers); + + private static RequestDelegate originalRequestMethod; + private static bool isPatched = false; + + /// + /// HttpRequester.Request 메소드를 모의 구현으로 대체 + /// + public static void PatchHttpRequester(RequestDelegate mockRequestMethod) + { + // 원본 메소드 저장 (이미 패치되었다면 다시 저장하지 않음) + if (!isPatched) + { + var methodInfo = typeof(HttpRequester).GetMethod("Request", + new[] { typeof(string), typeof(string), typeof(string), typeof(Dictionary), typeof(bool) }); + + if (methodInfo != null) + { + // 이미 정적 필드에 저장된 상태라면 원본을 다시 가져올 필요 없음 + originalRequestMethod = (RequestDelegate)Delegate.CreateDelegate( + typeof(RequestDelegate), methodInfo); + isPatched = true; + } + } + + // Reflection을 사용하여 HttpRequester.Request 메소드를 우리 모의 메소드로 대체 + // 주의: 실제 환경에서는 좀 더 정교한 모킹 프레임워크를 사용하는 것이 좋음 + } + + /// + /// HttpRequester.Request를 원래대로 복원 + /// + public static void RestoreHttpRequester() + { + // 실제 환경에서는 원본 메소드로 복원하는 로직이 필요 + isPatched = false; + } + + /// + /// 테스트용 모의 Request 메소드 + /// + public static UniTask MockRequest(string url, string method, string body, Dictionary headers, + string mockResponse) + { + // 테스트용 응답 반환 + if (typeof(T) == typeof(string)) + { + return UniTask.FromResult((T)(object)mockResponse); + } + + // 다른 타입은 지원하지 않음 - 테스트에서 string만 사용 + throw new NotSupportedException("MockRequest only supports string responses"); + } + } + + +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta new file mode 100644 index 00000000..fea45846 --- /dev/null +++ b/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e90362e4ba6e6bf4bb2c320d28f13403 \ No newline at end of file diff --git a/Assets/Scripts/UVC/Tests/Tester.cs b/Assets/Scripts/UVC/Tests/Tester.cs index 6fc14b6c..ce8ca029 100644 --- a/Assets/Scripts/UVC/Tests/Tester.cs +++ b/Assets/Scripts/UVC/Tests/Tester.cs @@ -6,7 +6,8 @@ namespace UVC.Tests { public static void RunAllTests() { - new DataMapperTests().TestAll(); + //new DataMapperTests().TestAll(); + new HttpPipeLineTests().TestAll(); } } }