티스토리 뷰
모 부트캠프를 다니는 동안 테스트 용이하도록 코드를 작성하도록 하라는 설명도 많이 들었기도 하며 나를 더욱더 성장하기 위해 유닛 테스트를 해야겠다는 마음을 먹고 있었다
해보려고 시도도 몇번 해보았지만 어렵게 느껴지고 이해도 되지 않아 주줌 하기도 했지만 이번 개인 프로젝트에는 꼭 적용시켜보자는 의지와 이전에 참고한 글들을 다시 또 보고 조금은 이해가 되기도 했다
우선적으로 테스트를 진행하고 싶었던 것은 네트워크 테스트였다.
앱을 개발하면서 앱이 네트워크에 의존이 많은것을 느껴 네트워크의 작성된 코드가 중요하다 생각되기 때문에
실제 네트워크에 요청하지 않아야 하는 이유
- 실제 서버와 통신하게되면 단위 테스트의 속도가 느려질 뿐만 아니라 인터넷 연결에 의존하기 때문에 테스트를 신뢰할 수 없다
- 실제 서버와 통신하면 의도치 않은 결과를 불러올 수 있다
내 목표는 인터넷 연결에 의존적이지 않는 테스트를 원하였기에 인터넷에 의존하지 않고 Mock의 값을 반환하도록 만들 예정이다
기본적으로 통신하는 코드를 작성
struct JokerAPI: Decodable {
let value: Joker
}
struct Joker: Decodable {
let id: Int
let joke: String
}
enum NetworkError: Error {
case fail
}
struct JokerAPIProvider {
func requestJoker() -> AnyPublisher<JokerAPI, NetworkError> {
URLSession.shared.dataTaskPublisher(for: URL(string:"https://api.icndb.com/jokes/random")!)
.map{ $0.data }
.decode(type: JokerAPI.self, decoder: JSONDecoder())
.mapError { _ in
NetworkError.fail
}.eraseToAnyPublisher()
}
}
class ViewController: UIViewController {
var cancellable = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
JokerAPIProvider().requestJoker().sink { _ in
} receiveValue: { joker in
print(joker)
}.store(in: &cancellable)
}
}
결합이 강하게 되어 있는 상태, JokerAPIProvider는 URLSession에 의존적이다
생성자 주입을 추가 하고 프로토콜을 사용하여 URLSession 의존성을 끊는다
protocol JokerProtocol {
func requestJoker() -> AnyPublisher<JokerAPI, NetworkError>
}
protocol Requestable {
func request<T: Decodable>() -> AnyPublisher<T, NetworkError>
}
extension URLSession: Requestable {
func request<T: Decodable>() -> AnyPublisher<T, NetworkError> {
self.dataTaskPublisher(for: URL(string:"https://api.icndb.com/jokes/random")!)
.map{ $0.data }
.decode(type: T.self, decoder: JSONDecoder())
.mapError { _ in
NetworkError.fail
}.eraseToAnyPublisher()
}
}
struct JokerAPIProvider: JokerProtocol {
let session: Requestable
init(session: Requestable) {
self.session = session
}
func requestJoker() -> AnyPublisher<JokerAPI, NetworkError> {
return session.request()
}
}
JokerProtocol과 Requestable 프로토콜을 추가하고 JokerAPIProvider 상속 및 의존하도록 추가하였다
이로써 Requestable 채택한 객체를 주입할 수 있게 되었다
request메서드는 제네릭을 방출하기에 반환 값 지정이 필요한데 이것을 JokerProtocol을 이용하여 반환 값을 지정해줌으로 해당 값으로 반환하도록 만들었다
현재는 request 메서드에서 URL을 직접 넣어주었지만 매개변수로 넣어주도록 수정하면 다른 요청 URL로 변경이 가능하다
이제 Mock을 만들 차례
struct MockSession: Requestable {
func request<T: Decodable>() -> AnyPublisher<T, NetworkError> {
return Just(JokerAPI(value: Joker(id: 123123, joke: "jokeDummy")) as! T)
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
}
}
struct MockFailSession: Requestable {
func request<T: Decodable>() -> AnyPublisher<T, NetworkError> {
return Fail(error: NetworkError.fail).eraseToAnyPublisher()
}
}
Just객체를 이용해 테스트 할 JokerAPI 직접 만들어 두었고 아래는 실패 했을때의 결과를 만들어 두었다
Just의 경우 실패가 없기 때문에 <Output, Never> 가지고 있어 반환타입 에러는 NetworkError 이기때문에 setFailureType 통해
업스트림의 에러타입을 변경시켜줘야한다
Mock 작성하면서 한 가지 아쉬웠던 점은 request 메서드가 제네릭이기 때문에 as! T를 강제적으로 해줘야 했다는 점
이렇게 하는 게 맞는 것 일수도 있지만 강제적으로 한다는 게 나에겐 조금 불편해 보였다
이제 테스트를 해보도록 하자
var mockSession: Requestable!
var mockFailSession: Requestable!
var cancellable = Set<AnyCancellable>()
override func setUpWithError() throws {
self.mockSession = MockSession()
self.mockFailSession = MockFailSession()
}
override func tearDownWithError() throws {
self.mockSession = nil
self.mockFailSession = nil
}
func test_requestJoker() {
// given
let jokerAPIProvider = JokerAPIProvider(session: mockSession)
// when
jokerAPIProvider.requestJoker().sink { fail in
if case .failure(_) = fail {
XCTFail("잘못된 접근입니다")
}
} receiveValue: { result in
// then
XCTAssertEqual(123123, result.value.id)
XCTAssertEqual("jokeDummy", result.value.joke)
}.store(in: &cancellable)
}
func test_requestFailJoker() {
// given
let jokerAPIProvider = JokerAPIProvider(session: mockFailSession)
// when
jokerAPIProvider.requestJoker().sink { fail in
// then
if case .failure(let error) = fail {
XCTAssertEqual(NetworkError.fail, error)
}
} receiveValue: { result in
XCTFail("잘못된 접근입니다")
}.store(in: &cancellable)
}
작성한 Mock 값이 정상적으로 출력되는것을 확인 할 수 있다
requestJoker() 메서드에서 if case .failure(_) 을 작성한 이유는 sink의 cycle을 통해 문제가 발생했을땐 에러를 방출 하고 정상적으로 끝나면 finish를 방출하기 때문이다 if case .failure 없다면 정상적으로 불러온다고 하더라도 finish할때 XCTFail이 발생할수 있기 때문이다
이로써 실제로 쓰이는 JokerProvider에서는 실제서버를 통해 값을 가져오지만 Mock을 주입한 곳에서는 Mock으로 넣은 값이 나오는것을 볼 수 있어 우리가 원하는 네트워크 의존하지 않아도 결과를 불러오는 테스트를 작성하게 되었다.
URLSessionDataTask 클래스이므로 이 객체를 상속해서 Mock 만드는 예제들이 있다
하지만 Combine은 반환값인 DataTaskPublisher가 구조체이므로 DataTaskPublisher의 상속 할수도 따로 Mock을 만들기가 쉽지 않을것으로 생각된다 DataTaskPublisher 생성자가 init(URLRequset, URLSession) 정해진 매개변수가 있어 이것 또한 발목을 잡는다.
아직은 많이 부족한 테스트 실력이지만 지속적으로 작성해 나아가면서 올바른 테스트를 작성하도록 실력을 키워야겠다
'IOS' 카테고리의 다른 글
Combine MultipleAPI 병렬처리(MergeMany, ZipMany) (0) | 2022.02.14 |
---|---|
Swift Chart 라이브러리 BarChartView CornerRadius (0) | 2021.08.15 |
상단에 위치하는 커스텀 탭바 만들기 (1) | 2021.08.01 |