Quick/Nimbleとは
Quick
RSpec
風にユニットテストを書けるSwiftのBDDテストフレームワーク。
自然な言語に近い形でテストの構造を組み立てられるのが魅力👍
describe("ログイン機能") {
context("正しい入力のとき") {
it("成功すべき") {
// テスト処理をここに書く
}
}
}
→ これがそのまま「仕様書」になる。読みやすくて伝わるテストが書ける。
Nimble
Quickと一緒によく使われる 柔軟で読みやすいアサーションライブラリ。
expect(value).to(equal(x))
expect(error).to(beNil())
expect(array).toEventually(haveCount(1))
非同期処理も .toEventually
でサクッと書けるのが魅力。
Quick/Nimbleを使うと何が嬉しいのか
Before(XCTest) | After(Quick/Nimble) |
---|---|
XCTAssertEqual(result, 42) | expect(result).to(equal(42)) |
testSearchSuccess() | it("成功すべき") |
手続き的で読みにくい | 仕様書みたいに読める |
非同期処理は苦痛 | .toEventually で簡潔に記述 |
テスト=めんどくさい
から、テスト=書くの気持ちいい
に変わる。
describe
, context
, it
の意味と使い分け
describe("ある機能") {
context("ある状況") {
it("こうなるべき") {
// 検証
}
}
}
describe
→ テスト対象(クラス、関数)context
→ 状況(入力値、状態など)it
→ 結果(どうなるべきか)
→ 「〇〇のとき、××すべき」って構造になるから、読んでもわかりやすいのが特徴。
サンプルアプリでざっくり学ぶ
多分コーディングテストとか課題とかでよく出てくる、GithubのRepository検索機能をざっくり作ってそのテストケースを作成。

Quick/Nimbleのインストール (SPM)
Quick: https://github.com/Quick/Quick
NImble: https://github.com/Quick/Nimble
ターゲットはTestの方に入れないとコンパイルエラーになるのでそこだけ気をつけましょう。

GitHubAPIClient.swiftと、MockGitHubAPIClient.swift
MockGitHubAPIClient.swift
の方は、Testsディレクトリに入れてターゲットもTestにしておくこと。
import Foundation
import Combine
class GitHubAPIClient {
func searchRepositories(query: String) -> AnyPublisher<[Repository], Error> {
guard let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "https://api.github.com/search/repositories?q=\(encodedQuery)") else {
return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: SearchResponse.self, decoder: JSONDecoder())
.map(\.items)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
import Quick
import Nimble
import Combine
@testable import GitHubSearchApp
class MockGitHubAPIClient: GitHubAPIClient {
var mockResult: Result<[Repository], Error> = .success([]) // テストケース用のMockデータの役割
override func searchRepositories(query: String) -> AnyPublisher<[Repository], any Error> {
switch mockResult {
case .success(let repos):
return Just(repos)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
case .failure(let error):
return Fail(error: error)
.eraseToAnyPublisher()
}
}
}
SearchViewModel.swift
import SwiftUI
import Combine
final class SearchViewModel: ObservableObject {
@Published var query: String = ""
@Published var results: [Repository] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let apiClient: GitHubAPIClient
private var cancellables = Set<AnyCancellable>()
init(apiClient: GitHubAPIClient = GitHubAPIClient()) {
self.apiClient = apiClient
}
func search() {
guard !query.isEmpty else { return }
isLoading = true
errorMessage = nil
apiClient.searchRepositories(query: query)
.sink(receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
}, receiveValue: { [weak self] repos in
self?.results = repos
})
.store(in: &cancellables)
}
}
SearchView.swift (もしくはContentView、アプリエントリーポイント対象のView)
import SwiftUI
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
NavigationView {
VStack {
TextField("Search GitHub repos", text: $viewModel.query)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("Search") {
viewModel.search()
}
if viewModel.isLoading {
ProgressView()
}
List(viewModel.results) { repo in
VStack(alignment: .leading) {
Text(repo.name).font(.headline)
if let desc = repo.description {
Text(desc).font(.subheadline)
}
}
}
}
}
.navigationTitle("GitHub Search")
}
}
#Preview {
SearchView()
}
SearchViewModelSpec.swift (テストコード記述ファイル)
今回の肝、簡潔に
- VMの初期化 → ヨシ
- 成功時のデータ返還 → ヨシ
- 失敗時のデータ返還 → ヨシ
- QueryがEmptyの時のデータ返還 → ヨシ
これらのチェックを行いましょう。
import Quick
import Nimble
@testable import GitHubSearchApp
final class SearchViewModelSpec: QuickSpec {
override class func spec() {
// 各テスト前に使い回す変数(mockとViewModel)
var mockApiClient: MockGitHubAPIClient!
var viewModel: SearchViewModel!
// 初期状態のテスト(テスト対象が正しく初期化されるか)
describe("SearchViewModel") {
it("can be initialized") {
let viewModel = SearchViewModel()
expect(viewModel.query).to(equal(""))
expect(viewModel.results).to(beEmpty())
}
}
describe("SearchViewModel Execute Search") {
// 各テストごとに ViewModel と モックAPI を毎回初期化
beforeEach {
mockApiClient = MockGitHubAPIClient()
viewModel = SearchViewModel(apiClient: mockApiClient)
}
context("When search succeeds") {
it("Updates the results array") {
let expectedRepo = Repository(id: 1, name: "swift", description: "Swift language", html_url: "https://github.com/apple/swift")
mockApiClient.mockResult = .success([expectedRepo])
viewModel.query = "swift"
viewModel.search()
expect(viewModel.results).toEventually(haveCount(1))
expect(viewModel.results.first?.name).toEventually(equal("swift"))
}
}
context("When search fails") {
it("sets errorMessage") {
mockApiClient.mockResult = .failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Mock Error"]))
viewModel.query = "fail"
viewModel.search()
expect(viewModel.errorMessage).toEventually(equal("Mock Error"))
}
}
context("When query is empty") {
it("Does not perform search") {
mockApiClient.mockResult = .success([Repository(id: 1, name: "test", description: nil, html_url: "")])
viewModel.query = ""
viewModel.search()
expect(viewModel.results).toEventually(beEmpty())
}
}
}
}
}
完成したら、command + U
を実行するとテストビルドが回ります、そこで落ちていた場合はFailedになって、詳細を確認できます。
今回だと、成功時の”swift”を”swifts”にするとエラー詳細を見れます。
お前swifts
をexpectしてんのにswift
返ってきてんじゃねーかって感じで怒ってくれます。

Quick/Nimbleが最強な理由まとめ
- 📘 可読性が高く、仕様書みたいに書ける
- 🔁 beforeEach/afterEach で共通処理がシンプルに
- 🧪 柔軟なアサーションで書きたい通りに書ける
- 🔮 非同期処理もスマートに検証できる
まだまだ基礎部分だけの紹介なので、今後も色々とテスト技術を書いていこうと思います。