[TDD] 네트워크 클라이언트
Updated:
네트워크 클라이언트 static shared property
네트워크를 이용한 데이터 요청을 할 때마다 관련 클래스를 새로 생성하는 것은 네트워크 성능이나 기기의 배터리 상태 영향을 줄 수 있다.
이러한 비용을 줄이기 위해서 단 하나만 생성하여 사용할 수 있도록 하는 싱글톤 패턴을 이용하여 static shared property를 만들어 줄 수 있다.
class DogPatchClient {
let baseURL: URL
let session: URLSession
static let shared = DogPatchClient(
baseURL: URL(string: "https://dogpatchserver.herokuapp.com/api/v1")!,
session: URLSession.shared,
)
init(baseURL: URL,
session: URLSession) {
self.baseURL = baseURL
self.session = session
}
.
.
.
그리고 이 shared의 프러퍼티를 테스트한다.
class DogPatchClientTests: XCTestCase {
.
.
.
// shared가 가진 base url이 올바른지 테스트
func test_shared_setBaseURL() {
// given
let baseURL = URL(string: "https://dogpatchserver.herokuapp.com/api/v1")!
// then
XCTAssertEqual(DogPatchClient.shared.baseURL, baseURL)
}
// shared가 가진 session이 올바른지 테스트
func test_shared_setsSession() {
// given
let session = URLSession.shared
// then
XCTAssertEqual(DogPatchClient.shared.session, session)
}
.
.
.
네트워크 클라이언트의 데이터 요청 테스트하기
위에서 만든 DogPatchClient의 shared 프로퍼티를 바료 이용하여 테스트를 하면 다음과 같이 고려해야 할 점이 있다.
- 인터넷 연결이 필요한 실제 네트워크 통신을 하게 된다.
- 인터넷 연결이 되어 있지 않으면 네트워크 통신 관련 테스트는 불가능하게 된다.
- 인터넷 연결이 되어 네트워크 통신이 가능하게 되더라도 가변적인 데이터 값을 예측할 수 없다. 그러므로 테스트가 어렵다.
- 네트워크 데이터 요청을 통해 데이터를 받기 까지 소모되는 시간이 부담된다.
테스트를 할 때는 실제 앱에서 사용할 클래스를 가지고 사용하지 않고 mock 네트워크 클라이언트를 만들어야 한다. 그리고 다음과 같이 두가지 방법이 있다.
- 위에서 만들었던 네트워크 클라이언트 클래스를 상속해서 관련 메서드를 오버라이딩 한다. 이 방법은 특정 메서드를 실수로 오버라이딩하지 못할 경우 실제 네트워크 요청을 하게 될 가능성이 있다.
- 네트워크 클라이언트 protocol 을 정의하고 의존성을 주입해서 사용한다. 이렇게 하면 실제 네트워크 요청을 할 가능성을 없앨 수 있다.
protocol DogPatchService {
func getDogs(completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask
}
extension DogPatchClient: DogPatchService {
getDogs(completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask {
.
.
.
}
}
이제 테스트에 사용하게 될 mock 네트워크 클라이언트를 만든다. mock 네트워크 클라이언트는 앱 코드에 영향을 미치지 않고 테스트를 사용할 용도로만 사용하게 된다.
@testable import DogPatch
import Foundation
class MockDogPatchService: DogPatchService {
var getDogsCallCount = 0
var getDogsDataTask = URLSessionDataTask()
var getDogsCompletion: (([Dog]?, Error?) -> Void)!
func getDogs(completion: @escaping ([Dog]?, Error?) -> Void) -> URLSessionDataTask {
getDogsCallCount += 1
getDogsCompletion = completion
return getDogsDataTask
}
}
위에서 만든 Mock 네트워크 클라이언트 클래스를 테스트에 사용하는 예시는 다음과 같다.
class ListingsViewControllerTests: XCTestCase {
.
.
.
func test_refreshData_setsRequest() {
// given
mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
// when
sut.refreshData()
// then
XCTAssertEqual(sut.dataTask, mockNetworkClient.getDogsDataTask)
}
.
.
.
위의 refreshData 메서드는 유저가 테이블 뷰를 당겼을 때도 호출될 수 있기 때문에 refreshData가 중복되서 요청이 될 수가 있다. 이러한 불필요한 중복요청을 없애기 위한 테스트 코드를 작성해보면 다음과 같다.
class ListingsViewControllerTests: XCTestCase {
.
.
.
func test_refreshData_ifAlreadyRefreshing_doesntCallAgain() {
// given
givenMockNetworkClient()
// when
sut.refreshData()
sut.refreshData()
// then
XCTAssertEqual(mockNetworkClient.getDogsCallCount, 1) // 1번만 호출될 것을 예상
}
TDD 사이클 기준으로 위의 테스트 코드는 실패하기 때문에 앱코드로 돌아가서 테스트 코드가 성공할 수 있도록 코드를 수정해야한다.
class ListingsViewController: UIViewController {
.
.
.
// MARK: - Refresh
@objc func refreshData() {
guard dataTask == nil else { return } // 추가
dataTask = networkClient.getDogs { dogs, error in
self.dataTask = nil // 추가
self.tableView.reloadData()
}
}
tableView Mooking 하여 테스트하기
class ListingsViewControllerTests: XCTestCase {
.
.
.
func test_refreshData_givenDogsResponse_reloadsTableView() {
// given
givenMockNetworkClient()
let dogs = givenDogs()
// 1
class MockTableView: UITableView {
var calledReloadData = false
override func reloadData() {
calledReloadData = true
}
}
// 2
let mockTableView = MockTableView()
sut.tableView = mockTableView
// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)
// then
// 3
XCTAssertTrue(mockTableView.calledReloadData)
}
Leave a comment