自動化されたテストはiOSアプリの開発において不可欠な部分であり、リグレッションへの対策とコードの効果的な開発手法を提供します。この記事では、iOSアプリのUIテストの効果的な書き方について探求します。
良いテストの構造
iOSアプリに新しい機能を開発する際、APIからローカルの天気データを取得し表示するボタンなどの新機能を追加する場合、良いテストの書き方とその構造が鍵となります。ユニットテストの書き方について2つのアプローチを考察しましょう。
クラスと関数に基づくテスト
1つのアプローチは、作成しようとしているクラスと関数に密接に関連するテストを書くことです。たとえば、APIリクエストを行うネットワークサービスを構築している場合、そのサービスに特化したテストを書くでしょう。ただし、この方法はテストを密接に結びつける可能性があり、コードの保守性とリファクタリングに支障をきたすことがあります。
NetworkService:
class NetworkService {
func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) inif let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
} else {
let unknownError = NSError(domain: "NetworkService", code: 0, userInfo: nil)
completion(.failure(unknownError))
}
}
task.resume()
}
}
これらのテストでは、さまざまなシナリオをカバーするためにUrlSessionをモックする必要があり、これを実現するには、テストでモックインスタンスを作成するための冗長なプロトコルまたはインターフェースを純粋に作成する必要があります。さらに重要なのは、この方法でテストをコードに密接に結びつけることです。これは大規模なアプリケーション全体にこのアプローチを適用した場合、各々が独自のテストを持ち、それらが予想通りに連携して機能することを確認するさまざまなテストが存在することを考えると、問題が生じる可能性があります。小さな変更を行った場合を想像してみてください。何の理由もなくテストが失敗するでしょう。アプリの動作に変更がない限り、なぜテストが失敗する必要があるのでしょうか?このアプローチに従うことは、リファクタリングを妨げ、コードベースの改善を難しくします。
振る舞いに基づくテスト
まず、「ハッピーパス」テストを書くことから始めましょう。これらのテストは、典型的な条件下でアプリが予想どおりに機能することを確認します:
ボタンが押され、晴れの天気データが返される場合、晴れの天気アイコンが表示されることを確認します.
ボタンが押され、雨の天気データが返される場合、雨の天気アイコンが表示されることを確認します.
これらの振る舞いに基づくテストは、エッジケースに立ち入ることなく、アプリケーション機能のほとんどを構築できるようにします。主要な機能が備わったら、より複雑なシナリオを探索できます:
ボタンが押され、APIエラーが発生した場合、エラーメッセージが表示されることを確認します.
ボタンが押され、APIがデータを返さない場合、友好的なデフォルトメッセージを表示します.
これらのテストは、アプリの内部実装の詳細に立ち入らず、予想される結果に焦点を当てています.
アプリの関数をインジェクトする
振る舞いに基づくテストを書くためには、副作用関数のインジェクションを可能にするアプリの構造が必要です。関数として依存関係をインジェクトすることで、コードの保守性とテストの可能性が向上します.
func buttonPressed(appF: AppFunctions) {
let weatherResponse = appF.fetchWeatherData()
if(weatherResponse.successful) {
// render weather
} else {
// show error message
}
}
例えば、fetchWeatherDataは外部APIを呼び出す副作用を持つ関数です。したがって、テスト用に操作できる機能をインジェクトしたいと考えます. これは「制御の反転」と呼ばれるクリーンコーディングの原則であり、私は関数型のスタイルで書くことを好むため、インジェクトする依存関係は関数です.
iOSでの依存性のインジェクションの実行方法
iOSでは、UIテストで依存性のインジェクションを実行することは、エントリーポイントの不足により難しい場合があります. 代わりに、テスト目的でクラスをJSONにエンコードし、AppDelegateでそれをインターセプトできます. iOSは関数の直接インジェクションをサポートしていないため、エンコーダブルフィールドを持つクラスをエンコードして関数の振る舞いをシミュレートできます. こういったことを実現する方法を以下に示します. AppFunctionsプロトコルを持っています:
protocol AppFunctions {
func fetchWeatherData() -> Repsonse
}
その後、本物のAppF:
class AppF: AppFunctions {
func fetchWeatherData() -> Repsonse {
// code that makes api call
return response
}
}
それから、レスポンスを設定できるテストバージョンがあります。これがTestAppF:
class TestAppF: AppFunctions, Codable {
private var response: Response? = nil
init(response: Response) {
self.response = response
}
func fetchWeatherData() -> Response {
return response!
}
}
テストでは、アプリを起動し、エンコードされたTestAppFをインジェクトします:
let testAppF = createTestF()
app.launchEnvironment["InjectedAppF"] = testAppF.json
app.launch()
ここで.jsonはTestAppFをJSON文字列にエンコードします:
extension Encodable {
var json: String? {
guard let data = data else {
return nil
}
return String(data: data, encoding: .utf8)
}
var data: Data? {
return try! JSONEncoder().encode(self)
}
}
その後、AppDelegateでJSONを取り出し、AppFunctionsにデコードします:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appF: AppFunctions?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
appF = ProcessInfo.processInfo.decodeAppF() ?? AppF()
return true
}
...
}
ここでdecodeAppFはProcessInfoの拡張です:
extension ProcessInfo {
func decodeAppF() -> TestAppF? {
guard
let environment = environment["InjectedAppF"],
let codable = TestAppF.decode(from: environment) else {
return nil
}
return codable
}
}
これらの原則に従うことで、iOSアプリのためにクリーンで振る舞いに焦点を当てたテストを書くことができ、コードの品質とテストの可能性を確保できます.
サポート方法
このコンテンツは常に無料で提供されます。価値があると感じた場合、他の人と共有を検討してください. さらに、私たちのゲームをダウンロードして正直なレビューを残すことは、私たちを大いにサポートします. 質問やフィードバックがある場合は、お気軽にお問い合わせください。できる限りお答えします.
今すぐApple App StoreからFalling Skyをダウンロード: https://apps.apple.com/app/id6446787964
このブログポストに感謝: https://medium.com/egym-developer/painless-ui-testing-in-ios-part-1-mocking-the-network-ffbd6ab4809a