Let's TDD (2)

alwaysblu·2021년 5월 7일
0

출처

(Suyeol Jeon)

뷰 컨트롤러 테스트

SearchRepositoryViewController 테스트

잘 테스트되고 있는 서비스를 가지고 있다.

그 서비스를 뷰 컨트롤러에서 잘 쓰고 있는지를 테스트한다.

searchBarDidClick 관련된 Delegate 메소드가 호출되었을 때

잘 만들어 놓은 RepositoryService의 search라는 메소드를 잘 호출하는지를 테스트 해볼 것이다.

⭐ 이때 RepositoryService 대신에 테스트할 객체 RepositoryServiceStub을 만들어서 테스트한다.

⭐ RepositoryServiceProtocol을 만들어서 RepositoryServiceStub가 준수하게 하여 간접화시킨다.

"보일러 플레이트가 많습니다. 역시 스토리보드는 안좋은 것 같아요"


보일러 플레이트란?
컴퓨터 프로그래밍에서 보일러플레이트 또는 보일러플레이트 코드라고 부르는 것은
최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다.
즉 프로그래밍에서 상용구를 의미한다.


보일러 플레이트가 많은데 스토리보드가 안좋다는 건 무슨 말이지?
아래의 문구를 읽어보면 무슨 뜻인지 알 수 있다.
"보일러플레이트는 프로그래밍에서 상용구 코드를 말합니다.
어떤 일을 하기위해서 꼭 작성해야 하는 코드로서
자바에서는 클래스의 getter, setter 메소드를 말합니다.
자바에서 getter, setter는 꼭 필요하지만
코드의 길이를 길어지게 하고 개발자에게 의미없는 노동을 강요하게 됩니다."


즉, 스토리보드를 쓰므로서 의미없이 써야하는 코드가 길어지는 단점을 지적하는 것이다.

  1. SearchRepositoryViewController를 생성하여 viewController 상수에 저장한다.

  1. 뷰 컨트롤러에서 서치바를 찾고 delegate 메소드인
    searchBarSearchButtonClicked(searchBar: UISearchBar) 메소드를 호출할 것이다.

given은 주어진 환경이고 -> 생성한 뷰 컨트롤러 코드
when은 트리거이다. -> searchBarSearchButtonClicked 메서드
then은 트리거로 발생하는 이벤트를 의미한다. -> 4번

  1. 검색 버튼을 눌렀을 때 이미 만들어 놓은 서비스 메서드에 있는 특정 메서드가
    잘 호출되는지를 확인하면 된다.
    그러기 위해서는 1편에서 sessionManager의 stub(spy)을 만든 것처럼
    RepositoryService final 클래스의 stub(spy)을 만들어 볼 것이다.
final class RepositoryServiceStub {
}

아래 이미지와 같이 만든 RepositoryServiceStub 클래스의 인스턴스를 생성해서
viewController의 repositoryService에 삽입시켜준다.

  1. (then) 가짜 service의 searchParameters가 갖고 있는 keyword가
    저희가 설정한 키워드가 맞는지 비교한다.
    (리포지토리 검색 서비스이므로 검색 키워드를 비교하여 찾는 과정이다.)
XCTAssertEqual(self.repositoryService.searchParameters?.keyword, "여기에 비교할 키워드를 설정한다.")
  1. 4번에서 처럼 키워드를 비교하기 위해서는 final class RepositoryService에 RepositoryServiceProtocol이라는 프로토콜을 만들어서 한번 더 간접화를 시켜야 한다.
// LetsGitHubSearch/LetsGitHubSearch/Sources/Services/RepositoryService.swift
import Alamofire

protocol RepositoryServiceProtocol {
  @discardableResult
  func search(keyword: String, completionHandler: @escaping (Result<RepositorySearchResult>) -> Void) -> DataRequest
}

final class RepositoryService: RepositoryServiceProtocol {
  private let sessionManager: SessionManagerProtocol

  init(sessionManager: SessionManagerProtocol) {
    self.sessionManager = sessionManager
  }

  @discardableResult
  func search(keyword: String, completionHandler: @escaping (Result<RepositorySearchResult>) -> Void) -> DataRequest {
    let url = "https://api.github.com/search/repositories"
    let parameters: Parameters = ["q": keyword]
    return self.sessionManager.request(url, method: .get, parameters: parameters, encoding: URLEncoding(), headers: nil)
      .responseData { response in
        let decoder = JSONDecoder()
        let result = response.result.flatMap {
          try decoder.decode(RepositorySearchResult.self, from: $0)
        }
        completionHandler(result)
      }
  }
}
  1. 3번에서 생성한 RepositoryServiceStub 파이널 클래스에
    5번에서 만든 RepositoryServiceProtocol 프로토콜을 적용시킨다.
    RepositoryServiceStub의 변수로 searchParameters: String?을 만든다.
    이때 컴플리션 핸들러는 검증하지 않고 키워드 파라미터만 검증하는 걸로 하자.
    search 메소드가 호출되었을 때 searchParameters 변수에
    keyword 파라미터를 저장하는 방식을 사용해볼 것이다.
final class RepositoryServiceStub: RepositoryServiceProtocol{
	var searchParameters: String?
    
	@discardableResult
	func search(keyword: String, completionHandler: @escaping (Result<RepositorySearchResult>)) -> Void) -> DataRequest{
		self.searchParameters = keyword
		return DataRequest.init(session: URLSession(), requestTask: .data(nil, nil))
	}
}

@discardableResult 속성 조사하기!

  1. SearchRepositoryViewControllerTests.swift의 (when) 부분에
    viewController.searchController.searchBar.text = "Hello, world!"
    코드를 추가하고
    searchBarSearchButtonClicked라는 delegate 메소드가 호출되었을 때
    (then) 부분을 아래 XCTAssertEqual와 같이 바꿔서
    3번에서 삽입한 service의 searchParameters가 "Hello, world!" 가 맞는지 확인한다.
XCTAssertEqual(service.searchParameters, "Hello, world!")
  1. 3번에서 viewController를 만들 때는 service에 대한 콘크리트? 클래스(RepositoryStub 클래스의 인스턴스)를 주입받도록 만들어 놓았기 때문에
    아래 코드와 같이 SearchRepositoryViewController 클래스의 repositoryService 변수의 타입을
    RepositoryService 타입에서 RepositoryServiceProtocol 타입으로 바꿔준다.

concrete class :
모든 연산에 대한 구현을 가지고 있는 클래스를 concrete class 라고 할 수 있다.
추상 클래스가 아닌 클래스는 모두 concrete class라고 할 수 있다.
정의한 모든 연산에 대한 구현을 가지고 있는 완전한 클래스이므로
우리는 이 클래스의 인스턴스를 만들 수 있다.

Tip : 프로토콜 타입 변수 혹은 상수는 해당 프로토콜을 준수하는 타입의 어떤 인스턴스라도 할당할 수 있다.

class SearchRepositoryViewController: UIViewController {
	var repositoryService: RepositoryServiceProtocol!
    ...
}
@testable import Alamofire
@testable import LetsGitHubSearch

final class RepositoryServiceStub: RepositoryServiceProtocol {
  var searchParameters: (keyword: String, completionHandler: (Result<RepositorySearchResult>) -> Void)?

  @discardableResult
  func search(keyword: String, completionHandler: @escaping (Result<RepositorySearchResult>) -> Void) -> DataRequest {
    self.searchParameters = (keyword, completionHandler)
    return DataRequest(session: URLSession(), requestTask: .data(nil, nil))
  }
}
  1. 5번에서 만든 RepositoryServiceProtocol 프로토콜을 RepositoryService 클래스에 적용시킨다.

  2. 영상에서 스토리보드의 viewController의 인스턴스를 얻어올 때는
    해당 viewController의 identifier가 있어야하는데 이를 깜빡하면 테스트에 실패한다. 주의하자!
    얻어오고자하는 SearchRepositoryViewController의 identifier를 작성한다.

  1. 영상에서는 identifier를 작성하므로서 뷰컨트롤러는 정상적으로 얻어왔지만
    search 메소드가 호출이 안되서 테스트에 실패하는 문제가 발생하였다.
    breakpoint를 걸어 디버그한 결과
    searchBarSearchButtonClicked로 안들어오고 있다는 사실을 발견하였다.
    이유는 3번에서 RepositoryServiceStub 클래스의 인스턴스를 생성해서
    viewController에 삽입시켜주기 직전에 아래와 같은 코드를 작성하지 않았기 때문이라고 한다.
viewController.loadViewIfNeeded()

loadViewIfNeeded() 조사하기

  1. 테스트 코드에 통과하였다.

  2. 테스트 코드가 잘작성되었는지를 확인하기 위해서 잘못된 키워드를 넣어서 테스트에 실패하는지 확인해봐야한다. 영상에서 잘못된 키워드를 넣으면 테스트에 실패하게 된다.

  1. xcode에서 자체적으로 제공하는 코드 커버리지를 모으는 기능이 있다.
    xcode -> product -> Scheme -> Edit Scheme 에 들어가서
    왼쪽의 Test를 누르고 오른쪽에 Code Converage를 체크하고 테스트를 돌려보면
    실제로 테스트되는 동안 코드 라인마다 커버가 되었는지 안되었는지를 실제로 확인할 수 있다.

Code Coverage가 뭐지? 커버가 되다? 조사하기
코드 커버리지는 소프트웨어의 테스트 케이스가 얼마나 충족되었는지를 나타내는 지표 중 하나이다. 테스트를 진행하였을 때 ‘코드 자체가 얼마나 실행되었느냐’는 것이고,
이는 수치를 통해 확인할 수 있다.
코드 커버리지는 소스 코드를 기반으로 수행하는 화이트 박스 테스트를 통해 측정한다.

  1. 여기까지하면 뷰컨트롤러 테스트가 끝난다.

인디케이터 테스트

처음 로딩할 때 인디케이터가 도는 것의 상태가 반전되어 있는 상태였다.
그 반전되어 있는 인디케이터 상태에 대한 테스트를 작성하면서 인디케이터를 고쳐보도록 하자.

  1. 위에서 뷰컨트롤러를 테스트하기 위해 작성했던 testSearchBar_search() 메소드를 복사해서 붙여 놓은 후 이 함수의 이름을 testActivityIndicator_animating_whenLoading()으로 변경하자.

  2. 검색중일 때 Activity indicator가 로딩되는 것에 대한 테스트를 해보자.

현재 Activity indicator가 정상적으로 작동하고 있지 않기 때문에 아래와 같은 테스트는 실패해야한다.

testActivityIndicator_animating_whenLoading() 함수 안의
복사 붙여넣기로 인해서 then에 있는 기존의 키워드 파라메터를 위한 테스트 코드를
아래 코드와 같이 바꿔주자.

XCTAssertEqual(
	viewController.activityIndicatorView.isAnimating, 
    true
)
  1. viewController로 돌아가서 잘못된 코드를 찾아보자.

위의 setLoading 메서드에 잘못된 코드가 들어있다.
stopAnimating()과 startAnimating()을 바꾸면 정상적인 코드가 된다.

이제 테스트를 하면 테스트에 통과한다.

  1. 하지만 아래 그림과 같이 코드를 작성해도 테스트에 통과하게 된다.

왜냐하면 위의 이미지에서 위에 있는 빨간색 네모 코드에 대한 테스트만 2번에서 작성하였기 때문이다.

이제 아래있는 빨간색 네모 코드에 대한 테스트 코드도 작성을 해보도록 하자.

  1. 두번째 빨간색 네모 코드에 대한 테스트 코드를 아래와 같이 작성하자.
    위에서 작성한
    testActivityIndicator_animating_whenLoading() 함수를 복사해서 붙여넣은 후
    함수 이름을 testActivityIndicator_notAnimating_whenNotLoading()으로 변경하고
    then 부분의 코드를 기존의 true에서 false로 바꿔주자.
XCTAssertEqual(
	viewController.activityIndicatorView.isAnimating, 
    false
)
  1. 5번에 작성한 테스트 코드를 테스트하는 방법은 아래와 같다.
    이 로딩이 끝났다는 것은 결국 service 메서드에서 컴플리션 핸들러가 호출이 되었다는 것을 의미한다.
    그렇기 때문에 이 컴플리션 핸들러를 테스트 코드에서 직접 호출하는 방법을 사용해볼 것이다.

컴플리션 핸들러를 직접 호출하기 위해서는 컴플리션 핸들러의 참조를 가지고 있어야 한다.

  1. 컴플리션 핸들러의 참조를 갖고 있게 하기위해서 RepositoryServiceStub 클래스의 searchParameters를 아래 이미지와 같은 튜플 형식으로 변경해준다.

  1. searchParameters를 튜플 형식으로 인터페이스를 변경했으므로 키워드를 검증하는 코드를 아래와 같이 바꿔준다.
XCTAssertEqual(service.searchParameters?.keyword, "Hello, world!")
  1. testActivityIndicator_notAnimating_whenNotLoading() 함수의 when부분에
    animating 중이 아닐 때 의도적으로 컴플리션 핸들러를 직접호출을 시켜두기 위한 코드를 작성한다.
    이때 Result 타입의 값이 중요한게 아니라 그냥 컴플리션 핸들러가 호출이 되었다는 것이 중요하다.
    (Result 타입의 값으로 failure를 지정한다.
    failure안의 NSError(domain: "", code: 0, userInfo: nil) 값은
    테스트를 위한 의미없는 값이다.)
service.searchParameters?.completionHandler(
	.failure(NSError(domain: "", code: 0, userInfo: nil))
)
  1. 9번까지 작성한 후에 테스트를 하면 실패해야한다.
    왜냐하면 아직 로딩이 끝난 후의 코드를 startAninmating()으로 해놓았기 때문이다.

  1. 이제 이 로딩이 끝난 후의 코드를 startAninmating() 에서 stopAninmating()으로 변경하자 그리고 테스트를 하면 테스트에 통과하게 된다.

중간 결론

이 11번까지의 과정이 뷰 테스트이다.
뷰를 테스트하는 것은 눈에 보이는 것을 테스트하는 것이기 때문에
많은 사람들이 막연하게 생각하고 어려워한다.
설마 layout도 테스트해야하는지 물어본다.(그래서??? 해야함????)
뷰를 테스트할 때는 layout을 테스트할 때와는 별개로
상태에 따라 뷰의 속성이 변하는 경우에만 그 속성을 테스트하면 된다.
이 예시가 바로
현재의 로딩 상태에 따라
activityIndicator가 애니메션 중인지 아닌지를 나타내는 속성이 달라지는 것이다.

이와 같은 테스트 코드를 먼저 작성해보면 뷰를 테스트하는 것에 대한 감을 잡을 수 있을 것이라고 한다.

추가적으로 setLoading함수에서 self.tableView.isHisdden이라는 속성이
현재 로딩중인지 아닌지의 상태에 따라 변하는 것을 테스트해보면
뷰 테스트를 하는 감을 잡는데 도움이 될 수 있을 거라고 한다.

커버리지 확인

search에 관한 테스트만 작성했음에도 벌써
SearchRepositoryViewController.swft 파일의 커버리지가
81.1%인 것을 확인할 수 있다.

만약 여기에 테이블 뷰 셀이 잘 맞는 데이터를 랜더링하는지와 같은 테스트까지 모두 작성하면 커버리지가 100%가 나온다고 한다. 시간관계상 테이블뷰 테스트를 작성하는 것은 생략한다고 한다.

렌더링(rendering) : 렌더링(Rendering)은 컴퓨터 프로그램을 사용하여 모델(또는 이들을 모아놓은 장면인 씬(scene) 파일)로부터 완성된 화면이나 영상을 만들어내는 과정을 말한다.

전체 코드

코드 출처

테스트될 구현 코드

//LetsGitHubSearch/LetsGitHubSearch/Sources/ViewController/SearchRepositoryViewController.swift

import UIKit

import Alamofire

class SearchRepositoryViewController: UIViewController {
  struct Dependency {
    let repositoryService: RepositoryServiceProtocol!
    let urlOpener: URLOpenerProtocol!
    let firebaseAnalytics: FirebaseAnalyticsProtocol.Type!
  }
  var dependency: Dependency!

  let searchController = UISearchController(searchResultsController: nil)

  @IBOutlet private(set) var tableView: UITableView!
  @IBOutlet private(set) var activityIndicatorView: UIActivityIndicatorView!

  var currentSearchRequest: DataRequest?
  var repositories: [Repository] = []

  override func viewDidLoad() {
    super.viewDidLoad()
    self.definesPresentationContext = true

    self.searchController.searchBar.delegate = self
    self.searchController.searchBar.autocapitalizationType = .none

    self.navigationItem.searchController = self.searchController
    self.navigationItem.hidesSearchBarWhenScrolling = false
  }
}

extension SearchRepositoryViewController: UISearchBarDelegate {
  func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    if let text = searchBar.text {
      self.search(keyword: text)
      self.dependency.firebaseAnalytics.logEvent("search", parameters: ["keyword": text])
    }
    self.searchController.dismiss(animated: true, completion: nil)
  }

  private func search(keyword: String) {
    self.cancelPreviousSearchRequest()
    self.setLoading(true)

    self.currentSearchRequest = self.dependency.repositoryService.search(keyword: keyword) { [weak self] result in
      guard let self = self else { return }
      self.setLoading(false)

      switch result {
      case let .success(searchResult):
        self.setSearchResult(searchResult)

      case let .failure(error):
        self.showErrorAlert(error: error)
      }
    }
  }

  private func cancelPreviousSearchRequest() {
    self.currentSearchRequest?.cancel()
  }

  private func setLoading(_ isLoading: Bool) {
    if isLoading {
      self.activityIndicatorView.startAnimating()
      self.tableView.isHidden = true
    } else {
      self.activityIndicatorView.stopAnimating()
      self.tableView.isHidden = false
    }
  }

  private func setSearchResult(_ searchResult: RepositorySearchResult) {
    self.repositories = searchResult.items
    self.tableView.reloadData()
  }

  private func showErrorAlert(error: Error) {
    let alertController = UIAlertController(title: "⚠️", message: error.localizedDescription, preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self.present(alertController, animated: true, completion: nil)
  }
}

extension SearchRepositoryViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.repositories.count
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let repository = self.repositories[indexPath.row]
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
    cell.textLabel?.text = repository.fullName
    cell.detailTextLabel?.text = self.formattedStargazersCount(repository.stargazersCount)
    return cell
  }

  private func formattedStargazersCount(_ count: Int) -> String? {
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    guard let formattedCount = formatter.string(from: count as NSNumber) else { return nil }
    return "⭐️ \(formattedCount)"
  }
}

extension SearchRepositoryViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let repository = self.repositories[indexPath.row]
    let urlString = "https://github.com/\(repository.fullName)"
    guard let url = URL(string: urlString) else { return }
    self.dependency.urlOpener.open(url, options: [:], completionHandler: nil)
  }
}

테스트할 테스트 코드

//LetsGitHubSearch/LetsGitHubSearchTests/Sources/ViewControllers/SearchRepositoryViewControllerTests.swift

import XCTest
@testable import LetsGitHubSearch

final class SearchRepositoryViewControllerTests: XCTestCase {
  private var repositoryService: RepositoryServiceStub!
  private var urlOpener: URLOpenerStub!
  private var firebaseAnalytics: FirebaseAnalyticsStub.Type!
  private var viewController: SearchRepositoryViewController!

  override func setUp() {
    super.setUp()
    self.repositoryService = RepositoryServiceStub()
    self.urlOpener = URLOpenerStub()
    self.firebaseAnalytics = FirebaseAnalyticsStub.self
    self.firebaseAnalytics.logEventParameters = nil

    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let identifier = "SearchRepositoryViewController"
    self.viewController = storyboard.instantiateViewController(withIdentifier: identifier) as? SearchRepositoryViewController
    self.viewController.dependency = .init(
      repositoryService: self.repositoryService,
      urlOpener: self.urlOpener,
      firebaseAnalytics: self.firebaseAnalytics
    )
    self.viewController.loadViewIfNeeded()
  }

  func testSearchBar_whenSearchBarSearchButtonClicked_searchWithText() {
    // when
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // then
    XCTAssertEqual(self.repositoryService.searchParameters?.keyword, "ReactorKit")
  }

  func testSearchBar_whenSearchBarSearchButtonClicked_logAnalyticsSearchEvent() {
    // when
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "Let'Swift 18"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // then
    let parameters = self.firebaseAnalytics.logEventParameters
    XCTAssertEqual(parameters?.name, "search")
    XCTAssertEqual(parameters?.parameters as? [String: String], ["keyword": "Let'Swift 18"])
  }

  func testActivityIndicatorView_isAnimating_whileSearching() {
    // when
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // then
    XCTAssertTrue(self.viewController.activityIndicatorView.isAnimating)
  }

  func testActivityIndicatorView_isNotAnimating_afterSearching() {
    // given
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // when
    self.repositoryService.searchParameters?.completionHandler(.failure(TestError()))

    // then
    XCTAssertFalse(self.viewController.activityIndicatorView.isAnimating)
  }

  func testTableView_isHidden_whileSearching() {
    // when
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // then
    XCTAssertTrue(self.viewController.tableView.isHidden)
  }

  func testTableView_isVisible_afterSearching() {
    // given
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // when
    self.repositoryService.searchParameters?.completionHandler(.failure(TestError()))

    // then
    XCTAssertFalse(self.viewController.tableView.isHidden)
  }

  func testTableView_configureRepositoryCell_afterSearching() {
    // given
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    // when
    let repositories = [
      Repository(name: "ReactorKit1", fullName: "devxoul/ReactorKit1", stargazersCount: 1289),
      Repository(name: "ReactorKit2", fullName: "younatics/ReactorKit2", stargazersCount: 987),
      Repository(name: "ReactorKit3", fullName: "cruisediary/ReactorKit3", stargazersCount: 543),
    ]
    let searchResult = RepositorySearchResult(totalCount: 3, items: repositories)
    self.repositoryService.searchParameters?.completionHandler(.success(searchResult))

    // then
    let numberOfRows = self.viewController.tableView.numberOfRows(inSection: 0)
    XCTAssertEqual(numberOfRows, 3)

    let cell0 = self.viewController.tableView.cellForRow(at: IndexPath(row: 0, section: 0))
    XCTAssertEqual(cell0?.textLabel?.text, "devxoul/ReactorKit1")
    XCTAssertEqual(cell0?.detailTextLabel?.text?.contains("1,289"), true)

    let cell1 = self.viewController.tableView.cellForRow(at: IndexPath(row: 1, section: 0))
    XCTAssertEqual(cell1?.textLabel?.text, "younatics/ReactorKit2")
    XCTAssertEqual(cell1?.detailTextLabel?.text?.contains("987"), true)

    let cell2 = self.viewController.tableView.cellForRow(at: IndexPath(row: 2, section: 0))
    XCTAssertEqual(cell2?.textLabel?.text, "cruisediary/ReactorKit3")
    XCTAssertEqual(cell2?.detailTextLabel?.text?.contains("543"), true)
  }

  func testTableView_openRepositoryWebPage_whenSelectItem() {
    // given
    let searchBar = self.viewController.searchController.searchBar
    searchBar.text = "ReactorKit"
    searchBar.delegate?.searchBarSearchButtonClicked?(searchBar)

    let repositories = [
      Repository(name: "ReactorKit1", fullName: "devxoul/ReactorKit1", stargazersCount: 1289),
      Repository(name: "ReactorKit2", fullName: "younatics/ReactorKit2", stargazersCount: 987),
      Repository(name: "ReactorKit3", fullName: "cruisediary/ReactorKit3", stargazersCount: 543),
    ]
    let searchResult = RepositorySearchResult(totalCount: 3, items: repositories)
    self.repositoryService.searchParameters?.completionHandler(.success(searchResult))

    // when
    let tableView = self.viewController.tableView!
    tableView.delegate?.tableView?(tableView, didSelectRowAt: IndexPath(row: 0, section: 0))

    // then
    XCTAssertEqual(self.urlOpener.openParameters?.absoluteString, "https://github.com/devxoul/ReactorKit1")
  }
}

0개의 댓글