iOS

[iOS] Unit Test 기본 개념과 적용

양밀루 2025. 6. 23. 20:59

✅ 단위 테스트(Unit Test)란?

  • 정의: 코드의 작은 단위(예: 함수, 메서드)가 예상대로 동작하는지 자동으로 확인하는 것
// 예를 들어, 이런 함수가 있다면
func add(a: Int, b: Int) -> Int {
    return a + b
}

// 테스트는 이런 식으로 검증
// "add(2, 3)을 호출하면 5가 나와야 한다"

 

 

✅ 테스트의 핵심 개념

1. Given-When-Then 패턴

모든 테스트는 이 3단계로 구성

func test_saveSummaryState_shouldSaveCorrectly() {
    // Given (준비): 테스트에 필요한 데이터 준비
    let bookTitle = "Harry Potter"
    let isExpanded = true
    
    // When (실행): 실제로 테스트할 동작
    repository.saveSummaryState(isExpanded, of: bookTitle)
    
    // Then (검증): 예상 결과와 비교
    let savedValue = repository.loadSummaryState(of: bookTitle)
    XCTAssertEqual(savedValue, isExpanded)
}

 

2. 주요 XCTAssert 메서드들

XCTAssertEqual(a, b)        // a == b
XCTAssertNotEqual(a, b)     // a != b
XCTAssertTrue(condition)    // condition이 true
XCTAssertFalse(condition)   // condition이 false
XCTAssertNil(value)         // value가 nil
XCTAssertNotNil(value)      // value가 nil이 아님

 

3. setUp과 tearDown

class MyTests: XCTestCase {
    var repository: UserDefaultsRepository!
    
    override func setUp() {
        super.setUp()
        // 각 테스트 실행 전 초기화
        repository = UserDefaultsRepositoryImp()
    }
    
    override func tearDown() {
        super.tearDown()
        // 각 테스트 실행 후 정리
        repository = nil
    }
}

 

✅ 적용해보기

 Xcode에서 테스트 파일 만들기

  1. File > New > Target
  2. iOS Unit Testing Bundle 선택
  3. UserDefaultsTests.swift 파일 생성

UserDefaults 예시

  • saveState(_:of:): 제대로 저장되었는가?
  • loadState(of:): 저장된 값을 제대로 불러오는가?
  • Mock 객체를 사용한 의존성 주입 테스트: UserDefaults.standard는 실제 저장소라 테스트 간 값이 섞일 수 있음
final class MockUserDefaultsRepository: UserDefaultsRepository {
    var savedStates: [String: Bool] = [:]
    
    func saveState(_ isExpanded: Bool, of bookTitle: String) {
        savedStates[bookTitle] = isExpanded
    }
    
    func loadState(of bookTitle: String) -> Bool {
        return savedStates[bookTitle] ?? false
    }
}
// 일부 예시 코드
final class BookViewModelTests: XCTestCase {
    var mockUserDefaults: MockUserDefaultsRepository!
    var mockBookRepository: MockBookRepository!
    var viewModel: BookViewModel!
    
    override func setUp() {
        super.setUp()
        mockUserDefaults = MockUserDefaultsRepository()
        mockBookRepository = MockBookRepository()
        viewModel = BookViewModel(
            bookRepository: mockBookRepository,
            userDefaultsRepository: mockUserDefaults
        )
    }
    
    func test_saveState_savesCorrectValue() {
        // Given
        mockBookRepository.booksToReturn = [
            Book(title: "HP1", author: "J.K", releaseDate: Date(), 
                 pages: 300, dedication: "", summary: "", chapters: [])
        ]
        viewModel.loadBooks()
        viewModel.selectBook(at: 0)
        
        // When
        viewModel.saveSummaryExpandedState(true)
        
        // Then
        XCTAssertEqual(mockUserDefaults.savedStates["HP1"], true)
    }
}

 

👉 tearDown() 없는 이유: Mock 객체들은 메모리상의 데이터라서 테스트 끝나면 자동으로 해제되기 때문에 불필요

 

 

프로토콜 + DI 구조로 설계한 덕분에 생각보다 어렵지 않게 시도해볼 수 있었다