[MVVM] 8.유닛 테스트와 MVVM, Mocking을 통한 Dependency Injection 기반의 테스트 환경 구축

소프트웨어 개발에서 테스트는 필수 요소이며, 특히 MVVM (Model-View-ViewModel) 패턴을 사용하는 WPF 애플리케이션에서는 유닛 테스트의 필요성이 더욱 강조됩니다. 본 포스트에서는 MVVM 패턴을 기반으로 한 WPF 애플리케이션에서 유닛 테스트를 작성하는 방법과 Mocking을 통한 Dependency Injection(DI) 기반의 테스트 환경을 구축하는 방법에 대해 자세히 설명하겠습니다.

1. MVVM 아키텍처 소개

MVVM 패턴은 WPF와 같은 데이터 바인딩을 사용하는 기술에 이상적인 아키텍처입니다. 이 패턴은 세 가지 주요 구성 요소로 나뉘며, 각 구성 요소는 다음과 같은 역할을 수행합니다.

  • Model: 애플리케이션의 데이터 구조를 정의하고, 데이터와 관련된 비즈니스 로직을 처리합니다.
  • View: 사용자 인터페이스를 표현하며, 사용자와 상호작용을 담당합니다.
  • ViewModel: View와 Model 간의 상호작용을 조정하고, View에 필요한 데이터를 제공합니다.

2. 유닛 테스트의 중요성

유닛 테스트는 각 애플리케이션의 구성 요소가 기대한 대로 작동하는지 확인하기 위해 개별적으로 검증하는 프로세스입니다. WPF와 MVVM 아키텍처에서는 ViewModel의 동작을 테스트하는 것이 중요하며, 이를 통해 UI와 비즈니스 로직의 분리를 검증할 수 있습니다. 유닛 테스트를 통해 얻는 이점은 다음과 같습니다:

  • 버그 조기 발견
  • 코드 변경 시 안정성 확보
  • 신뢰할 수 있는 리팩토링 가능
  • 문서화된 코드

3. Testing Framework 및 Mocking Library

유닛 테스트를 쉽게 수행하기 위해 다양한 Testing Framework과 Mocking Library가 존재합니다. 대표적으로 다음과 같은 도구들이 있습니다:

  • xUnit: 경량화된 유닛 테스트 프레임워크.
  • NUnit: .NET의 강력한 유닛 테스트 프레임워크.
  • Moq: 간단하고 유연한 Mocking Framework.

4. Dependency Injection(DI)의 개념

Dependency Injection은 객체 지향 설계 원칙 중 하나로, 클래스가 필요한 의존성을 외부에서 주입받도록 하여 의존성을 줄이고, 코드의 테스트 용이성을 높입니다. MVVM 아키텍처에서 DI는 주로 ViewModel의 의존성을 관리하는 데 사용됩니다.

4.1. DI 컨테이너

DI를 구현하기 위해 DI 컨테이너를 사용할 수 있으며, 대표적으로 Microsoft.Extensions.DependencyInjectionAutofac가 있습니다. DI 컨테이너는 객체의 생명주기를 관리하고, 필요에 따라 인스턴스를 제공하는 역할을 합니다.

5. MVVM을 활용한 유닛 테스트 사례

이제 실제 MVVM 구조의 WPF 애플리케이션을 예시로 들어 유닛 테스트를 작성해보겠습니다. 간단한 Todo List 어플리케이션을 예로 들어보겠습니다.

5.1. Model 정의

public class Todo
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

5.2. ViewModel 정의

public class TodoViewModel : INotifyPropertyChanged
{
    private ObservableCollection _todos;
    public ObservableCollection Todos
    {
        get { return _todos; }
        set
        {
            _todos = value;
            OnPropertyChanged(nameof(Todos));
        }
    }

    public TodoViewModel()
    {
        Todos = new ObservableCollection();
    }

    public void AddTodo(string title)
    {
        Todos.Add(new Todo { Title = title, IsCompleted = false });
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

5.3. 유닛 테스트 클래스 정의

public class TodoViewModelTests
{
    [Fact]
    public void AddTodo_ShouldAddTodoToCollection()
    {
        // Arrange
        var viewModel = new TodoViewModel();
        string todoTitle = "Test Todo";

        // Act
        viewModel.AddTodo(todoTitle);

        // Assert
        Assert.Single(viewModel.Todos);
        Assert.Equal(todoTitle, viewModel.Todos.First().Title);
    }
}

6. Mocking을 통한 테스트

Mocking을 사용하면 외부 의존성이 있는 객체를 대체하여 테스트를 실행할 수 있습니다. 예를 들어, 데이터베이스 연결이나 외부 API 호출 등을 Mock으로 대체하여 테스트 환경을 구성할 수 있습니다.

6.1. Repository Pattern과 Mocking

Todo List 앱에서 Repository Pattern을 사용하여 데이터를 관리한다고 가정해보겠습니다. Todo 데이터를 관리하는 ITodoRepository 인터페이스를 정의하겠습니다.

public interface ITodoRepository
{
    void Add(Todo todo);
    IEnumerable GetAll();
}

6.2. ViewModel을 Mocking할 경우

public class TodoViewModelWithMockedRepo
{
    private readonly ITodoRepository _todoRepository;
    private ObservableCollection _todos;

    public ObservableCollection Todos
    {
        get { return _todos; }
        private set
        {
            _todos = value;
            OnPropertyChanged(nameof(Todos));
        }
    }

    public TodoViewModelWithMockedRepo(ITodoRepository todoRepository)
    {
        _todoRepository = todoRepository;
        Todos = new ObservableCollection(_todoRepository.GetAll());
    }

    public void AddTodo(string title)
    {
        var todo = new Todo { Title = title, IsCompleted = false };
        _todoRepository.Add(todo);
        Todos.Add(todo);
    }
}

6.3. Mocking을 이용한 유닛 테스트

public class TodoViewModelWithRepoTests
{
    [Fact]
    public void AddTodo_ShouldCallAddOnRepo()
    {
        // Arrange
        var mockRepo = new Mock();
        var viewModel = new TodoViewModelWithMockedRepo(mockRepo.Object);
        string todoTitle = "Mocked Todo";

        // Act
        viewModel.AddTodo(todoTitle);

        // Assert
        mockRepo.Verify(repo => repo.Add(It.IsAny()), Times.Once);
    }
}

7. 결론

여기까지 WPF 애플리케이션에서 MVVM 패턴을 활용한 유닛 테스트와 Mocking을 통한 Dependency Injection 기반의 테스트 환경 구축 방법에 대해 알아보았습니다. 이러한 패턴과 기술을 사용하면 코드의 유지 관리성과 테스트 용이성을 극대화할 수 있습니다. 특히, WPF가 제공하는 데이터 바인딩 기능과 결합하여 MVVM 구조를 통해 고품질의 소프트웨어를 개발할 수 있습니다.

8. 참고 자료