フューチャー技術ブログ

Swiftの自動テスト〜導入と基本的なテスト手法〜

はじめに

はじめまして、2023年3月キャリア入社、HealthCare Inovation Group(HIG)所属の寒河江です。
春の入門ブログ連載の17日目です。

本記事はSwiftでの自動テストについての入門記事です。SwiftはXcodeを用いてiOSアプリが作成でき、自作して手元の端末で動きが見やすいため初心者がモチベーション維持したまま開発できる良い題材かなと思います。「作って動いて楽しいなぁ」→「テストしてみよう!」→ 「楽にできる方法ないかな?」 と思った方に見ていただければです。

簡単なメモアプリを作成し、それに対して部分的にテストコードを書いてみたので早速紹介していきます。

作ったメモアプリ

https://github.com/SagaeKugo/CRUD

メモ動画.gif

XCTestのセットアップ

今回はプロジェクト作成後にテストを追加します。

Xcodeを開いてFile→New→Targetを選択し

対象となるテストを追加します。今回はUT/UIテストどちらも実施するためどちらも追加します。

テスト用のフォルダが作成され、TARGETにテストが追加されました。

UT対象

例のため、極端に簡素化した関数を作成してテストします

Utils.swift
import Foundation

// テスト用足し算関数
public func calcAdd(a: Int , b: Int) -> Int {
return a + b
}
// テスト引き算関数
public func calcSub(a: Int , b: Int) -> Int {
return a - b
}
// テスト用割り算関数
public func calcDiv(a: Int , b: Int) -> Int {
return a / b
}
// テスト用掛け算関数
public func calcMulti(a: Int , b: Int) -> Int {
return a * b
}

UTの実施(1)〜デフォルトで入っている関数の扱い〜

先ほどUnit Testing Bungleを選択して追加したCRUDTestsにテストコードを記載していきます。

CRUDTests.swift
import XCTest
@testable import CRUD

final class CRUDTests: XCTestCase {

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}

func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
// 足し算
func testCulcAdd() throws {
XCTAssertEqual(CRUD.calcAdd(a: 27 , b: 2) , 29)
XCTAssertEqual(CRUD.calcAdd(a: 2 , b: 2) , 4)
}

// 引き算
func testCulcSub() throws {
XCTAssertEqual(CRUD.calcSub(a: 27 , b: 2) , 25)
XCTAssertEqual(CRUD.calcSub(a: 2 , b: 22) , -20)
}

// 割り算
func testCulcDiv() throws {
XCTAssertEqual(CRUD.calcDiv(a: 27 , b: 2) , 13)
XCTAssertEqual(CRUD.calcDiv(a: 2 , b: 2) , 1)
}

// 掛け算
func testCulcMulti()throws {
XCTAssertEqual(CRUD.calcMulti(a: 27 , b: 2) , 54)
XCTAssertEqual(CRUD.calcMulti(a: 3 , b: 2) , 6)
}
}

デフォルトで4つのテスト関数が記載されていますが、これらはざっと以下の意味で、今回は変更する必要はないので無視します。

  • setUpWithError()
    • →各テストを実行する前に毎回呼ばれます。
  • tearDownWithError()`
    • →各テスト実行後に毎回呼ばれます。
  • testExample()
    • →テストコードのサンプルです。<test + テスト対象のメソッド名>
  • testPerformanceExample()
    • →パフォーマンステストのサンプルです。<testPerformance + テスト対象のメソッド名>

そのため、XCTestCaseをまとめてテストを回すと、

  1. setUpWithError()
  2. testExample()
  3. tearDownWithError()
  4. setUpWithError()
  5. testCulcAdd()
  6. tearDownWithError()

…という流れでテストが動きます。

UTの実施(2)〜関数の追加、テスト実行〜

@testable import CRUDと記載することでCRUDアプリ内のpublic,internalシンボルのテストが可能になります。

Sampleアプリを作成した場合は@testable import Sampleになりますね。

続いてテスト関数を作成します。

内容は特筆すべき部分はないと思うので省略(参考サイトは末尾に記載)しますが、テスト関数を作成する際は必ず先頭にtestと入れる必要があります。
先頭にtestと記載することでテストメソッドとして認識され、テストできる関数には左側に菱形マークがつきます。

それぞれの関数単位で菱形ボタンを押してテスト実行することも可能ですし、CRUDTestsクラスの菱形ボタンを押すことでクラス内のテストをまとめて実施することも可能です。

成功すると緑のチェックがつき、失敗した場合は赤くバツが出てくれます。

失敗した時のログはこちらから確認できます。

UIテストの実施(1)

UI Testing Bungleを選択して追加したCRUDUITestsにテストコードを記載していきます。

デフォルトでCRUDUITests.swiftCRUDUITestsLaunchTests.swiftの2ファイル作成されていますが、今回はCRUDUITests.swiftにテストを記載します。デフォルトで書かれているテスト関数はUTで紹介しているものと同じ役割なので省略します。

UIテストの実施(2)

今回テストしたのは下記の1関数のみです。

func testAddDelete() {
let app = XCUIApplication()
XCTContext.runActivity(named: "Launch app") { _ in
app.launch()
}
app.buttons["testAdd"].firstMatch.tap()
sleep(1)
app.buttons["testDelete"].firstMatch.tap()
}

testAddボタンを押下することでメモを10件登録し、testDeleteボタンを押下することでメモを全件削除しています。

ユーザーが操作せずに見た目が変わることだけを確認したいためメモ内容はチェック対象外としています。メモ内容をテストする場合は別途アサーションを追加しましょう。

let app = XCUIApplication()

まずはテスト対象のアプリXCUIApplication()のインスタンスを作成します。

XCTContext.runActivity(named: "Launch app") { _ in
app.launch()
}

今回の規模であれば不要ですが、XCTContext.runActivity(named: "~~")を使って長いテストメソッドを名前付きの小さなサブステップに分割できます。app.launch()はnamedに記載されている通り、アプリの起動を行っています。

app.buttons["testAdd"].firstMatch.tap()
sleep(1)
app.buttons["testDelete"].firstMatch.tap()

テスト対象のアプリ内のtestAddtestDeleteというラベルのついたボタンを検索し、最初に見つかったものをタップしています。複数同じ名前のボタンがある場合は、accessibilityIdentifierを設定することで分類できます。(参考サイトにリンク記載)

UIテスト動画

マウス操作しなくてもシミュレータ画面左上のtestAdd,testDeleteボタンが押下されています。

UItest動画

さいごに

長い記事になってしまいましたが、読んでくださった方ありがとうございます。

「はじめに」にも書きましたがiOSアプリは普段使う分実機テストもしやすく自分の欲しいものを作ることもできるので、初心者がモチベーションを維持しながら技術を身につけるにはいい題材だと思います。(私も初心者なので書いて遊んでます)

今回紹介できませんでしたが、CoreDataというのを使ってサーバーを立てなくてもアプリ上で扱うデータのCRUD処理が簡単にできたりと面倒ごとが少ないのも利点です。せっかく技術を身につけるなら動かして楽しんでスキルアップしていきましょう!!

次は斎藤賢太さんのJSパッケージ管理ツールpnpmの概要と内部構造を眺める です。

参考サイト