こんにちは!
引っ越しのために本棚をひっくり返していたら、エンジニアなりたての頃の勉強ノートが出てきました。
今となっては全く役に立たないノートなのに、なんとなく捨てられない とと です。
毎日頭が沸騰するんじゃないかと思うくらい頭をフル回転させて、人生で一番カロリーを使っていたのか、あのときほど減量に成功した日はいまだかつてありません。
(プログライミングダイエットと呼んでいます ※効果には個人差があります)
Unitテスト書いてますか?GitHub Copilot使ってますか?
さて、わたしは普段、STORES 決済 アプリ/SDK を開発するチームでiOSエンジニアをしています。
この2つのプロジェクトの現在のUnitテストのカバレッジは以下の通りとなっています。
- アプリ: 33.15%
- SDK: 27.98%
結構頑張っている方だと思うのですが、どうでしょうか?
STORES 決済 iOSチームでは、Clean Swiftというアーキテクチャを採用しています。
Clean Swiftの詳細については、公式ページをご参照いただくことにして、 今回は、わたしたちがこのアーキテクチャにおいてどのようなテストを書いているのか、そして昨今話題のGitHub Copilotがこのテストを書くのをちょっと…いやかなり楽にしてくれたのでその経験をご紹介します。
どんなUnitテストを書いているのか
Workerクラスのテストや、StringをExtensionした部分のテストも書いていますが、今回は紹介を割愛します。
以下は、Clean SwiftのうちのInteractorとPresenterのクラスのテストです。
Interactor
Interactorは、ViewContorollerからイベントの通知を受け、Workerに処理を投げたりさまざまな処理をしたのち、Presenterの関数を呼び出します。
protocol HogeBusinessLogic { func doSomething(request: Hoge.Something.Request) } class HogeInteractor: HogeBusinessLogic { var presenter: HogePresentationLogic = HogePresenter() var worker: HogeWorker = HogeWorker() func doSomething(request: Hoge.Something.Request) { let result = worker.doSomeWork() presenter.presentSomething(response: .init(result: result)) } }
Presenterをテストではテストダブルに差し替えられるよう、変数にしておきます。
テストケースによってWorkerでの処理の結果も変える必要があるときは、Workerクラスも差し替え可能にしておきます。
(※テストダブル (Test Double):テスト対象のコンポーネントを置き換える代替品のこと)
Interactorのクラスはこのようなテストを書きます。
@testable import CleanSwiftTests import XCTest class HogeInteractorTests: XCTestCase { var sut: HogeInteractor! // MARK: - Test doubles class HogePresentationLogicSpy: HogePresentationLogic { var presentSomethingCalled = false func presentSomething(response: Hoge.Something.Response) { presentSomethingCalled = true } } // MARK: - Tests func testDoSomething() { // Given let spy = HogePresentationLogicSpy() sut.presenter = spy // When sut.doSomething(request: .init()) // Then XCTAssertTrue(spy.presentSomethingCalled, "doSomething(request:) should ask the presenter to present something") } }
差し替えたPresenterがフラグを更新するだけのシンプルなものですが、これでInteractorの関数が想定どおりのPresenterの関数を呼ぶかをテストができます。
Presenter
Presenterは、Interactorから処理の結果を受け取り、Viewの表示に必要な情報を加えたり、分岐をして、ViewContorollerに通知します。
protocol HogePresentationLogic { func presentSomething(response: Hoge.Something.Response) } class HogePresenter: HogePresentationLogic { var viewController: HogeDisplayLogic = HogeViewController() func presentSomething(response: Hoge.Something.Response) { viewController?.displaySomething(viewModel: .init()) } }
さきほどのInteractorのテストとやることはほとんど同じです。
ViewContollolerのクラスをテストダブルに差し替えられるようにしておきます。
@testable import CleanSwiftTests import XCTest class HogePresenterTests: XCTestCase { var sut: HogePresenter! // MARK: - Test doubles class HogeDisplayLogicSpy: HogeDisplayLogic { var displaySomethingCalled = false func displaySomething(viewModel: Hoge.Something.ViewModel) { displaySomethingCalled = true } } // MARK: - Tests func testPresentSomething() { // Given let spy = HogeDisplayLogicSpy() sut.viewController = spy // When sut.presentSomething(response: .init(result: result)) // Then XCTAssertTrue(spy.displaySomethingCalled, "presentSomething(response:) should ask the view controller to display something") } }
Presenterの関数が想定どおりのViewContorollerの関数を呼ぶかのテストができました。
GitHub Copilotを使ってちょっと楽にUnitテストを書こう
さて、ご覧いただいたとおりClean Swiftのテストの内容自体はシンプルです。
このアーキテクチャでは、ひとつの処理ごとにViewContoroller→Interactor→Presenter→ViewContorollerと、ぐるっと環を描くように処理を実装する必要があります。
結果的に、ひとつのクラスで定義される関数は、その画面でのイベントの数や処理の多さと比例して増えていくことになります。
すると、テスト対象の関数名や、期待する値の入った変数名がちょっとずつだけ違うテストを大量に作ることになります。
地味〜〜〜に、めんどくさいですね。
ひとつの処理のテストを書くにしても、Interactorのテストだけでも手動だと
// MARK: - Test doubles class HogePresentationLogicSpy: HogePresentationLogic { // step①: ここに`Presenterの関数名+Called = false`を追加 var presentSomethingCalled = false // step②プロトコルで定義された関数を追加(これはXcodeがやってくれる) func presentSomething(response: Hoge.Something.Response) { // step③ 関数内で`Presenterの関数名+Called = true`を追加 presentSomethingCalled = true } } // MARK: - Tests // step④ テストケースを適当なところからコピー&ペースト // step⑤ `test関数名(頭文字を大文字に)`に変更 func testDoSomething() { // Given let spy = HogePresentationLogicSpy() sut.presenter = spy // When // step⑥ Interactorの関数を呼び出す sut.doSomething(request: .init()) // Then // step⑦ 期待値に`spy.Presenterの関数名+Called`を入れる // step⑧ コメントを`Interactorの関数 should ask the presenter to Presenterの関数名`にする XCTAssertTrue(spy.presentSomethingCalled, "doSomething(request:) should ask the presenter to present something") }
という、最低8Stepが必要になります。
矩形選択と正規表現での置換を駆使して少しでも楽にと思っていたのですが、それでも少々手間でしたし、
ちょっとしたミスを発生させてしまったときに、同じようなコードが並んでいるせいで、目視で確認が厳しくミスした箇所に気付きづらいという欠点がありました。
GitHub Copilotの導入
当社では、社員は誰でもGitHub Copilotを使うことができます。詳細はこちらの記事をご参照ください。
GitHub Copilot for Business 始めました - STORES Product Blog
XcodeでGitHub Copilotを使うために、CopilotForXcodeを使用させていただきました!
いざGitHub Copilotを使うと
たとえば、プロダクトコード側でPerformTask()
という処理を追加してみましょう。
Interactorにはfunc performTask(request: Hoge.PerformTask.Request)
、HogePresentationLogic及びPresenterにはfunc presentPerformTask (response: Hoge.PerformTask.Response)
という関数を追加しました。
すると、XcodeがHogeInteractorTestsのクラスに書いた、HogePresentationLogicSpyがHogePresentationLogicプロトコルにある関数を書いていないと怒りますのでこれは素直に従いましょう。
これは先程のStep②でもやった作業です。(Step①)
ここからはGitHub Copilotの出番です。
呼び出しがかかったかどうかのフラグの定義を提案してくれました。(Step②)
関数でこのフラグを更新する処理も提案してくれます。(Step③)
なんと、テストも提案してくれます。(Step④)
GitHub Copilotを使うと、8step必要だったのが半分の4stepまで減らすことができました。
しかも、最初の3文字くらいを手動で入力すれば、ほとんどポチポチするだけで終わりました。 お見事。
精度や速さについて
最初は精度がいまいちだったのですが、すぐにぴったりのコードを提案してくれるようになりました。
シンプルな関数であれば、手直しなしで思った通りのテストコードが出てきます。
修正が必要なこともありますが、それはあえて他のテストケースと違うことをやりたいときだったり、イレギュラーなテストケースをしたいときのみにほとんど留まりました。
速さについては、サジェストまでワンテンポ待つ感じはあります。
さすがにXcodeのサジェストや、静的解析エラーの方が少し早いですが、自分で打つ方が早い…と思うほどは待たずにすみました。
途中でCopilotForXcodeをアップデートしたのですが、それによってさらに少し速くなったような気がしています。
おわりに
正直わたしはまだSwiftのプロダクトコードの実装で"GitHub Copilotめっちゃありがたい!!"という経験はまだできていないのですが、Unitテストを書くのにはかなり頼りになりました。
今回の記事は、あえてシンプルな関数を例に出しましたが、実際のテストコードでは一工夫が必要なことも多々あります。
単純作業をGitHub Copilotがやってくれるおかげで、そういう複雑なテストを書くのに集中力や工数を割けることができるようになったと感じています。
GitHub Copilot最高!ありがとう!
さて、今回の記事ではあまり変わったことはご紹介しておりませんし新たな知見となったかは自信がありませんが、皆様のUnitテストを書くモチベーションが少しでも上がれば嬉しいです。
以上!Bye
iOSエンジニア募集中です!