WireMock详细解析(二) —— Xcode中基于WireMock和UI Tests的本地API调用(二)

版本记录

版本号 时间
V1.0 2019.08.12 星期一

前言

WireMock是基于HTTP的API的模拟器,下面我们就一起来学习和了解。感兴趣的看下面几篇文章。
1. WireMock详细解析(一) —— Xcode中基于WireMock和UI Tests的本地API调用(一)

源码

1. Swift

首先看下工程组织结构

WireMock详细解析(二) —— Xcode中基于WireMock和UI Tests的本地API调用(二)_第1张图片

下面就是源码了

1. StartupUtils.swift
import Foundation

struct StartupUtils {
  static func shouldRunLocal() -> Bool {
    return CommandLine.arguments.contains("-runlocal")
  }
}
2. CharacterListTableViewController.swift
import UIKit

class CharacterListTableViewController: UITableViewController {
  var networkClient = StarWarsAPINetworklClient()
  var viewModel: CharacterListViewModel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.accessibilityIdentifier = AccessibilityIdentifiers.characterListTable
    viewModel = CharacterListViewModel(networkClient: networkClient,
                                       delegate: self)
    viewModel.requestCharacterList()
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard
      let viewController = segue.destination as? CharacterDetailViewController,
      let selectedIndexPath = tableView.indexPathForSelectedRow
      else {
        fatalError("Incorrect destination viewcontroller or selectedIndexPath received")
    }
    
    let character = viewModel.character(for: selectedIndexPath)
    viewController.character = character
  }
}

// MARK: - UITableViewDataSource
extension CharacterListTableViewController {
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return viewModel.numberOfCharacters()
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "characterCell", for: indexPath) as? CharacterTableViewCell else {
      fatalError("Invalid cell type received")
    }
    
    let character = viewModel.character(for: indexPath)
    cell.configure(with: character)
    
    return cell
  }
}

extension CharacterListTableViewController: CharacterListViewModelDelegate {
  func characterListWasUpdated(withCharacters characters: [StarWarsCharacter]) {
    tableView.reloadData()
  }
}
3. CharacterDetailViewController.swift
import UIKit

class CharacterDetailViewController: UIViewController {
  @IBOutlet var nameLabel: UILabel!
  @IBOutlet var hairColorLabel: UILabel!
  @IBOutlet var eyeColorLabel: UILabel!
  @IBOutlet var birthYearLabel: UILabel!
  
  var character: StarWarsCharacter!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    nameLabel.text = character.name
    hairColorLabel.text = character.hairColor
    eyeColorLabel.text = character.eyeColor
    birthYearLabel.text = character.birthYear
    
    setupAccessibilityIdentifiers()
  }
  
  func setupAccessibilityIdentifiers() {
    nameLabel.accessibilityIdentifier = AccessibilityIdentifiers.characterDetailNameLabel
    hairColorLabel.accessibilityIdentifier = AccessibilityIdentifiers.characterDetailHairColorLabel
    eyeColorLabel.accessibilityIdentifier = AccessibilityIdentifiers.characterDetailEyeColorLabel
    birthYearLabel.accessibilityIdentifier = AccessibilityIdentifiers.characterDetailBirthYearLabel
  }
}
4. CharacterListViewModel.swift
import Foundation

protocol CharacterListViewModelDelegate: AnyObject {
  func characterListWasUpdated(withCharacters characters: [StarWarsCharacter])
}

class CharacterListViewModel {
  weak var delegate: CharacterListViewModelDelegate?
  
  private var characterList: [StarWarsCharacter] = []
  private var networkClient: StarWarsAPINetworklClient
  
  init(networkClient: StarWarsAPINetworklClient,
       delegate: CharacterListViewModelDelegate) {
    self.networkClient = networkClient
    self.delegate = delegate
  }
  
  func requestCharacterList() {
    networkClient.requestAllCharacters { [weak self] characters in
      guard let self = self else {
        return
      }
      
      self.characterList = characters
      
      DispatchQueue.main.async {
        self.delegate?.characterListWasUpdated(withCharacters: characters)
      }
    }
  }
  
  func numberOfCharacters() -> Int {
    return characterList.count
  }
  
  func character(for indexPath: IndexPath) -> StarWarsCharacter {
    return characterList[indexPath.row]
  }
}
5. CharacterTableViewCell.swift
import UIKit

class CharacterTableViewCell: UITableViewCell {
  @IBOutlet var nameLabel: UILabel!
  
  func configure(with character: StarWarsCharacter) {
    nameLabel.text = character.name
    nameLabel.accessibilityIdentifier = AccessibilityIdentifiers.characterCellNameLabel
    accessibilityLabel = AccessibilityIdentifiers.characterCellIdentifier(forCharacterName: character.name)
  }
}
6. StarWarsCharacter.swift
struct StarWarsCharacter: Codable {
  let name: String
  let hairColor: String
  let eyeColor: String
  let birthYear: String
}
7. CharacterListResponse.swift
import Foundation

struct CharacterListResponse: Codable {
  let count: Int
  let results: [StarWarsCharacter]
}
8. StarWarsAPINetworkClient.swift
import Foundation

class StarWarsAPINetworklClient {
  let defaultSession = URLSession(configuration: .default)
  
  var dataTask: URLSessionDataTask?
  var baseURLString: String {
    if StartupUtils.shouldRunLocal() {
      return "http://localhost:9999/swapi.co/api/"
    } else {
      return "https://swapi.co/api/"
    }
  }
  
  func requestAllCharacters(completion: @escaping ([StarWarsCharacter]) -> Void) {
    dataTask?.cancel()
    let url = baseURL()
    let listURL = url.appendingPathComponent("people")
    
    dataTask = defaultSession.dataTask(with: listURL, completionHandler: {(data, response, error) in
      guard
        error == nil,
        let data = data
        else {
          completion([])
          return
      }
      
      do {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
        let characterResponse = try jsonDecoder.decode(CharacterListResponse.self, from: data)
        completion(characterResponse.results)
      } catch {
        completion([])
      }
    })
    
    dataTask?.resume()
  }
  
  func baseURL() -> URL {
    guard let url = URL(string: baseURLString) else {
      fatalError("Invalid url generation")
    }
    
    return url
  }
}
9. AccessibilityIdentifiers.swift
enum AccessibilityIdentifiers {
  // MARK: - Character List
  static let characterListTable = "characterListTable"
  static let characterCellPrefix = "characterCell"
  static let characterCellNameLabel = "characterCellNameLabel"
  
  // MARK: - Character Detail
  static let characterDetailNameLabel = "characterDetailNameLabel"
  static let characterDetailHairColorLabel = "characterDetailHairColorLabel"
  static let characterDetailEyeColorLabel = "characterDetailEyeColorLabel"
  static let characterDetailBirthYearLabel = "characterDetailBirthYearLabel"
  
  static func characterCellIdentifier(forCharacterName name: String) -> String {
    return "\(AccessibilityIdentifiers.characterCellPrefix) \(name)"
  }
}
10. AccessibilityLabels.swift
import Foundation

enum AccessibilityLabels {
  static let characterDetailBackButton = "Characters"
}
11. StarWarsInfoUITests.swift
import XCTest

class StarWarsInfoUITests: XCTestCase {
  func testCharacterList() {
    let app = XCUIApplication()
    app.launchArguments = LaunchArguments.launchLocalArguments
    app.launch()
    
    let table = app.tables[AccessibilityIdentifiers.characterListTable]
    waitForElementToAppear(table, file: #file, line: #line, elementName: "Character List Table", timeout: 5.0)
    
    let identifiers = generateIdentifierList()
    identifiers.forEach { identifier in
      let cell = table.cells[identifier]
      XCTAssertTrue(cell.exists, "\(identifier) cell should be present")
    }
  }
  
  func testCellDetail() {
    let app = XCUIApplication()
    app.launchArguments = LaunchArguments.launchLocalArguments
    app.launch()
    
    let table = app.tables[AccessibilityIdentifiers.characterListTable]
    waitForElementToAppear(table, file: #file, line: #line, elementName: "Character List Table")

    let identifier = "\(AccessibilityIdentifiers.characterCellPrefix) Luke Skywalker"
    let cell = table.cells[identifier]
    XCTAssertTrue(cell.exists, "\(identifier) cell should be present")
      
    cell.tap()
    
    let nameLabel = app.staticTexts[AccessibilityIdentifiers.characterDetailNameLabel]
    XCTAssertEqual(nameLabel.label, "Luke Skywalker", "Name label should match character")
    
    let hairColorLabel = app.staticTexts[AccessibilityIdentifiers.characterDetailHairColorLabel]
    XCTAssertEqual(hairColorLabel.label, "blond", "Hair Color label should match character")
    
    let eyeColorLabel = app.staticTexts[AccessibilityIdentifiers.characterDetailEyeColorLabel]
    XCTAssertEqual(eyeColorLabel.label, "blue", "Eye Color label should match character")
    
    let birthYearLabel = app.staticTexts[AccessibilityIdentifiers.characterDetailBirthYearLabel]
    XCTAssertEqual(birthYearLabel.label, "19BBY", "Name label should match character")
    
    let backButton = app.buttons[AccessibilityLabels.characterDetailBackButton]
    XCTAssertTrue(backButton.exists, "Back button should be present")
    backButton.tap()
    
    waitForElementToAppear(table, file: #file, line: #line, elementName: "Character List Table")
  }

  func generateIdentifierList() -> [String] {
    let names = [
      "Luke Skywalker",
      "C-3PO",
      "R2-D2"
    ]
    
    let identifiers = names.map { name in
      return "\(AccessibilityIdentifiers.characterCellPrefix) \(name)"
    }
    
    return identifiers
  }
}

12. XCTestCase+Helpers.swift
import XCTest

extension XCTestCase {
  func waitForElementToAppear(_ element: XCUIElement,
                              file: StaticString,
                              line: UInt,
                              elementName: String,
                              timeout: TimeInterval = 5.0) {
    let predicate = NSPredicate(format: "exists == true")
    let existsExpectation = expectation(for: predicate,
                                        evaluatedWith: element,
                                        handler: nil)
    let result = XCTWaiter().wait(for: [existsExpectation],
                                  timeout: timeout)
    
    guard result == .completed else {
      let failureMessage = "\(elementName) should be present)"
      XCTFail(failureMessage, file: file, line: line)
      return
    }
  }
}
13. LaunchArguments.swift
import Foundation

struct LaunchArguments {
  static var launchLocalArguments: [String] {
    return [
      "-runlocal"
    ]
  }
}

后记

本篇主要讲述了Xcode中基于WireMock和UI Tests的本地API调用,感兴趣的给个赞或者关注~~~

WireMock详细解析(二) —— Xcode中基于WireMock和UI Tests的本地API调用(二)_第2张图片

你可能感兴趣的:(WireMock详细解析(二) —— Xcode中基于WireMock和UI Tests的本地API调用(二))