• 테스트 피라미드

    • Unit Test
    • Integration
    • End-to-End
  • Testing network requests

    • 네트워크 요청하는 부분은 4가지로 나눌 수 있음 → 코드를 잘게 쪼개야 테스트 하기 좋다.
      • request 생성 - 그대로 테스트 가능
      • 요청 후 데이터 받아옴
      • 받아온 데이터를 파싱 - 그대로 테스트 가능
      • 데이터 반영
    • URLSession을 어떻게 테스트 할까?
      • 여기서는 URLProtocol을 Mocking한다.
    class MockURLProtocol: URLProtocol {
    	static var requestHandler:((URLRequest) throws -> (HTTPURLResponse, Data))?
    	override class func canInit(with request: URLRequest) -> Bool {
    		return true
    	}
    
    	override class func canonicalRequest(for request: URLRequest) -> URLRequest {
    		return request
    	}
    
    	override func startLoading() {
    		guard let handler = MockURLProtocol.requestHandler else {
    			XCTFail("Received unexpected request with no handler set")
    			return
    		}
    		
    		do {
    				let (response, data) = try handler(request)
    
    				client?.urlProtocol(self, didReceive: cacheStoragePolicy: .notAllowed)
    				client?.urlProtocol(self, didLoad: data)
    				client?.urlProtocolDidFinishLoading(self)
    		} catch {
    				cliend?.urlProtocol(self, didFailWithError: error)
    		}
    	}
    
    	override func stopLoading() {
    		// startLoading과 비슷하다고만 하고 넘어감
    	}
    }
    
    • 이제 테스트를 작성한다.
    class APILoaderTests: XCTestCase {
    	var loader: APIRequestLoader<PointOfInterestRequest>!
    
    	override func setUp() {
    		let request = PointsOfInterestRequest()
    
    		let configuration = URLSessionConfiguration.ephemeral
    		configuration.protocolClasses = [MockURLProtocol.self]
    		let urlSession = URLSession(configuration: configuration)
    		
    		loader = APIRequestLoader(apiRequest: request, urlSession: urlSession)
    	}
    
    	func testLoaderSuccess() {
    		let inputCoordinate = CLLocationCoordinate2D(latitude: 37.3293, longtitude: -121.8893)
    		let mockJSONData = "[{\\"name\\":\\"MyPointOfInterest\\"}]".data(using: .utf8)!
    		
    		MockURLProtocol.requestHandler = { request in
    			XCTAssertEqual(request.url?.query?.contains("lat=37.3293"), true)
    			return (HTTPURLResponse(), mockJSONData)
    		}
    
    		let expectation = XCTestExpectation(description: "response")
    		loader.loadAPIRequest(requestData: inputCoordinate) { pointOfInterest, error in
    			XCTAssertEqual(pointsOfInterest, [PointOfInterest(name: "MyPointOfInterest")])
    			expectation.fulfill()
    		}
    	
    		wait(for: [expectation], timeout: 1)
    	}
    
    
    • end-to-end 테스트 할 때는 실제 서버가 아니라 mock 서버를 넣는 것도 고려해볼만 하다.
    • 아니면 유닛테스팅 번들에서 UI업데이트 하기 전까지만 하던가
  • Working with Notification

    • 테스트 대상이 올바르게 Notification을 받는지 테스트 해야 한다.
    • 한번에 하나씩만 테스트 할 수 있도록 격리해야 된다.
      • Notification은 1대 다 연결이기 때문에 특히 주의해야 한다.
    • 개별적인 NotificationCenter를 가지도록 하고, 이를 주입받는 식으로 구현해야 한다. → NotificationCenter.default를 다 바꿔라.
    • Notification을 제대로 보내는지 테스트 하려면 어떻게?
      • 개별 Center까지는 동일
      • XCTNSNotificationExpectation을 사용할 수 있다.
    class CurrentLocationProviderTests: XCTestCase {
    	func testNotifyAuthChanged() {
    		let notificationCenter = NotificationCenter()
    		let poster = CurrentLocationProvider(notificationCenter: notificationCenter) // 기본 인자로 default를 넘기면 프로덕션에도 영향이 없어진다.
    
    		let name = CurrentLocationProvider.authChangeNotification
    		let expectation = XCTNSNotificationExpectation(name: name,
    																									object: poster,
    																									notificationCenter: notificationCenter)
    
    		poster.notifyAuthChanged()
    		wait(for: [expectation], timeout: 0)
    	}
    }
    
  • Mocking with Protocols

    • 필요성
      • 클래스는 앱 내의 다른 클래스나 다른 SDK의 클래스들과 소통한다.
      • SDK클래스들은 직접 만들기 어려운 경우가 많다
      • 델리게이트 프로토콜은 테스트를 더 어렵게 한다.
      • 해결법: 외부 클래스의 인터페이스를 프로토콜로 모킹하자.
    • 서브 클래스를 이용한 모킹도 가능은 하지만, 리스크가 있다.
      • 일부 클래스는 서브클래스가 불가능하도록 되어 있다.
      • 메소드 오버라이드를 까먹기도 한다.
    • 프로토콜로 외부 인터페이스를 감싸놓고 쓴다
  • Test execution speed

    • 시간 관련한 코드들은 실제로 기다리게 만들면 너무 느리다.
    • 즉, 이를 위해서는 딜레이 매커니즘 등을 모킹해서, 스케쥴된 작업을 바로 실행해야 한다.
    class FeaturedPlaceManagerTests: XCTestCase {
    	func testScheduleNextPlace() {
    		var timerScheduler = MockTimeScheduler()
    		var timerDelay = TimeInterval(0)
    		timerScheduler.handleAddTimer = { timer 
    			timerDelay = timer.fireDate.timeIntervalSinceNow
    			timer.fire()
    		}
    		
    		let manager = FeaturedPlaceMananger(timerScheduler: timerScheduler)
    		let beforePlace = manager.currentPlace
    		manager.scheduleNextPlace()
    		
    		XCTAssertEqual(timerDelay, 10, accuracy: 1)
    		XCTAssertNotEqual(manager.currentPlace, beforePlace)
    	}
    }
    
    • Expectation 사용에 주의하라
      • predicate 기반의 XCTNSPredicateExpectation은 느리고 UI 테스트에 적합하다
      • 유닛 테스트에서는 더 빠르고 콜백 기반인 Expectation들을 사용하는 게 좋다.
        • XCTestExpectation
        • XCTNSNotificationExpectation
        • XCTKVOExpectation
    • 호스팅 앱이 있는 경우, 앱 런칭이 끝나야지만 테스트가 시작된다.
      • 앱 실행 속도를 빠르게 하는 게 좋다.
      • 테스트용 스킴을 만드는 것도 고려해보자