Files
video-v1/vav2/docs/completed/testing/UNIT_TEST_REFACTORING_PLAN.md
2025-09-28 17:10:41 +09:00

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 인터페이스 리팩토링 (선택 시)

  1. IWebMFileReader.h 생성 (2시간)
  2. WebMFileReader.h 수정 (1시간)
  3. IVideoRenderer.h 생성 (1시간)
  4. SimpleGPURenderer.h 수정 (1시간)
  5. VideoPlayerControl.xaml.h 의존성 업데이트 (30분)
  6. 빌드 테스트 (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일

우선순위

  1. 의사결정: Option A vs B 선택
  2. WebMFileReaderTest.cpp (가장 핵심적)
  3. AV1DecoderTest.cpp (디코딩 로직)
  4. VideoDecoderFactoryTest.cpp (팩토리 패턴)
  5. SimpleGPURendererTest.cpp (GPU 안정성)
  6. 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 (구식)