【iOS】Swift Testingで単体テストの実装方法と#expect/#requireの使い方!

【iOS】Swift Testingで単体テストの実装方法と#expect/#requireの使い方!

この記事からわかること

  • XcodeSwift Testing使い方
  • 単体テスト実装方法
  • #expect/#require検証マクロの使い方

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

iOSアプリにおけるテスト

iOSアプリ開発においてテスト機能は統合開発環境であるXcodeに内包されています。その中でも「XCTest」と「Swift Testing」の2種類があり今回は「Swift Testing」の使い方についてまとめていきます。「XCTest」やその他以下のことは別記事でまとめているので参考にしてください。

XCTestとSwift Testingの違いや特徴は以下の記事が参考になりました。

おすすめ記事:Swiftの新テストライブラリ「swift-testing」特徴と導入

Swift Testing

公式リファレンス:Swift Testing

Swift Testing」は「XCTest」に変わるテスト機能です。以前からライブラリとして公開はされていましたがXcode16以降から標準で組み込まれるようになりました。既存でXCTest存在しても並行して動作させることができるため新規プロジェクトでなくてもこれから活用していけるようになっています。

GitHub:swiftlang / swift-testing

Swift Testingを導入する

新規プロジェクトにSwift Testingを導入したい場合は「Testing System」で「Swift Testing with XCTest UITests」を選択します。

【iOS】Swift Testingで単体テストの実装方法と使い方!

プロジェクト作成後からテストを導入するためにはXcodeの上部メニューから「File」 > 「New」 > 「Target...」をクリックし、Test内の「UI Testing Bundle」または「Unit Testing Bundle」を選択、「Testing System」を「Swift Testing」を選択し「NEXT」をクリックします。

【Swift/Xcode】XCTestの使い方!Unit Test(単体テスト)を作ろう

テスト関数の定義

公式リファレンス:Defining test functions

Swift Testingを導入するとプロジェクトに新しいターゲットとして追加されます。デフォルトでテスト用の構造体が記述されたファイルが生成されます。Swift Testingでは構造体structやアクターactorfinal classなどSwift6での並行性のチェックでも問題のないような設計で実装することが推奨されています。

import Testing

struct StructTest {
    @Test func example() { }
}

actor ActorTest {
    @Test func example() {}
}

この中に@Testアノテーショションを付与した関数を定義しテストしたい内容をその中に記述していきします。検証には#expect#requireなどのアサーションマクロ(※)を使用していきます。

※ マクロとはコンパイル時にコードを自動生成する機能です。#expectを使用することでテストの失敗を検知する処理として動作させることができます。

#expect

#expect

テストでの値の検証には#expectマクロを使用し、引数に検証したい結果を真偽値で渡します。テストメソッド内には検証したい数だけ#expectを記述することが可能で、失敗してもテストは終了せず、後続の処理が実行されます。

@Test func animalTest() {
    // 複数検証してもOK 
    let result = "Cat"
    print("------1")
    #expect(result == "Cat")
    print("------2")
    // 失敗(false)しても後続の処理は行われる
    #expect(result.contains("S")) // Expectation failed: (result → "Cat").contains("S" → S)
    print("------3")
    #expect(result.contains("C"))
    print("------4")
}

Test run started.Testing Library Version: 102 (arm64-apple-ios13.0-simulator)Suite SampleAppTests started.Test animalTest() started.
------1
------2Test animalTest() recorded an issue at ClipImageAppTests.swift:17:9: Expectation failed: (result → "Cat").contains("S"S)
------3
------4Test animalTest() failed after 0.001 seconds with 1 issue.Suite SampleAppTests failed after 0.001 seconds with 1 issue.Test run with 1 test failed after 0.002 seconds with 1 issue.

XCTestではXCTAssertEqualXCTAssertNotNilなど数多くの検証用メソッドが用意され、適切な場面で適切なメソッドを使用する必要がありましたが、Swift Testingなら#expect1つでまかなうことが可能になっているので学習コストが低く、初学者でも使いやすくなっています。

テストを実行するにはテストごとに「開始ボタン」が左側に表示されるのでクリック、またはCommand + Uで実行することができます。実行するとビルドでシミュレーターが立ち上がりテストが実行されデバッグログにテスト結果が出力されます。

Test run started.Testing Library Version: 102 (arm64-apple-ios13.0-simulator)Suite SampleAppTests started.Test animalTest() started.Test animalTest() passed after 0.001 seconds.Suite SampleAppTests passed after 0.001 seconds.Test run with 1 test passed after 0.001 seconds.
【Swift/Xcode】XCTestの使い方!Unit Test(単体テスト)を作ろう

#require

#require

似たような検証マクロに#requireがあります。基本的な使い方は#expectと同じですが失敗した場合にテストが終了し後続の処理を実行しない特徴があります。また値がnilの場合でも失敗扱いとなるので、意図しないnilを検知することも可能です。呼び出す際は例外ExpectationFailedErrorを投げる可能性があるのでtryを使用し、do〜catchでエラーを補足することができます。

@Test func animalNullTest() {
    print("------1")
    let result: String? = nil
    do {
        print("------2")
        // 失敗すると後続の処理は行わない
        let nonNull = try #require(result)
        print("------3")
        #expect(nonNull != nil)
    } catch {
        // 失敗はキャッチできる
        print("ERROR:", error)
    }
}

Test run started.Testing Library Version: 102 (arm64-apple-ios13.0-simulator)Suite SampleAppTests started.Test animalNullTest() started.
------1
------2Test animalNullTest() recorded an issue at SampleAppTests.swift:29:31: Expectation failed: result → nil
ERRORExpectationFailedError(expectation: Testing.Expectation(evaluatedExpression: Testing.__Expression.Kind.generic("result"), mismatchedErrorDescription: nil, differenceDescription: nil, mismatchedExitConditionDescription: nil, isPassing: false, isRequired: true, sourceLocation: SampleAppTests/SampleAppTests.swift:29:31))Test animalNullTest() failed after 0.006 seconds with 1 issue.Suite SampleAppTests failed after 0.006 seconds with 1 issue.Test run with 1 test failed after 0.006 seconds with 1 issue.

@Testアノテーション

@Testの引数には文字列を渡すことでXcodeのログやコマンドライン上で表示されるテスト名をカスタマイズすることができます。

@Test("動物名確認テスト")
func animalTest() {
    let result = "Cat"
    #expect(result == "Cat")
}

他にも例外を投げるthrows、非同期にするためのasyncなどを付与することも可能です。

@Test func animalTest() async throws { ... }

パラメタライズテスト

@Testの引数argumentsテストメソッドのパラメーターに渡したい値をコレクション形式などで渡すことで複数の値でのテストを自動で実行させることが可能です。

@Test(arguments: ["Cat", "Dog", "Lion"])
func animalTest(animal: String) {
    #expect(animal == "Cat")
}

@Test(arguments: 1...99)
func someIntTest(_ value: Int) {
  #expect(int > 50)
}

メインスレッドでの実行

Swift Testingのテスト処理は基本的に任意のスレッドで実行されます。そのため意図的にメインスレッドで実行したい場合@MainActorを付与してスレッドを調整してあげる必要があります。

@Test() func threadCheck() {
    // Expectation failed: (Thread → NSThread).isMainThread → false
    #expect(Thread.isMainThread)
}

@Test()
@MainActor
func threadCheckMain() {
    // 成功:メインスレッドで実行される
    #expect(Thread.isMainThread)
}

@Suiteアノテーション

@Suiteテストをグループ化し同時に実行できるようにするための仕組みです。これにより複数のテストクラスを一括で管理することができ、テストクラスの責務の細分化と可読性を向上させることができるようになります。

定義の中にテスト関数があれば明示的に@Suiteを記述しなくても暗黙的にスイートとして識別されます。@Suiteで明示的にスイートにしたいときはテスト関数がなく、複数のスイートなどを1つにまとめたい時になります。

// 複数のスイートやテストケースを包括したスイート
@Suite
struct AllTests {
    static let allTests = [
        UserTests.self,
        MathTests.self,
        NetworkTests.self
    ]
}
// @Suiteを宣言しなくても暗黙的にスイートとして識別される
struct UserTests {
    @Test func someTest() { }
}

@Suiteを付与したオブジェクトはテスト関数(staticまたはclassキーワードのないもの)があれば実行します。

また引数に文字列を渡すことでスイート名をつけることも可能です。

@Suite("ユーザー登録テスト")

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article