20 KiB
20 KiB
🧪 Unit Test 구현 계획 - 2025-09-23 개정판
✅ 전제 조건: MAJOR_REFACTORING_GUIDE 완료, GPU 파이프라인 재설계 완료 🎯 현재 목표: 단순화된 아키텍처 (734줄)에 대한 포괄적 Unit Test 구현
🔍 현재 상황 (2025-09-23)
✅ 완료된 작업
- MAJOR_REFACTORING_GUIDE: 복잡한 파이프라인 제거, ProcessSingleFrame() 25줄 단순화
- GPU 파이프라인: SimpleGPURenderer 구현, CPU-GPU 하이브리드 아키텍처
- 기본 Unit Test: VideoTypesTest.cpp 완료
- 헤드리스 프로젝트: 빌드 및 실행 성공
📋 현재 Unit Test 구조
D:\Project\video-av1\vav2\Vav2Player\Vav2Player\
├── Vav2UnitTest.vcxproj # ✅ 기존 테스트 프로젝트
├── unit-test/
│ ├── pch.h / pch.cpp # ✅ 테스트 전용 PCH
│ └── VideoTypesTest.cpp # ✅ 기본 데이터 구조체 테스트
└── x64/Debug/UnitTest/ # ✅ 빌드 출력
🎯 추가 필요한 테스트
- WebMFileReader: 파일 파싱 로직 (실제 구체 클래스 테스트)
- AV1Decoder: 디코딩 로직
- VideoDecoderFactory: 팩토리 패턴
- SimpleGPURenderer: GPU 렌더링 (스텁 모드)
- 통합 테스트: 전체 파이프라인
🔍 인터페이스 분석 (Unit Test 전 필수 검토)
현재 인터페이스 상황
✅ IVideoDecoder.h // 이미 인터페이스화 완료
❌ WebMFileReader.h // 구체 클래스, 인터페이스 없음
❌ SimpleGPURenderer.h // 구체 클래스, 인터페이스 없음
✅ VideoDecoderFactory.h // 팩토리 패턴, 인터페이스 기반
🔧 Option A: 인터페이스 추가 리팩토링 (권장)
A.1 IWebMFileReader 인터페이스 생성
// src/FileIO/IWebMFileReader.h (신규 생성)
#pragma once
#include "../Common/VideoTypes.h"
#include <string>
#include <vector>
namespace Vav2Player {
class IWebMFileReader {
public:
virtual ~IWebMFileReader() = default;
// 파일 열기/닫기
virtual bool OpenFile(const std::string& file_path) = 0;
virtual void CloseFile() = 0;
virtual bool IsFileOpen() const = 0;
// 파일 및 스트림 정보
virtual const VideoMetadata& GetVideoMetadata() const = 0;
virtual std::string GetFilePath() const = 0;
// 비디오 트랙 관리
virtual std::vector<VideoTrackInfo> GetVideoTracks() const = 0;
virtual bool SelectVideoTrack(int track_number) = 0;
// 패킷 읽기
virtual bool ReadNextPacket(VideoPacket& packet) = 0;
virtual bool SeekToTime(double time_seconds) = 0;
virtual bool SeekToFrame(uint64_t frame_number) = 0;
virtual bool Reset() = 0;
};
}
A.2 WebMFileReader 리팩토링
// src/FileIO/WebMFileReader.h 수정
class WebMFileReader : public IWebMFileReader {
// 기존 구현을 virtual 메서드로 변경
// 인터페이스 상속으로 변경
};
A.3 IVideoRenderer 인터페이스 생성
// src/Rendering/IVideoRenderer.h (신규 생성)
#pragma once
#include "../Common/VideoTypes.h"
#include <windows.h>
namespace Vav2Player {
class IVideoRenderer {
public:
virtual ~IVideoRenderer() = default;
// 초기화
virtual HRESULT Initialize(uint32_t width, uint32_t height) = 0;
virtual bool IsInitialized() const = 0;
virtual void Cleanup() = 0;
// 렌더링
virtual bool TryRenderFrame(const VideoFrame& frame) = 0;
virtual HRESULT RenderVideoFrame(const VideoFrame& frame) = 0;
// 설정
virtual void SetAspectFitMode(bool enabled) = 0;
virtual void UpdateContainerSize(uint32_t width, uint32_t height) = 0;
};
}
🚀 Option B: 인터페이스 없이 직접 테스트 (빠른 시작)
B.1 장점
- 즉시 테스트 작성 시작 가능
- 기존 코드 수정 없음
- 단순한 접근법
B.2 단점
- Mock 생성 어려움
- 의존성 주입 불가능
- 테스트 격리 한계
📋 단계별 구현 계획
🔄 Phase 0: 인터페이스 리팩토링 (선택적)
0.1 인터페이스 필요성 평가
// 테스트 복잡도 vs 인터페이스 도입 비용
Component | Mock 필요도 | 인터페이스 우선순위
--------------------|----------|----------------
WebMFileReader | 높음 | ⭐⭐⭐ 권장
SimpleGPURenderer | 중간 | ⭐⭐ 선택적
VideoDecoderFactory | 낮음 | ⭐ 이미 충분
0.2 인터페이스 리팩토링 (선택 시)
- IWebMFileReader.h 생성 (2시간)
- WebMFileReader.h 수정 (1시간)
- IVideoRenderer.h 생성 (1시간)
- SimpleGPURenderer.h 수정 (1시간)
- VideoPlayerControl.xaml.h 의존성 업데이트 (30분)
- 빌드 테스트 (30분)
총 예상 시간: 6시간
🔧 Phase 1: 핵심 컴포넌트 테스트 (우선순위 1)
1.1 WebMFileReader 테스트 (인터페이스 기반)
// unit-test/WebMFileReaderTest.cpp
#include "pch.h"
#include "../src/FileIO/WebMFileReader.h"
// Option A를 선택한 경우: Mock을 사용한 테스트
#ifdef USE_INTERFACE_APPROACH
#include "../unit-test/mocks/MockWebMFileReader.h"
#endif
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace Vav2Player;
TEST_CLASS(WebMFileReaderTest)
{
public:
// Option A: 인터페이스 기반 테스트 (Mock 사용 가능)
TEST_METHOD(OpenFile_ValidFile_ShouldReturnTrue_WithMock)
{
#ifdef USE_INTERFACE_APPROACH
// Arrange
MockWebMFileReader mockReader;
mockReader.SetOpenFileResult(true);
// Act
bool result = mockReader.OpenFile("test.webm");
// Assert
Assert::IsTrue(result);
#endif
}
// Option B: 직접 테스트 (인터페이스 없이)
TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly)
{
// Arrange & Act
WebMFileReader reader;
// Assert
Assert::IsFalse(reader.IsFileOpen());
}
TEST_METHOD(OpenFile_NonExistentFile_ShouldReturnFalse)
{
// Arrange
WebMFileReader reader;
std::string nonExistentFile = "nonexistent_file.webm";
// Act
bool result = reader.OpenFile(nonExistentFile);
// Assert
Assert::IsFalse(result);
}
TEST_METHOD(OpenFile_EmptyFilename_ShouldReturnFalse)
{
// Arrange
WebMFileReader reader;
std::string emptyFile = "";
// Act
bool result = reader.OpenFile(emptyFile);
// Assert
Assert::IsFalse(result);
}
};
1.2 AV1Decoder 테스트
// unit-test/AV1DecoderTest.cpp
#include "pch.h"
#include "../src/Decoder/AV1Decoder.h"
TEST_CLASS(AV1DecoderTest)
{
public:
TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly)
{
// Arrange & Act
AV1Decoder decoder;
// Assert
Assert::IsFalse(decoder.IsInitialized());
}
TEST_METHOD(Initialize_ValidMetadata_ShouldReturnTrue)
{
// Arrange
AV1Decoder decoder;
VideoMetadata metadata;
metadata.width = 1920;
metadata.height = 1080;
metadata.frame_rate = 30.0;
metadata.codec_type = VideoCodecType::AV1;
// Act
bool result = decoder.Initialize(metadata);
// Assert
Assert::IsTrue(result);
Assert::IsTrue(decoder.IsInitialized());
}
TEST_METHOD(Initialize_InvalidCodecType_ShouldReturnFalse)
{
// Arrange
AV1Decoder decoder;
VideoMetadata metadata;
metadata.codec_type = VideoCodecType::VP9; // Wrong codec
// Act
bool result = decoder.Initialize(metadata);
// Assert
Assert::IsFalse(result);
}
};
1.3 VideoDecoderFactory 테스트
// unit-test/VideoDecoderFactoryTest.cpp
#include "pch.h"
#include "../src/Decoder/VideoDecoderFactory.h"
TEST_CLASS(VideoDecoderFactoryTest)
{
public:
TEST_METHOD(CreateDecoder_AV1Software_ShouldReturnAV1Decoder)
{
// Act
auto decoder = VideoDecoderFactory::CreateDecoder(
VideoCodecType::AV1,
VideoDecoderFactory::DecoderType::SOFTWARE
);
// Assert
Assert::IsNotNull(decoder.get());
}
TEST_METHOD(CreateDecoder_AV1Auto_ShouldReturnDecoder)
{
// Act
auto decoder = VideoDecoderFactory::CreateDecoder(
VideoCodecType::AV1,
VideoDecoderFactory::DecoderType::AUTO
);
// Assert
Assert::IsNotNull(decoder.get());
}
TEST_METHOD(IsCodecSupported_AV1_ShouldReturnTrue)
{
// Act
bool isSupported = VideoDecoderFactory::IsCodecSupported(VideoCodecType::AV1);
// Assert
Assert::IsTrue(isSupported);
}
};
🔧 Phase 1.5: Mock 시스템 구현 (Option A 선택 시)
1.5.1 MockWebMFileReader 구현
// unit-test/mocks/MockWebMFileReader.h
#pragma once
#include "../../src/FileIO/IWebMFileReader.h"
#include <vector>
#include <string>
namespace Vav2Player {
class MockWebMFileReader : public IWebMFileReader {
private:
bool m_openFileResult = true;
bool m_isFileOpen = false;
VideoMetadata m_metadata;
std::vector<VideoPacket> m_testPackets;
size_t m_currentPacketIndex = 0;
public:
// Mock 설정 메서드들
void SetOpenFileResult(bool result) { m_openFileResult = result; }
void SetMetadata(const VideoMetadata& metadata) { m_metadata = metadata; }
void AddTestPacket(const VideoPacket& packet) { m_testPackets.push_back(packet); }
void ResetPackets() { m_testPackets.clear(); m_currentPacketIndex = 0; }
// IWebMFileReader 구현
bool OpenFile(const std::string& file_path) override {
m_isFileOpen = m_openFileResult;
return m_openFileResult;
}
void CloseFile() override {
m_isFileOpen = false;
m_currentPacketIndex = 0;
}
bool IsFileOpen() const override {
return m_isFileOpen;
}
const VideoMetadata& GetVideoMetadata() const override {
return m_metadata;
}
std::string GetFilePath() const override {
return "mock_file.webm";
}
std::vector<VideoTrackInfo> GetVideoTracks() const override {
std::vector<VideoTrackInfo> tracks;
VideoTrackInfo track;
track.track_number = 1;
track.codec_type = VideoCodecType::AV1;
track.width = 1920;
track.height = 1080;
tracks.push_back(track);
return tracks;
}
bool SelectVideoTrack(int track_number) override {
return track_number == 1;
}
bool ReadNextPacket(VideoPacket& packet) override {
if (m_currentPacketIndex >= m_testPackets.size()) {
return false;
}
packet = m_testPackets[m_currentPacketIndex++];
return true;
}
bool SeekToTime(double time_seconds) override {
m_currentPacketIndex = 0;
return true;
}
bool SeekToFrame(uint64_t frame_number) override {
m_currentPacketIndex = static_cast<size_t>(frame_number);
return m_currentPacketIndex < m_testPackets.size();
}
bool Reset() override {
m_currentPacketIndex = 0;
return true;
}
};
}
1.5.2 MockVideoRenderer 구현
// unit-test/mocks/MockVideoRenderer.h
#pragma once
#include "../../src/Rendering/IVideoRenderer.h"
namespace Vav2Player {
class MockVideoRenderer : public IVideoRenderer {
private:
bool m_initialized = false;
bool m_tryRenderResult = true;
HRESULT m_renderResult = S_OK;
uint32_t m_width = 0;
uint32_t m_height = 0;
public:
// Mock 설정
void SetTryRenderResult(bool result) { m_tryRenderResult = result; }
void SetRenderResult(HRESULT result) { m_renderResult = result; }
// 테스트 검증용
uint32_t GetLastWidth() const { return m_width; }
uint32_t GetLastHeight() const { return m_height; }
// IVideoRenderer 구현
HRESULT Initialize(uint32_t width, uint32_t height) override {
m_width = width;
m_height = height;
m_initialized = (width > 0 && height > 0);
return m_initialized ? S_OK : E_INVALIDARG;
}
bool IsInitialized() const override {
return m_initialized;
}
void Cleanup() override {
m_initialized = false;
m_width = 0;
m_height = 0;
}
bool TryRenderFrame(const VideoFrame& frame) override {
return m_initialized && m_tryRenderResult;
}
HRESULT RenderVideoFrame(const VideoFrame& frame) override {
return m_initialized ? m_renderResult : E_NOT_VALID_STATE;
}
void SetAspectFitMode(bool enabled) override {
// Mock implementation - do nothing
}
void UpdateContainerSize(uint32_t width, uint32_t height) override {
// Mock implementation - store values
m_width = width;
m_height = height;
}
};
}
⚡ Phase 2: GPU 렌더링 테스트
2.1 SimpleGPURenderer 테스트 (스텁 모드)
// unit-test/SimpleGPURendererTest.cpp
#include "pch.h"
#include "../src/Rendering/SimpleGPURenderer.h"
TEST_CLASS(SimpleGPURendererTest)
{
public:
TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly)
{
// Arrange & Act
SimpleGPURenderer renderer;
// Assert
Assert::IsFalse(renderer.IsInitialized());
}
TEST_METHOD(Initialize_ValidDimensions_ShouldReturnTrue)
{
// Arrange
SimpleGPURenderer renderer;
// Act
HRESULT hr = renderer.Initialize(1920, 1080);
// Assert
// Note: May fail in headless environment, but should not crash
// Success depends on D3D12 availability
Assert::IsTrue(SUCCEEDED(hr) || hr == DXGI_ERROR_UNSUPPORTED);
}
};
🧪 Phase 3: 통합 테스트
3.1 간단한 통합 테스트
// unit-test/IntegrationTest.cpp
#include "pch.h"
#include "../src/FileIO/WebMFileReader.h"
#include "../src/Decoder/VideoDecoderFactory.h"
TEST_CLASS(IntegrationTest)
{
public:
TEST_METHOD(BasicPipeline_ComponentCreation_ShouldWork)
{
// Arrange & Act
auto fileReader = std::make_unique<WebMFileReader>();
auto decoder = VideoDecoderFactory::CreateDecoder(
VideoCodecType::AV1,
VideoDecoderFactory::DecoderType::SOFTWARE
);
// Assert
Assert::IsNotNull(fileReader.get());
Assert::IsNotNull(decoder.get());
}
TEST_METHOD(VideoFrame_AllocationAndDeallocation_ShouldNotLeak)
{
// Memory leak test
for (int i = 0; i < 100; i++) {
VideoFrame frame;
bool result = frame.AllocateYUV420P(1920, 1080);
Assert::IsTrue(result);
}
// Frames should automatically deallocate
}
};
📁 최종 테스트 프로젝트 구조
목표 구조 (Phase 3 완료 후)
D:\Project\video-av1\vav2\Vav2Player\Vav2Player\
├── Vav2UnitTest.vcxproj
├── unit-test/
│ ├── pch.h / pch.cpp # ✅ 기존
│ ├── VideoTypesTest.cpp # ✅ 기존
│ ├── WebMFileReaderTest.cpp # 🆕 Phase 1
│ ├── AV1DecoderTest.cpp # 🆕 Phase 1
│ ├── VideoDecoderFactoryTest.cpp # 🆕 Phase 1
│ ├── SimpleGPURendererTest.cpp # 🆕 Phase 2
│ └── IntegrationTest.cpp # 🆕 Phase 3
└── x64/Debug/UnitTest/
프로젝트 파일 업데이트
<!-- Vav2UnitTest.vcxproj에 추가할 항목들 -->
<ClCompile Include="unit-test\WebMFileReaderTest.cpp" />
<ClCompile Include="unit-test\AV1DecoderTest.cpp" />
<ClCompile Include="unit-test\VideoDecoderFactoryTest.cpp" />
<ClCompile Include="unit-test\SimpleGPURendererTest.cpp" />
<ClCompile Include="unit-test\IntegrationTest.cpp" />
⚙️ 구현 상세
1. Mock 없는 실제 클래스 테스트
- 접근법: 구체 클래스를 직접 테스트, Mock 복잡성 제거
- 외부 의존성: 실패 케이스 테스트로 격리 (존재하지 않는 파일 등)
- GPU 의존성: 실패해도 크래시하지 않는지 검증
2. 테스트 데이터 최소화
- 파일 의존성: 실제 파일 대신 존재하지 않는 파일명으로 에러 케이스 테스트
- 메모리 테스트: 동적 할당/해제 반복으로 메모리 누수 검증
- 단순한 입력: 기본값과 경계값을 활용한 테스트
3. CI/CD 통합
# 자동 테스트 스크립트
cd "D:\Project\video-av1\vav2\Vav2Player\Vav2Player"
# 1. 유닛 테스트 빌드
MSBuild Vav2UnitTest.vcxproj //p:Configuration=Debug //p:Platform=x64 //v:minimal
# 2. 테스트 실행
vstest.console.exe "x64\Debug\UnitTest\Vav2UnitTest.dll"
🤔 의사결정 가이드
Option A vs Option B 선택 기준
Option A 선택 (인터페이스 + Mock)
다음 조건 중 하나라도 해당하면 권장:
- 의존성 주입 패턴을 사용하고 싶은 경우
- 복잡한 시나리오 테스트가 필요한 경우
- 외부 의존성 격리가 중요한 경우
- 장기적인 테스트 확장성이 필요한 경우
예상 시간: Phase 0 (6시간) + Phase 1-3 (4일) = 5일
Option B 선택 (직접 테스트)
다음 조건에 해당하면 권장:
- 빠른 테스트 구현이 우선인 경우
- 기존 코드 수정을 최소화하려는 경우
- 단순한 기능 검증만 필요한 경우
- 리소스(시간/인력)가 제한적인 경우
예상 시간: Phase 1-3 (4일) = 4일
권장사항
현재 상황 고려 시 🎯 Option A 권장
이유:
1. WebMFileReader는 파일 I/O 의존성이 높음
2. GPU 렌더링은 환경 의존성이 높음
3. 장기적인 유지보수성 필요
4. MAJOR_REFACTORING_GUIDE 완료로 안정적인 기반 확보됨
🕐 실행 스케줄
Option A 선택 시 (권장)
- Phase 0: 1일 (인터페이스 리팩토링)
- Phase 1.5: 1일 (Mock 시스템)
- Phase 1: 2일 (핵심 컴포넌트 테스트)
- Phase 2: 1일 (GPU 렌더링 테스트)
- Phase 3: 1일 (통합 테스트)
총 6일
Option B 선택 시
- Phase 1: 2-3일 (핵심 컴포넌트 테스트)
- Phase 2: 1일 (GPU 렌더링 테스트)
- Phase 3: 1일 (통합 테스트)
총 4-5일
우선순위
- 의사결정: Option A vs B 선택
- WebMFileReaderTest.cpp (가장 핵심적)
- AV1DecoderTest.cpp (디코딩 로직)
- VideoDecoderFactoryTest.cpp (팩토리 패턴)
- SimpleGPURendererTest.cpp (GPU 안정성)
- IntegrationTest.cpp (전체 검증)
🎯 성공 지표
정량적 목표
- 테스트 커버리지: 핵심 public 메서드 80% 이상
- 테스트 실행 시간: 30초 이내
- 테스트 파일 수: 6개 (기존 1개 + 신규 5개)
- 빌드 성공률: 100%
정성적 목표
- 크래시 방지: 모든 테스트가 크래시 없이 완료
- 회귀 방지: 기존 기능 변경 시 테스트로 검증 가능
- 유지보수성: 새로운 컴포넌트 추가 시 테스트 패턴 재사용 가능
⚠️ 주의사항
1. 환경 의존성 최소화
- D3D12: GPU 없는 환경에서도 실패하지 않도록 처리
- dav1d: 라이브러리 로드 실패 시에도 테스트 완료
- MediaFoundation: 지원되지 않는 환경에서 graceful failure
2. 테스트 격리
- 파일 시스템: 실제 파일에 의존하지 않는 테스트 우선
- 전역 상태: 테스트 간 상호 영향 최소화
- 메모리: 각 테스트 후 리소스 정리
3. 실용적 접근
- 완벽한 Coverage보다 핵심 기능 안정성
- 복잡한 Mock보다 간단한 실제 테스트
- 외부 의존성 Mock보다 에러 케이스 활용
🎯 핵심 메시지: "단순하고 실용적인 테스트로 안정성 확보"
문서 작성일: 2025-09-23 이전 문서 대체: UNIT_TEST_REFACTORING_PLAN.md (구식)