iOS

[Swift] Swift와 Alamofire를 사용한 네트워크 요청의 단위 테스트(Unit Testing) 구현하기

GODOLs 2023. 10. 27. 21:54

안녕하세요, 오늘은 iOS 개발 과정에서 매우 중요한 부분인 테스트 코드 작성에 대해 이야기하려고 합니다. 개발을 하면서 제가 테스트 코드를 시작하게 된 가장 큰 이유는 '신뢰성 있는 앱 구축'과 '향후 유지보수의 용이성'을 보장받기 위해서였습니다. 특히, 네트워크 호출과 같이 외부 시스템과 상호작용하는 부분은 앱의 안정성을 크게 좌우하기 때문에 이를 검증하는 것이 필수적이라고 느꼈습니다.


 

1.테스트 코드의 필요성과 함수 설명

테스트 코드는 작성한 코드가 예상대로 동작하는지 확인하는 데 도움이 되며, 앱의 기능이 올바르게 동작하는지 확인하고 버그가 발생하지 않도록 예방합니다. 오늘 다루게 될 테스트는 Alamofire를 이용한 네트워크 요청에 초점을 맞추고 있습니다. 다음은 테스트 코드에서 사용할 주요 함수에 대한 간단한 소개입니다:

  1. setUpWithError() 함수:
    • 테스트 사이클 중에서 각 테스트 메서드가 호출되기 전에 실행됩니다. 여기서 테스트 케이스 시작 전에 필요한 설정을 할 수 있습니다.
  2. tearDownWithError() 함수:
    • 각 테스트 메서드가 실행된 후에 호출됩니다. 이 함수는 테스트 중에 사용된 리소스를 정리하는 데 사용됩니다.
  3. testFetchTestData() 함수:
    • 실제 테스트가 수행되는 부분입니다. 이 함수는 네트워크에서 데이터를 가져오는 비동기 호출을 테스트합니다. XCTestExpectation을 사용하여 비동기 작업을 기다리고, 결과가 성공인지 실패인지에 따라 테스트를 판단합니다.

2. 테스트 코드 실습 해보기

이제 실제로 테스트 코드를 작성해 볼 시간입니다. 이 테스트 코드는 Alamofire를 사용하여 서버로부터 데이터를 성공적으로 받아오는지 테스트합니다. 우리는 서버의 응답을 모방하여 실제 네트워크 요청을 보내지 않고도 테스트를 할 수 있습니다. 아래는 테스트 메서드의 예시입니다:

 

지난번 Moya + Alamofire + Combine을 구현한 예시를 위해 썼었던 API를 다시 한번 사용해보겠습니다.

 

  • 네트워크 Request 테스트를 할 NetworkManager를 만들었습니다.
import Foundation
import Alamofire
class NetworkManager {
    // 지난번에 Moya를 통해 구현했던 축구팀 API를 단순 AF Request를 구현했습니다.
    static func fetchTestData(completion: @escaping (Result<[TeamStat], Error>) -> Void) {
        // URL을 선정해줍니다
        var url = "https://apiv3.apifootball.com"
        // Parameter을 정합니다. 파라미터는 URLEncoding으로 정의 할 것입니다.
        let parameters: Parameters =
        ["action":"get_standings",
         "league_id":152,
         "APIkey":"fb3816f9f0f2efe99d043b02e3ba5ca263c2b8cda14e98afd7f8f76df4d7a129",
        ]
        AF.request(url, parameters: parameters, encoding: URLEncoding.default).response { response in
            switch response.result {
            case .success(let data):
                let responseData = try? JSONDecoder().decode([TeamStat].self, from: data!)
                completion(.success(responseData!))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}
//이전에 구현했던 구조체와 동일합니다.
struct TeamStat: Codable {
    let countryName: String
    let leagueId: String
    let leagueName: String
    let teamId: String
    let teamName: String
    let overallPromotion: String
    let overallLeaguePosition: String
  // ... 이하 생략
    enum CodingKeys: String, CodingKey {
        case countryName = "country_name"
        case leagueId = "league_id"
        case leagueName = "league_name"
        case teamId = "team_id"
        case teamName = "team_name"
        case overallPromotion = "overall_promotion"
        case overallLeaguePosition = "overall_league_position"
    // ... 이하 생략
    }
}

 

 

  • 오늘의 메인메뉴인 테스트코드 클래스입니다!

가장 눈여겨 보셔야할 부분은 XCTAssertEqual 이라는 부분입니다.

첫번째 인자두번째 인자가 일치해야지 테스트가 통과하는데요.

 

제가 변수 expectedTeam을 "Tottenham"으로 설정을 했습니다.

response값은 프리미어리그 순위 별로 들어오기 때문에 1등 ~ 20등 순으로 되어있습니다.

그렇다면 response의 첫번째 배열 즉 response[0] 의 팀 이름(teamName)은 토트넘이 되어야 통과가 될 것입니다. 

 

2023년 10월 27일 기준 프리미어리그 1등은 토트넘입니다. 캡틴쏘니 만세!

import XCTest
@testable import sample

final class sampleTests: XCTestCase {
    // 테스트 케이스를 넣으려고 하면 무조건 함수 앞에는 'test'를 넣어줘야합니다
    func test프리미어리그1등팀은토트넘이되어야한다() throws {
        // 예상치를 설정합니다. 저는 프리미어리그 1등팀을 찾기 위해 토트넘을 설정했습니다.
        let expectedTeam = "Tottenham"
        // 응답을 성공적으로 받았는지 확인하기 위한 expectation을 생성합니다.
        let expectation = self.expectation(description: "Fetching Data")
        
        NetworkManager.fetchTestData(completion: { result in
            switch result {
            case .success(let response):
                /** 서버로부터의 실제 응답과 예상치를 비교합니다. Response 값이 API 문서에서는 순위별로 내려주므로
                 * 배열의 0번째가 1등팀이 되겠네요
                 **/
                XCTAssertEqual(response[0].teamName, expectedTeam)
            case .failure(let error):
                // 에러 발생 시 테스트를 실패하게 합니다.
                XCTFail("Error was not expected: \(error)")
            }
            // expectation을 충족시키며, 이로 인해 대기가 해제됩니다.
            expectation.fulfill()
        })
        // 비동기 호출이 완료될 때까지 기다립니다. 타임아웃은 네트워크 상태에 따라 조정할 수 있습니다.
        waitForExpectations(timeout: 10) { (error) in
            if let error = error {
                XCTFail("Waiting for expectation failed: \(error)")
            }
        }
    }
}

XCTFail은 이미 실패를 예상했을 경우를 표시하는데요 인자는 에러메시지를 담습니다.

중요!!! test함수를 test case에 추가하기 위해서는 함수이름앞에 꼭 test를 붙이셔야 합니다.

3. 테스트 결과 확인

이제 Xcode에서 작성한 테스트 코드를 실행하고 결과를 확인해 봅시다.

  1. Xcode의 좌측 네비게이터에서 테스트 파일을 선택합니다.
  2. 테스트 메서드 옆의 다이아몬드 아이콘을 클릭하여 테스트를 실행합니다.
  3. 테스트가 완료되면 Xcode 하단의 'test navigator'에서 결과를 확인할 수 있습니다.

테스트에 성공했습니다. 현재 토트넘이 1등이네요!

테스트가 성공적으로 통과했다면, 우리의 네트워크 호출 코드가 올바르게 작동하고, 예상대로 응답을 처리하는 것을 의미합니다. 반면, 테스트가 실패하면 오류 메시지를 통해 어떤 부분이 잘못되었는지 파악하고 수정할 수 있습니다.

 

일부러 한번 오류를 내보겠습니다.  response[1] 2등팀 의 팀 이름(teamName)을 한번 가져오면 어떻게 되는지 보실까요?

 

2등은 Manchester City (맨시티)군요 토트넘과 결과같이 다르기 때문에 테스트는 실패했습니다 ㅠㅠ

오 Test Failed와 동시에 기존에 있던 Tottenham 값과 Manchester City 값을 비교하네요! 현 시점 2등은 맨시티이기 때문에 예상대로 Failed!!

 

 

4. 결론

단위 테스트는 코드의 신뢰성을 검증하고 앱의 안정성을 유지하는 데 필수적인 과정입니다. Swift와 Alamofire를 사용하는 현대적인 Swift 기반 앱 개발에서 테스트 코드의 역할은 더욱 중요해집니다. 이러한 테스트 과정을 통해 애플리케이션이 예상대로 동작하고, 향후 발생 가능한 문제를 사전에 예방할 수 있습니다. 지속적인 테스트를 통해 프로젝트의 건강을 유지하고 품질을 개선해 나가는 것이 좋습니다.

반응형