로직 개선

This commit is contained in:
김형인
2025-06-07 01:53:51 +09:00
parent d0e299585b
commit 4b490d79f4
9 changed files with 671 additions and 58 deletions

View File

@@ -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);
}
}
}

View File

@@ -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에는 모든 속성이 적절한 타입으로 변환됨

View 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; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 505d9da15c2b309419069d612aa06f15

View File

@@ -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);

View File

@@ -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>

View 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");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e90362e4ba6e6bf4bb2c320d28f13403

View File

@@ -6,7 +6,8 @@ namespace UVC.Tests
{
public static void RunAllTests()
{
new DataMapperTests().TestAll();
//new DataMapperTests().TestAll();
new HttpPipeLineTests().TestAll();
}
}
}