iOS에서 Core Data를 사용할 때 가장 중요한 건
데이터의 CRUD(Create, Read, Update, Delete) 기능이
실제로 제대로 동작하는지 확인하는 것.
이번 포스팅에서는 XCTest 환경에서 Core Data의 CRUD 동작을
자동으로 검증하는 테스트 매니저(DiaryTestManager) 구조를 정리했다.
✅ 1. DiaryTestManager의 역할
DiaryTestManager는 Core Data의 저장 / 조회 / 수정 / 삭제 로직을 자동으로 검증하는 테스트 전용 매니저 클래스다.
테스트 환경에서 각 CRUD 메서드를 한 번에 실행하고 결과를 로그로 확인할 수 있다.
@MainActor
final class DiaryTestManager {
static let sharded = DiaryTestManager()
let container: NSPersistentContainer
private init() {
container = NSPersistentContainer(name: "LemonLog")
container.loadPersistentStores { _, error in
if let error = error {
LogManager.print(.error, "영구저장소에서 불러오기 실패")
}
}
}
}
📌 핵심 포인트
- NSPersistentContainer를 통해 Core Data 스택을 초기화한다.
- 실제 앱에서 사용하는 LemonLog 데이터 모델을 그대로 로드하므로,
실제 Persistent Store(SQLite 기반) 에 CRUD 결과가 반영된다.
- 즉, 테스트는 실제 Core Data 환경에서 수행된다.
🎨 2. 더미 데이터 생성 로직
테스트를 위해 실제 사용자 입력 없이도 데이터를 자동으로 생성해야 한다.
이를 위해 두 가지 도우미 함수를 준비했다.
(1) 색상 기반 더미 이미지 생성
private func dummyImage(_ color: UIColor) -> UIImage {
let size = CGSize(width: 100, height: 100)
UIGraphicsBeginImageContext(size)
color.setFill()
UIRectFill(CGRect(origin: .zero, size: size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndPDFContext()
return image ?? UIImage()
}
- 단순히 색상만 채운 UIImage를 생성한다.
- 실제 사진 대신 색상 블록으로 구성된 이미지 배열을 저장 테스트에 사용.
(2) 랜덤 감정 / 색상 생성
private func randomEmotion() -> EmotionCategory {
EmotionCategory.allCases.randomElement() ?? .angry_grade_1
}
private func randomColor() -> UIColor {
let colors: [UIColor] = [.systemYellow, .systemPink, .systemBlue, .systemGreen, .systemOrange, .systemRed]
return colors.randomElement() ?? .systemGray
}
- 감정(EmotionCategory)을 랜덤으로 선택
- 각 이미지 색상도 랜덤하게 설정하여 더미 데이터 다양성 확보
🧩 3. CRUD 테스트 구조
테스트는 다음 순서로 수행된다:
1️⃣ 저장 (Create)
2️⃣ 전체 불러오기 (Read All)
3️⃣ 감정으로 검색 (Read by Filter)
4️⃣ 키워드 검색 (Read by Keyword)
5️⃣ 최신 일기 조회 (Read Latest)
6️⃣ 수정 (Update)
7️⃣ 삭제 (Delete)
8️⃣ 스토어 초기화 (Clear)
✅ (1) 저장 테스트
func testSaveDiary() {
let emotionType = randomEmotion()
let dummy = EmotionDiaryModel(
id: UUID(),
emotion: emotionType.rawValue,
content: "오늘의 감정은 \(emotionType.rawValue) 🤔",
createdAt: Date(),
images: [dummyImage(randomColor()), dummyImage(randomColor())]
)
let success = coreDataManager.saveDiary(dummy)
LogManager.print(success ? .success : .error,
"✅ [SAVE TEST] \(emotionType.rawValue) 감정일기 저장 \(success ? "성공" : "실패")")
}
→ 더미 데이터를 만들어 Core Data에 저장.
→ 저장 성공 여부를 로그로 확인.
✅ (2) 전체 조회 테스트
func testFetchAllDiaries() {
let diaries = coreDataManager.fetchDiaries(mode: .all)
LogManager.print(.info, "✅ [FETCH TEST] 총 \(diaries.count)개의 감정일기 조회됨")
}
→ 전체 데이터를 가져와 개수 및 내용 확인.
✅ (3) 감정별 / 키워드 검색
func testFetchByEmotion() {
let randomType = randomEmotion()
let result = coreDataManager.fetchDiaries(by: randomType.rawValue)
LogManager.print(.info, "✅ [SEARCH EMOTION] \(randomType.rawValue) 감정일기 \(result.count)개")
}
→ 특정 감정으로 필터링된 fetch 결과를 확인.
✅ (4) 수정 테스트
func testUpdateDiary() {
guard var diary = coreDataManager.fetchDiaries(mode: .all).first else {
LogManager.print(.warning, "⚠️ [UPDATE TEST] 수정할 데이터가 없습니다.")
return
}
diary.content = "수정된 내용입니다 ✏️"
diary.images?.append(dummyImage(randomColor()))
let success = coreDataManager.updateDiary(diary)
LogManager.print(success ? .success : .error,
"✅ [UPDATE TEST] 수정 \(success ? "성공" : "실패")")
}
→ 첫 번째 일기의 내용을 수정하고,
→ 새 이미지를 추가하여 업데이트가 정상 반영되는지 확인.
✅ (5) 삭제 테스트
func testDeleteDiary() {
guard let first = coreDataManager.fetchDiaries(mode: .all).first else {
LogManager.print(.warning, "⚠️ [DELETE TEST] 삭제할 데이터가 없습니다.")
return
}
let success = coreDataManager.deleteDiary(by: first.id.uuidString)
LogManager.print(success ? .success : .error,
"🗑️ [DELETE TEST] 삭제 \(success ? "성공" : "실패")")
}
→ 첫 번째 일기를 삭제하고,
→ 이후 fetch 결과에서 사라졌는지 검증 가능.
💾 4. 더미 데이터의 저장 위치
이 부분이 핵심이다.
우리가 사용하는 NSPersistentContainer(name: "LemonLog")는
앱의 실제 Core Data Persistent Store (SQLite 기반) 을 로드한다.
즉, 테스트 중 저장되는 더미 데이터는 실제 LemonLog.sqlite 파일에 들어간다.
✅ 하지만 테스트 종료 후 clearAllData()로 완전 삭제 처리 가능.
func clearAllData() {
let coordinator = container.persistentStoreCoordinator
for store in coordinator.persistentStores {
do {
try coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: nil)
LogManager.print(.success, "모든 메세지 삭제 완료 \(coordinator.persistentStores.count)")
} catch {
print("❌ Failed to clear store: \(error)")
}
}
}
이 메서드는 테스트가 끝난 뒤 실제 SQLite 파일까지 완전히 파괴(destroy) 하므로,
더미 데이터가 남지 않는다.
즉,
- 테스트 중에는 실제 Persistent Store에 CRUD가 이루어지고
- 테스트 후에는 destroyPersistentStore()로 전체 삭제되어
- 데이터 오염 없이 실제 환경 검증이 가능하다 ✅
🚀 5. 전체 테스트 실행
모든 CRUD를 순서대로 수행하고 마지막에 데이터를 정리하는 통합 테스트 함수.
func runAllTests() {
print("🚀 ==== LemonLog Core Data 테스트 시작 ====")
testSaveDiary()
testFetchAllDiaries()
testFetchByEmotion()
testSearchByKeyword()
testFetchLatestDiary()
testUpdateDiary()
testDeleteDiary()
clearAllData()
print("✅ ==== 모든 테스트 완료 ====")
}
이 함수를 XCTest에서 한 번 실행하면
저장 → 조회 → 수정 → 삭제 → 정리까지 한 번에 검증된다.
🧾 6. 정리 요약
| 구분 | 설명 |
| 테스트 대상 | Core Data CRUD 동작 전체 |
| 저장소 타입 | 실제 Persistent Store (SQLite) |
| 더미 데이터 생성 | 색상 기반 이미지 + 랜덤 감정 |
| 테스트 순서 | 저장 → 조회 → 수정 → 삭제 → 초기화 |
| 테스트 후 정리 | destroyPersistentStore()로 완전 삭제 |
| 장점 | 실제 환경 기반 테스트 + 데이터 오염 방지 |
📘 결론:
이번 구조는 단순히 Core Data 로직이 “돌아간다” 수준이 아니라,
실제 Persistent Store에 CRUD가 정확히 반영되는지까지 통합적으로 검증한다.
또한 테스트 후 destroyPersistentStore()로 완전히 초기화하므로
“더미 데이터가 남는 문제” 없이 안전한 실전 검증이 가능하다.
import Foundation
import CoreData
import UIKit
@testable import LemonLog
@MainActor
final class DiaryTestManager {
// MARK: ✅ SingleTon
static let sharded = DiaryTestManager()
let container: NSPersistentContainer
private init() {
container = NSPersistentContainer(name: "LemonLog")
container.loadPersistentStores { _, error in
if error != nil {
LogManager.print(.error, "영구저장소에서 불러오기 실패")
}
}
}
// MARK: ✅ Property
private let coreDataManager = DiaryCoreDataManager.shared
// MARK: ✅ 더미 이미지 색성 (색상 기반)
private func dummyImage(_ color: UIColor) -> UIImage {
let size = CGSize(width: 100, height: 100)
UIGraphicsBeginImageContext(size)
color.setFill()
UIRectFill(CGRect(origin: .zero, size: size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndPDFContext()
return image ?? UIImage()
}
// MARK: ✅ 랜덤 감정 생성
private func randomEmotion() -> EmotionCategory {
EmotionCategory.allCases.randomElement() ?? .angry_grade_1
}
// MARK: ✅ 랜덤 색상 (이미지용)
private func randomColor() -> UIColor {
let colors: [UIColor] = [ .systemYellow, .systemPink, .systemBlue, .systemGreen, .systemOrange, .systemRed]
return colors.randomElement() ?? .systemGray
}
// MARK: ✅ 1. 저장 테스트
func testSaveDiary() {
let emotionType = randomEmotion()
let dummy = EmotionDiaryModel(
id: UUID(),
emotion: emotionType.rawValue,
content: "오늘의 감정은 \(emotionType.rawValue) 🤔",
createdAt: Date(),
images: [
dummyImage(randomColor()),
dummyImage(randomColor())
]
)
let success = coreDataManager.saveDiary(dummy)
LogManager.print(success ? .success : .error,
"✅ [SAVE TEST] \(emotionType.rawValue) 감정일기 저장 \(success ? "성공" : "실패")")
}
// MARK: ✅ 2. 전체 조회 테스트
func testFetchAllDiaries() {
let diaries = coreDataManager.fetchDiaries(mode: .all)
LogManager.print(.info, "✅ [FETCH TEST] 총 \(diaries.count)개의 감정일기 조회됨")
diaries.forEach {
LogManager.print(.info, " ▶️ \($0.emotion): \($0.content)")
}
}
// MARK: ✅ 3. 특정 감정 검색 테스트
func testFetchByEmotion() {
let randomType = randomEmotion()
let result = coreDataManager.fetchDiaries(by: randomType.rawValue)
LogManager.print(.info, "✅ [SEARCH EMOTION] \(randomType.rawValue) 감정일기 \(result.count)개")
}
// MARK: ✅ 4. 키워드 검색 테스트
func testSearchByKeyword() {
let keyword = "angry_grade_1"
let result = coreDataManager.fetchDiaries(by: keyword)
LogManager.print(.info, "✅ [SEARCH KEYWORD] '\(keyword)' 결과 \(result.count)개")
}
// MARK: ✅ 5. 최신 일기 테스트
func testFetchLatestDiary() {
if let latest = coreDataManager.fetchLatestDiary() {
LogManager.print(.success, "✅ [LATEST] 최근일기: \(latest.emotion) / \(latest.content)")
} else {
LogManager.print(.warning, "⚠️ [LATEST] 최근 일기를 찾을 수 없습니다.")
}
}
// MARK: ✅ 6. 수정 테스트
func testUpdateDiary() {
guard var diary = coreDataManager.fetchDiaries(mode: .all).first else {
LogManager.print(.warning, "⚠️ [UPDATE TEST] 수정할 데이터가 없습니다.")
return
}
diary.content = "수정된 내용입니다 ✏️"
diary.images?.append(dummyImage(randomColor()))
let success = coreDataManager.updateDiary(diary)
LogManager.print(success ? .success : .error,
"✅ [UPDATE TEST] 수정 \(success ? "성공" : "실패")")
}
// MARK: ✅ 7. 삭제 테스트
func testDeleteDiary() {
guard let first = coreDataManager.fetchDiaries(mode: .all).first else {
LogManager.print(.warning, "⚠️ [DELETE TEST] 삭제할 데이터가 없습니다.")
return
}
let success = coreDataManager.deleteDiary(by: first.id.uuidString)
LogManager.print(success ? .success : .error,
"🗑️ [DELETE TEST] 삭제 \(success ? "성공" : "실패")")
}
// MARK: ✅ 8. 더미데이터 삭제
func clearAllData() {
let coordinator = container.persistentStoreCoordinator
for store in coordinator.persistentStores {
do {
try coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: nil)
LogManager.print(.success, "모든 메세지 삭제 완료 \(coordinator.persistentStores.count)")
} catch {
print("❌ Failed to clear store: \(error)")
}
}
}
// MARK: 🧩 전체 테스트 실행
func runAllTests() {
print("🚀 ==== LemonLog Core Data 테스트 시작 ====")
testSaveDiary()
testFetchAllDiaries()
testFetchByEmotion()
testSearchByKeyword()
testFetchLatestDiary()
testUpdateDiary()
testDeleteDiary()
clearAllData()
print("✅ ==== 모든 테스트 완료 ====")
}
}'감정일기(가칭)' 카테고리의 다른 글
| 🧩 LemonLog Core Data 테스트 콘솔 로그 분석 (0) | 2025.10.21 |
|---|---|
| 🚀 Xcode에서 Core Data CRUD 테스트 실행하기 (0) | 2025.10.21 |
| 💥 Xcode 테스트 실행 시 “DSTROOT install style is not supported on this device.” 에러 해결법 (0) | 2025.10.20 |
| ⚙️ Core Data에서 catch가 작동하지 않는 이유 (0) | 2025.10.20 |
| 🧪 DiaryTestManager, 어디에 두는 게 맞을까? (0) | 2025.10.20 |