Swift Testing + GitHub Copilot で、楽にユニットテストを書きたい(その1)


こんにちは!デバイスソフトウエア開発部の山内です。

私は現在建設DXを担うiPadアプリの開発を担当しているのですが、その中で、最近Swift Testingフレームワークでテストの自動化に取り組んでいます。 しかしこれがなかなか大変というか面倒というか…。

そこで思いました。GitHub Copilot の /tests コマンド でテストを自動生成すれば、工数を大幅に減らせるのでは…?

今回はそれを実際に試してみました。

まずは結論(3行で要約)

  • GitHub Copilot の /tests はプロジェクトのコンテキストや付随プロンプト次第で出力が変わる
  • どう書いてもおおむね品質は良さそうだが、コスパ的に.github/copilot-instructions.md で指示を固める手法が一番おすすめ
  • 複雑なコードに対する品質や、人間の手直しを極力減らす仕組みの模索が今後の課題

ユニットテストの基本的な考え方

本題に入る前に、ユニットテストの考え方とSwift Testingに関する説明を軽くしておきましょう。 すでに知っている方は「検証プロジェクトの紹介」セクションまで読み飛ばして構いません。

テストを書くと何が嬉しいのか

バグを早期発見できる、ということはよく言われますが、個人的には、「ソースコードの変更が怖くなくなる」という点が大きいと思います。

ある処理を書き直したとき、テストをパスすれば「既存の処理を壊していない」=デグレードが発生していないことを確認できます。逆にテストに失敗が出れば「どこかが変わった」とすぐに気づけます。 この安心感があることで、機能追加やリファクタリングを進めやすくなります。

Arrange-Act-Assert(AAA)

テストコードの書き方として、AAAパターンというArrange-Act-Assertの3段構成がよく使われます。 (3段構成のパターンとして Given-When-Then もあります。両者は単にステップの意味付け・解釈の違いと言って差し支えありませんが、個人的には動詞で揃っているAAAのほうが好きです。)

  • Arrange(準備):テスト対象のオブジェクトや入力値を用意する
  • Act(実行) :テスト対象の処理を呼ぶ
  • Assert(検証) :結果が期待通りかを確認する

慣れると自然にこの構造で書けるようになりますが、Copilot にテストを生成させるときのプロンプトにも活用できる ので、頭の片隅に入れておくと後で役立ちます。

何をテストすべきか

「全部のコードにテストを書かないといけないの?」という疑問をよく見聞きします。現実的には ビジネスロジックのある箇所に集中 するのが効率的でしょう。

テストの対象として優先度が高いのは、

  • 分岐のある処理(if / switch / guard)
  • エラーが起き得る処理(try-catch / optional型)
  • 入力に対して出力が決まる純粋な関数
といった箇所。 逆に、SwiftUIのViewのようにフレームワーク依存が強かったり見た目の確認が混ざる箇所は、ユニットテストが難しいため、最初は後回しにして問題ありません。

Swift Testing とは・導入方法

Swift Testingは、Apple が WWDC 2024 で発表した比較的新しいテストフレームワークです。同年 9 月にリリースされた Xcode 16 から正式統合 され、新規テストターゲット作成時のデフォルトフレームワークになっています。

XCTest と何が違うのか

Swift Testing は XCTest と比べて記述量が減り、Swift の言語機能との整合性が高いのが特徴です。大きな違いを表にまとめると以下のようになります。

項目 XCTest Swift Testing
テストの書き方 XCTestCase を継承したクラス struct / actor / グローバル関数
テスト関数の識別 メソッド名を test から始める @Test マクロを付ける
アサーション XCTAssertEqual 等、多数 #expect / #require に統一
セットアップ setUp() / tearDown() init() / deinit
非同期処理 XCTestExpectation で待機 async/await がそのまま使える
並列実行 複数プロセス 単一プロセス内の軽量な並列化

クラス継承が不要になった点と、アサーションが #expect に統合された点が大きいでしょう。

導入方法

Xcode 16 以降なら追加パッケージ不要です。

新規テストターゲットを追加すると、Swift Testing がデフォルトで選択された状態になります。 テストファイルの先頭に import Testing と書くだけで使い始められます。

基本構文

@Test マクロ

関数名に test プレフィックスは不要です。@Test を付けた関数がテストとして認識されます。

displayName を指定することもできます。

@Suite マクロ

関連するテストをまとめるには @Suite を使います。XCTest の XCTestCase に近いイメージですが、struct でも書くことができます

#expect と #require

#expect は失敗してもテストを続行します。#require は失敗したら即テストを中断します。

#expect の強力な点は、失敗時に式の値を自動でレポートしてくれることです。どの値がどう違ったか一目でわかります。

パラメタライズド(パラメータ化)テスト

複数の入力パターンを効率よく検証するのに使います。

各引数が 独立したテストケースとして Xcode 上に表示される ので、どのパターンで失敗したかが一目でわかるつくりとなっています。


検証プロジェクトの紹介

今回の検証では、以下のシンプルな PriceCalculator を題材にしてみました。

  • 複数の guard 分岐(ネガティブケース)がある
  • throws でエラーを返す
  • パラメータ化テストの題材として扱いやすい

実際のプロジェクトでいえば、ViewModelのロジック部分やバリデーション処理に近いイメージです。

GitHub Copilot /tests コマンドを使ってみる

さて、本題です。GitHub Copilot Extension for Xcode の /tests コマンドで、上記の PriceCalculator のテストを生成してみます。今回は4パターン試しました。

なお、検証した環境は以下の通りです:

Xcodeバージョン:26.2

モデル:GPT-5.2-Codex

事前準備:Copilot for Xcode のセットアップ

インストールしていない場合は以下の手順で準備します。

  1. github/CopilotForXcode の Releases から最新の .dmg をダウンロードしてインストール or Homebrewでインストール
  2. macOS の「システム設定 → プライバシーとセキュリティ → アクセシビリティ」で GitHub Copilot を許可
  3. Xcode 再起動後、メニューバーのCopilotアイコン → Open Chat でチャットを起動


パターン① /tests だけ入力(コンテキストなし)

PriceCalculator.swift のみが存在するプロジェクトで、何も指示せず /tests を実行。

結果:XCTest 形式で出てきた。

テストの内容自体は悪くないです。境界値(ネガティブケース、割引率 0 / 1 の境界)まで考慮してくれています。ただし、Copilot の学習データには XCTest のコードが圧倒的に多く、Swift Testing への参照が少ないためか、何も指示しないと XCTest を選んできます


パターン② プロジェクトに Swift Testing ファイルがある状態で /tests

新規テストターゲット作成時に Xcode が自動生成した空の Swift Testing ファイル(import Testing だけ書いてある)がプロジェクトに存在する状態で同じく /tests を実行。

結果:Swift Testing 形式で出てきた。

何も特別な指示をしていないのに、プロジェクト内に Swift Testing のファイルがあるというだけで正しくフレームワークを選んでくれました。 Copilot はプロジェクト内の既存コードをコンテキストとして参照しているということがよくわかる結果です。

ただし、よく見ると気になる点が一か所あります。async throws がすべてのテスト関数についていますね。 実際には PriceCalculator の処理はどれも非同期ではないので async は不要です。動くのですが、不要な記述が混入しているのは把握しておきたいところです。


パターン③ /tests にプロンプトを追加する

「Swift Testing で作って」という(雑)指示を添えて実行。

/tests XCTestじゃなくてSwift Testingで作って

結果:Swift Testing 形式、ただし @Suite なし(グローバル関数)。

import Testing は正しく使えていて、エラーの型も PriceError.invalidDiscount と具体的に指定できています。 ただし、グローバル関数の並びになっていて @Suite ではまとめられていません。


パターン④ .github/copilot-instructions.md を置いてから /tests

プロジェクトのルートに .github/copilot-instructions.md を配置してから同じく /tests を実行。

ファイルの中身は以下のように書きました。

結果:@Suite 付きの struct 形式。指示がほぼほぼ遵守されている。

比較的構造の整ったテストコードが出てきました。copilot-instructions.md の内容を守ってくれています。 テスト内容が単純なためAAA(Arrange-Act-Assert)の3段構成は断片的にしか見られませんが、1テストにつき1つの検証という形にはなっています。


生成されたコードは信用して良いか

4パターンとも、そのままコピーして使えるかというと、若干不安な部分は残ります。 以下、生成されたコードの中で見つけた注意点をいくつか見ていきましょう。

注意点①:エラーの型が曖昧

(any Error).self はどんなエラーが投げられても通過します。意図していない種類のエラーが出ていても気づけません。 具体的なエラー型を指定したほうが適切です。

注意点②:Double の等値比較

PriceCalculator の計算が 100 * 1.10 という単純な掛け算なのでこのケースは実際にはパスしますが、SwiftのDouble型の精度は約15〜16桁のため、桁が多かったり計算が複雑だとたまに丸め誤差で失敗する可能性があります。 abs(result - 110) < 1e-9 のようにして単純なイコールの使用を避けたほうが安全です。

注意点③:不要な async

PriceCalculator の処理は同期処理なので async は不要です。動作には影響しませんが、他人が読んだ時に「なぜ async がついているのか」という疑問を与えかねませんので、こういった点は取り除いておきたいところです。

実務での使い方を考える

4パターンを比較した感想として、著しくクオリティに差がつくというよりかは、XCTestを出してくる点さえ防止できれば、多少の差はあっても、どれもある程度品質の良いテストコードを出してくれる印象です。
しかしその上で、カスタム指示を一度書けば使いまわせる点から .github/copilot-instructions.md を使う方法が最も良いと感じます。

ただし、先に挙げた細かな改善点はどうしても出てきますので、Copilot の /tests は「そのまま完成品」ではなく「良い感じの叩き台」という位置付けで使うのが良いでしょう。ただそれだけでも、人間の仕事はテストコードのレビュー・修正作業くらいになりますから、テスト作成のハードルは相当下がるはずです。

今後に向けて

…とは言ったものの。 今回の検証はどちらかというと基礎的なものにとどまっています。 次はこんなことを知りたくなりますよね:

  • もっと複雑な処理でも精度保てるの?
  • そもそもテストの網羅性どうやって保証するの?
  • copilot-instructions.md を工夫して「そのまま完成品」の状態まで持っていけないの?
  • etc.
というわけで、また次回、もうちょっと踏み込んだ検証をして「本当に」実務に使えるのかを深掘りしていこうと思います!

ここまでお読みくださりありがとうございます。

エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!