로직 개선

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

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