SPIDERPLUS Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

UnitTestを調べ直した件について

みなさん、こんにちは。
プラットフォーム部MobileチームのEMをやっていますオダギリです。

突然ですが、みなさん、UnitTestは書いていますか?
我々スパイダープラスのプロダクトでもUnitTestは行っています。

今回、プロダクトの品質向上のために、改めてUnitTestについて調べましたので、皆様にも共有できればと思います。

What’s UnitTest?

UnitTestは、アプリケーションやプログラムの個々の要素(今回はメソッド)を独立してテストし、その動作が正しいかを確認するテスト手法です。

UnitTestのメリット

たくさんあると思いますが、今回はその中でも個人的に大きいと思う3つを挙げます。

バグの早期発見

バグが特定の機能やメソッドに閉じ込められているかを早期に確認できる

リファクタリングの安心感

リファクタリングやアップデート時に機能の正確性が維持されているか確認できるため、安心してコード変更ができやすい

ドキュメンテーション代わり

テストケースの内容がそのままコードの利用方法や期待される動作のドキュメントとしても役立つ

結論として、UnitTestを活用することで、開発の効率性やコードの品質が大幅に向上します。特に、バグの早期発見、リファクタリング時の安心感、ドキュメンテーションとしての役割という3つのメリットが得られるため、プロダクト全体の安定性や保守性が高まる可能性があります。

いいUnitTestとは

私が思ういいUnitTestかどうかの観点は下記の2つです。

副作用(他の影響)のないテストのこと

  • 1つの機能(メソッド)のみを対象としていること
  • メソッド内で参照している外部メソッドは、テストダブルを利用して外部依存をなくすことで対象のメソッドだけのテストを可能にしている

可読性、 保守性が高いこと

  • テストの内容から、どんなテストをやっているかが判明できること
  • Arrange(準備)、 Act(実行)、 Assert(検証)のブロックごとに記載されていることで、どの部分で何をやっているか判明できる状態になっていること

例:

実装例

文章だけだとイメージが湧きにくいと思いますので、実際のコードでみてみましょう。
言語はSwiftで、アーキテクチャはMVVM + CleanArchitectureのサンプルを用意しました。今回は、ViewModelのテストを例にしてみたいと思います。
※Xcode16からのSwiftTestingを利用しています。

サンプルコード

protocol ViewModel {
    func onAppear()
}

protocol Output {
    func loadView(viewData: ViewData)
}

protocol UseCase {
    func fetch() -> Int
}

struct ViewData {
    var number: Int = 0
}

final class ViewModelImpl: ViewModel {
    private let output: Output
    private let useCase: UseCase
    private var viewData: ViewData

    init(output: Output, useCase: UseCase) {
        output = output
        useCase = UseCase
        viewData = .init()
    }

    func onAppear() {
        let result = useCase.fetch()
        viewData.number = result
        output.loadView(viewData: viewData)
    }
}

Mock

    class OutputMock: Output {
    private(set) var loadViewCallCount: Int = 0
    var loadViewHandler: ((_ viewData: ViewData) -> Void)?
    func loadView(viewData: ViewData) {
        loadViewCallCount += 1
        loadViewHandler?(viewData)
    }
}

class UseCaseMock: UseCase {
    private(set) var fetchCallCount: Int = 0
    var fetchHandler: (() -> Int)?
    func fetch() -> Int {
        fetchCallCount += 1
        if let fetchHandler { 
            return fetchHandler?()
        }
        return -999
    }
}
    

UnitTest

    import Testing
@testable import YourProject

@Suite("ViewModelのテスト")
struct ViewModelTests {
    let viewModel: ViewModel!
    let useCase: UseCaseMock!
    let output: OutputMock!
    
    init() {
        output = .init()
        useCase = .init()
        viewModel = .init(output: Output, useCase: UseCase)
    }

    @Test("onAppearのテスト")
    func onAppear() {
        let expected = 1
        useCase.fetchHandler = {
            expected
        }

        output.loadViewHandler = { viewData in
            #expect(viewData.number == expected, "値が一致すること")
        }

        viewModel.onAppear()

        #expect(useCase.fetchCallCount == 1, "useCase.fetchが1度だけ呼ばれること")
        #expect(output.loadViewCallCount == 1, "output.loadViewが1度だけ呼ばれること")
    } 
}
    

解説

初期化

ViewModelの初期化時に、outputとuseCaseのMockを注入する

    init() {
    output = .init()
    useCase = .init()
    viewModel = .init(output: Output, useCase: UseCase)
}

更新された値になっているかの確認

output.loadViewの引数で渡っているViewDataのnumberは、 useCase.fetchで取得できた値と一致していることを確認する

    output.loadViewHandler = { viewData in
    #expect(viewData.number == expected, "値が一致すること")
}

外部クラスのメソッドの呼び出し回数の確認

useCase.fetchとoutput.loadのメソッドは、それぞれ1度ずつしか呼ばれていないことを確認する

    #expect(useCase.fetchCallCount == 1, "useCase.fetchが1度だけ呼ばれること")
     #expect(output.loadViewCallCount == 1, "output.loadViewが1度だけ呼ばれること")

参考にした記事

qiita.com

 

qiita.com

 

qiita.com

 

以上です。

今回、改めてUnitTestについて調べたことで、今までなんとなく作っていたUnitTestに対して前述の「UnitTestのメリット」のようなUnitTestに対する価値や意義を再認識する機会になりました。

これからは、より強く意味のあるUnitTestを作っていければなと思います。

スパイダープラスでは仲間を募集中です。

スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。