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