728x90
SMALL
🧩 1️⃣ DiaryStore vs HomeViewModel 테스트의 차이
| 구분 | 대상 | 테스트 목적 | 테스트 방식 |
| DiaryStoreTests | 데이터 레이어 (SSOT) | Core Data와 Store의 CRUD 동작 검증 | 실제 DB 쓰기/읽기 테스트 |
| HomeViewModelTests | 뷰모델 레이어 (UI 로직) | ViewModel이 Store 데이터를 올바르게 가공하고 반영하는지 검증 |
Store를 Mock으로 대체하여 테스트 |
즉,
- DiaryStoreTests는 데이터 저장이 진짜로 되는가? 를 검증
- HomeViewModelTests는 데이터가 화면에 맞게 잘 보여지는가? 를 검증
두 테스트는 서로 다른 책임을 테스트합니다.
🧠 2️⃣ 왜 HomeViewModel도 테스트해야 할까?
이유는 간단해요.
ViewModel은 “비즈니스 로직”을 담당합니다.
즉, 단순히 데이터를 보여주는 게 아니라
- 주간 감정 비율 계산
- 최근 5개의 일기만 노출
- 이미지 로드 후 Combine 반영
이런 “가공” 과정을 수행하죠.
그래서 테스트해야 하는 건 이런 부분들입니다 👇
| 테스트 항목 | 기대 결과 |
| recentDiaries | 항상 최신순으로 최대 5개까지만 들어있는가 |
| emotionSummary | countByEmotion이 올바르게 계산되는가 |
| loadDiaryImages() | 이미지 리스트가 정상적으로 채워지는가 |
| Combine 구독 (observeStore) | store의 변경이 즉시 반영되는가 |
🧩 3️⃣ 하지만! CoreData를 직접 쓰면 테스트가 느려져요 ⚠️
그래서 여기서 등장하는 게 Mock Store입니다 🎭
Mock은 “진짜 데이터베이스를 쓰지 않고, 가짜 데이터를 흉내 내는 객체”예요.
즉, HomeViewModel이 DiaryStore를 의존하지 않고,
DiaryProviding 프로토콜만 알고 있기 때문에
우리는 아래처럼 간단히 가짜 Store를 주입할 수 있습니다 👇
🧪 4️⃣ 예시: HomeViewModelTests.swift
import XCTest
import Combine
@testable import LemonLog
// MARK: - Mock Store
@MainActor
final class MockDiaryStore: DiaryProviding {
var diariesPublisher: AnyPublisher<[EmotionDiaryModel], Never> {
Just(mockDiaries).eraseToAnyPublisher()
}
var snapshot: [EmotionDiaryModel] { mockDiaries }
var mockDiaries: [EmotionDiaryModel] = []
var mockImages: [(UIImage?, String)] = []
func diary(with id: String) -> EmotionDiaryModel? {
mockDiaries.first { $0.id.uuidString == id }
}
func diaries(inWeekOf date: Date) -> [EmotionDiaryModel] { mockDiaries }
func countByEmotion(inWeekOf date: Date) -> [EmotionCategory: Int] {
Dictionary(grouping: mockDiaries) { EmotionCategory(rawValue: $0.emotion) ?? .happy_grade_1 }
.mapValues(\.count)
}
func fetchFirstImages() async -> [(UIImage?, String)] {
mockImages
}
}
// MARK: - HomeViewModel Test
@MainActor
final class HomeViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
func testRecentDiariesLimitToFive() {
// Given
let mockStore = MockDiaryStore()
mockStore.mockDiaries = (1...10).map {
EmotionDiaryModel(
id: UUID(),
emotion: "happy_grade_1",
content: "테스트 \($0)",
createdAt: Date().addingTimeInterval(-Double($0) * 100)
)
}
// When
let viewModel = HomeViewModel(store: mockStore)
// Then
XCTAssertEqual(viewModel.recentDiaries.count, 5)
}
func testEmotionSummaryCountsProperly() {
let mockStore = MockDiaryStore()
mockStore.mockDiaries = [
EmotionDiaryModel(id: UUID(), emotion: "happy_grade_1", content: "", createdAt: Date()),
EmotionDiaryModel(id: UUID(), emotion: "sad_grade_1", content: "", createdAt: Date()),
EmotionDiaryModel(id: UUID(), emotion: "happy_grade_1", content: "", createdAt: Date())
]
let viewModel = HomeViewModel(store: mockStore)
XCTAssertEqual(viewModel.emotionSummary[.happy_grade_1], 2)
XCTAssertEqual(viewModel.emotionSummary[.sad_grade_1], 1)
}
}
🧠 5️⃣ 이렇게 하면 좋은 이유
| 이유 | 설명 |
| ✅ 빠르다 | Core Data 접근 없이 테스트 → 즉시 실행 |
| ✅ 안정적이다 | DB에 의존하지 않으므로 테스트가 실패해도 환경 문제 아님 |
| ✅ 독립적이다 | ViewModel 로직만 검증 가능 |
| ✅ 재사용 가능 | 다른 ViewModel 테스트에도 Mock 주입 가능 |
🧩 6️⃣ 결론
✅ HomeViewModel도 XCTest로 테스트해야 합니다.
하지만 Core Data를 직접 쓰는 게 아니라,
DiaryProviding 프로토콜 기반으로 Mock Store를 주입해서 테스트하는 게 정석이에요.
728x90
LIST
'감정일기(가칭)' 카테고리의 다른 글
| 초보자도 쉽게 이해할 수 있는 단위 테스트의 기본 구조 - HomeViewModelTests (0) | 2025.10.24 |
|---|---|
| 처음 보는 사람도 다음엔 혼자 Mock을 만들 수 있게...설계가이드 (0) | 2025.10.24 |
| 🧩 Swift 6에서 @MainActor와 Singleton(.shared) 접근 시 발생하는 오류 해결하기 (0) | 2025.10.24 |
| 🍋 DiaryStore 이후의 ViewModel 설계 — 화면 단위로 나누는 이유와 구조 (0) | 2025.10.24 |
| eraseToAnyPublisher()와 AnyPublisher<[EmotionDiaryModel], Never>)은 Combine에서 가장 헷갈리지만 정말 중요한 개념 (0) | 2025.10.24 |