Files
XRLib/Assets/Scripts/UVC/Tests/Data/HttpPipeLineTests.cs

490 lines
18 KiB
C#
Raw Normal View History

2025-06-07 01:53:51 +09:00
#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");
}
}
}