로직 개선
This commit is contained in:
@@ -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)))
|
||||
{
|
||||
if (!this[i].ToString().Equals(otherArray[i].ToString()))
|
||||
addedList.Add(otherDict[id]);
|
||||
}
|
||||
|
||||
// 수정된 항목 확인 (양쪽 모두에 있지만 내용이 다른 항목)
|
||||
foreach (var id in thisDict.Keys.Where(id => otherDict.ContainsKey(id)))
|
||||
{
|
||||
modifiedList.Add(this[i]);
|
||||
this[i].UpdateDifferent(otherArray[i]);
|
||||
var thisItem = thisDict[id];
|
||||
var otherItem = otherDict[id];
|
||||
|
||||
if (!thisItem.ToString().Equals(otherItem.ToString()))
|
||||
{
|
||||
modifiedList.Add(thisItem);
|
||||
thisItem.UpdateDifferent(otherItem);
|
||||
}
|
||||
}
|
||||
else if (i < this.Count)
|
||||
|
||||
// 실제 컬렉션 업데이트
|
||||
// 현재 배열에서 제거된 항목들을 제거
|
||||
for (int i = this.Count - 1; i >= 0; i--)
|
||||
{
|
||||
removedList.Add(this[i]);
|
||||
}
|
||||
else if (i < otherArray.Count)
|
||||
if (removedList.Contains(this[i]))
|
||||
{
|
||||
addedList.Add(otherArray[i]);
|
||||
this.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 추가된 항목들을 현재 배열에 추가
|
||||
foreach (var item in addedList)
|
||||
{
|
||||
this.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
||||
@@ -52,7 +53,7 @@ namespace UVC.Data
|
||||
/// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
|
||||
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
|
||||
/// </remarks>
|
||||
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과 동일한 중첩 구조를 유지
|
||||
/// </code>
|
||||
/// </example>
|
||||
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에는 모든 속성이 적절한 타입으로 변환됨
|
||||
|
||||
26
Assets/Scripts/UVC/Data/DataMask.cs
Normal file
26
Assets/Scripts/UVC/Data/DataMask.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
#nullable enable
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class DataMask : JObject
|
||||
{
|
||||
/// <summary>
|
||||
/// DataObject의 Id에 해당하는 key 문자열
|
||||
/// </summary>
|
||||
public string? ObjectIdKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다.
|
||||
/// </summary>
|
||||
public string ObjectName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 교체 할 이름을 나타내는 딕셔너리입니다.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? NamesForReplace { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/DataMask.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/DataMask.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 505d9da15c2b309419069d612aa06f15
|
||||
@@ -8,8 +8,25 @@ using System.Linq;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class DataObject : Dictionary<string, object>, IDataObject
|
||||
public class DataObject : SortedDictionary<string, object>, IDataObject
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다.
|
||||
/// </summary>
|
||||
public string Id { get => (IdKey != null && ContainsKey(IdKey)) ? this[IdKey].ToString() : this.First().Value.ToString(); }
|
||||
|
||||
/// <summary>
|
||||
/// Id에 해당하는 key 문자열
|
||||
/// </summary>
|
||||
internal string? IdKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// DataObject의 이름을 나타내는 속성입니다.
|
||||
/// </summary>
|
||||
public string Name { get; internal set; } = string.Empty;
|
||||
|
||||
|
||||
// 직접적인 변경이 있었던 키를 저장하는 리스트
|
||||
protected List<string> changedProperies = new List<string>();
|
||||
|
||||
@@ -24,7 +41,7 @@ namespace UVC.Data
|
||||
/// <summary>
|
||||
/// 기본 생성자입니다. 빈 데이터 객체를 생성합니다.
|
||||
/// </summary>
|
||||
public DataObject() {}
|
||||
public DataObject() { }
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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 값의 문자열 표현으로 설정됨(첫글자 대문자)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// NamesForReplace 속성이 제대로 구현되었을 때 예상되는 동작을 테스트합니다.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_WithNamesForReplace_ShouldRenameProperties()
|
||||
{
|
||||
// Arrange
|
||||
var mask = new DataMask();
|
||||
mask["OldName"] = "홍길동";
|
||||
mask["Age"] = 25;
|
||||
mask.NamesForReplace = new Dictionary<string, string> {
|
||||
{ "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"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
490
Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs
Normal file
490
Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpPipeLine 클래스의 테스트를 위한 테스트 클래스입니다.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class HttpPipeLineTests
|
||||
{
|
||||
// 테스트에 사용할 HttpPipeLine 인스턴스
|
||||
private HttpPipeLine pipeLine;
|
||||
|
||||
/// <summary>
|
||||
/// 각 테스트 실행 전에 호출되는 설정 메서드입니다.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
pipeLine = new HttpPipeLine();
|
||||
// 테스트를 위한 DataRepository 초기화
|
||||
ClearDataRepository();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 테스트 메서드를 실행하는 메서드입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 클래스의 모든 테스트 메서드를 순차적으로 호출하고
|
||||
/// 각 테스트의 성공 또는 실패 여부를 로그로 출력합니다.
|
||||
/// </remarks>
|
||||
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 테스트 완료 =====");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 단일 테스트 메서드를 실행하고 결과를 로그로 출력합니다.
|
||||
/// </summary>
|
||||
/// <param name="testName">테스트 메서드 이름</param>
|
||||
/// <param name="testAction">실행할 테스트 메서드</param>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 HttpPipeLineInfo를 추가하는 테스트
|
||||
/// </summary>
|
||||
[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"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존에 존재하는 키로 HttpPipeLineInfo를 추가할 때 업데이트 테스트
|
||||
/// </summary>
|
||||
[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"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 존재하는 HttpPipeLineInfo를 제거하는 테스트
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 존재하지 않는 키에 대한 Remove 호출 테스트
|
||||
/// </summary>
|
||||
[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"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpPipeLine의 private infoList 필드 가져오기
|
||||
/// </summary>
|
||||
private Dictionary<string, HttpPipeLineInfo> GetInfoListField()
|
||||
{
|
||||
var fieldInfo = typeof(HttpPipeLine).GetField("infoList",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
return (Dictionary<string, HttpPipeLineInfo>)fieldInfo.GetValue(pipeLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataRepository를 테스트를 위해 초기화 (리플렉션 사용)
|
||||
/// </summary>
|
||||
private void ClearDataRepository()
|
||||
{
|
||||
var repositoryType = typeof(DataRepository);
|
||||
var instanceField = repositoryType.GetField("instance",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
if (instanceField != null)
|
||||
{
|
||||
// Lazy<DataRepository> 필드 값을 가져옴
|
||||
var lazyInstance = instanceField.GetValue(null);
|
||||
|
||||
// Lazy<T>의 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<string, IDataObject>;
|
||||
dataObjects.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excute 메소드에서 JObject 응답을 처리하는 기능 테스트
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excute 메소드에서 JArray 응답을 처리하는 기능 테스트
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excute 메소드에서 존재하지 않는 키를 호출했을 때의 동작 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Excute_WithNonExistingKey_DoesNothing()
|
||||
{
|
||||
// Arrange - 의도적으로 아무 것도 설정하지 않음
|
||||
|
||||
// Act & Assert - 예외가 발생하지 않아야 함
|
||||
Assert.DoesNotThrow(() => pipeLine.Excute("nonExistingKey"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 테스트를 위한 모의(Mock) HttpRequester 클래스입니다.
|
||||
/// 실제 네트워크 요청을 하지 않고 테스트할 수 있게 합니다.
|
||||
/// </summary>
|
||||
public static class MockHttpRequester
|
||||
{
|
||||
// 테스트 응답 설정을 위한 딕셔너리
|
||||
private static readonly Dictionary<string, string> mockResponses = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// 테스트용 응답을 설정합니다.
|
||||
/// </summary>
|
||||
public static void SetMockResponse(string url, string response)
|
||||
{
|
||||
mockResponses[url] = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 모의 응답을 제거합니다.
|
||||
/// </summary>
|
||||
public static void ClearMockResponses()
|
||||
{
|
||||
mockResponses.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청을 모의로 실행하고 미리 설정된 응답을 반환합니다.
|
||||
/// </summary>
|
||||
public static Task<string> Request(string url, string method, string body = null,
|
||||
Dictionary<string, string> headers = null)
|
||||
{
|
||||
if (mockResponses.TryGetValue(url, out string response))
|
||||
{
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
|
||||
// 기본 빈 응답
|
||||
return Task.FromResult("{}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excute 메소드 테스트를 위한 모의 HttpRequester
|
||||
/// </summary>
|
||||
class MockHttpRequesterHelper
|
||||
{
|
||||
// HttpRequester.Request 메소드를 대체할 delegate
|
||||
public delegate UniTask<T> RequestDelegate<T>(string url, string method, string body, Dictionary<string, string> headers);
|
||||
|
||||
private static RequestDelegate<string> originalRequestMethod;
|
||||
private static bool isPatched = false;
|
||||
|
||||
/// <summary>
|
||||
/// HttpRequester.Request 메소드를 모의 구현으로 대체
|
||||
/// </summary>
|
||||
public static void PatchHttpRequester(RequestDelegate<string> mockRequestMethod)
|
||||
{
|
||||
// 원본 메소드 저장 (이미 패치되었다면 다시 저장하지 않음)
|
||||
if (!isPatched)
|
||||
{
|
||||
var methodInfo = typeof(HttpRequester).GetMethod("Request",
|
||||
new[] { typeof(string), typeof(string), typeof(string), typeof(Dictionary<string, string>), typeof(bool) });
|
||||
|
||||
if (methodInfo != null)
|
||||
{
|
||||
// 이미 정적 필드에 저장된 상태라면 원본을 다시 가져올 필요 없음
|
||||
originalRequestMethod = (RequestDelegate<string>)Delegate.CreateDelegate(
|
||||
typeof(RequestDelegate<string>), methodInfo);
|
||||
isPatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reflection을 사용하여 HttpRequester.Request 메소드를 우리 모의 메소드로 대체
|
||||
// 주의: 실제 환경에서는 좀 더 정교한 모킹 프레임워크를 사용하는 것이 좋음
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HttpRequester.Request를 원래대로 복원
|
||||
/// </summary>
|
||||
public static void RestoreHttpRequester()
|
||||
{
|
||||
// 실제 환경에서는 원본 메소드로 복원하는 로직이 필요
|
||||
isPatched = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 테스트용 모의 Request 메소드
|
||||
/// </summary>
|
||||
public static UniTask<T> MockRequest<T>(string url, string method, string body, Dictionary<string, string> headers,
|
||||
string mockResponse)
|
||||
{
|
||||
// 테스트용 응답 반환
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
return UniTask.FromResult((T)(object)mockResponse);
|
||||
}
|
||||
|
||||
// 다른 타입은 지원하지 않음 - 테스트에서 string만 사용
|
||||
throw new NotSupportedException("MockRequest only supports string responses");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
2
Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta
Normal file
2
Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e90362e4ba6e6bf4bb2c320d28f13403
|
||||
@@ -6,7 +6,8 @@ namespace UVC.Tests
|
||||
{
|
||||
public static void RunAllTests()
|
||||
{
|
||||
new DataMapperTests().TestAll();
|
||||
//new DataMapperTests().TestAll();
|
||||
new HttpPipeLineTests().TestAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user