NSAssert("#testing".existing)
XCTAssert(everybodyDoingTesting)
一つのテストケースに複数のアサーションを書いてしまうと、もしアサーションの評価が失敗だったときにどれが失敗したのかがわかりにくいという問題です。
の「詳しくは」先のコメント https://qiita.com/hmhmsh/items/842e795d4b2a41556b5b#comment-a9bd5d57270c1eebfee2 ですが、XCTAssert
系は失敗しても止まらないですよ。XCTSAssert
系メソッドを書いてます。「XCTAssert系は失敗しても止まらない」ので、↓を作ったときは失敗の数が減っていくのを確認しながらバグフィックスしました。 https://github.com/koher/EasyImagy/blob/dev-0.4.0/Tests/EasyImagyTests/ExtrapolationTests.swift (edited)continueAfterFailure
が false
の場合は1つのアサーションが失敗したら、そのテストメソッドはそれ以降実行されない、 true
なら続きも実行される。デフォルトは true
(edited)guard
して XCTFail
→ return
してました。 (edited)false
になってます…。 true
なら続きも実行される。ですかね。恐縮っす。
(edited)XCTAssert系は失敗しても止まらない
のもcontinueAfterFailure
も知りませんでした...ありがとうございます。Discussion The default is true. Set this property to false within a test method to end test execution as soon as a failure occurs.
https://developer.apple.com/documentation/xctest/xctestcase/1496260-continueafterfailurefunc urlOrNil1(urlString: String?) -> URL? { guard let urlString = urlString else { return nil } return URL(string: urlString) } func urlOrNil2(urlString: String?) -> URL? { return urlString.flatMap(URL.init(string:)) }
後者の書き方を使うことが多かったのだけど、前者だとnil
を返すパターンをテストできてるかどうかcode coverageを使えばチェックできる事に気付いた。Optional
の map
や flatMap
を使ってましたが、今はほとんど guard let
使ってます。可読性のために。 ?.
も必要最小限しか使わないですね。nil
になるのがロジックエラーなら)よく使います。できるだけ、それが nil
にならない理由をコメントをつけるようにしています(本当はコメントではなくエラーメッセージにしてくれるものがあれば標準であればいいですが)。!
を含むロジックエラーは Swift でテストできないのが難点ですね。precondition
に引っかかることをテストしたいこととか結構よくあります・・・。precondition
に引っかかったらクラッシュしちゃいません?テストで precondition
等の失敗をハンドルする方法あるんですっけ?!
の失敗も。x
に負の数を入れて正しくエラーになることをテストしたいです。 func foo(_ x: Int) { preconditon(x >= 0) // x を使う処理 }
RuntimeException
なので catch
できるんですが・・・。precondition
や !
の失敗を階層的に食い止められるようになるならかなり良さそうだと妄想してます。var x: String? = nil var y: String = x!
precondition
にひっかかるテストはできるかも。
static let all: DispatchSource.ProcessEvent static let exec: DispatchSource.ProcessEvent static let exit: DispatchSource.ProcessEvent static let fork: DispatchSource.ProcessEvent static let signal: DispatchSource.ProcessEvent
!
の失敗やオーバーフローにどう対処するかって話をしてるし、それを actor で isolation する話っぽいから、目指してるものとしてはドンピシャのように思うけど、中身はそうじゃないのかな? (edited)viewDidAppear(:)
で呼ばれるメソッド等をテストしたい場合って皆さんどうしてますか? let viewController = HogeViewController() window.rootViewController = viewController window.makeKeyAndVisible() let expectation = XCTestExpectation(description: "wait viewDidAppear") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { XCTContext.runActivity(named: "viewDidAppearでメソッドがコールされた事を確認", block: { (_) in XCTAssertTrue(viewController.calledViewDidAppear) }) expectation.fulfill() } wait(for: [expectation], timeout: 0.2)
みたいなコードが乱立するとユニットテストの時間がどんどん長くなるんじゃないかという事を危惧しています。When running several simulators simultaneously you may exceed the maximum number of processes or maximum open file limits. If this happens attempts to start new processes or open more files will fail. You can recover by shutting down some simulators or closing open programs. macOS 10.13 automatically scales these limits based on available system memory. On earlier releases these limits are fixed values. These limits can be increased by using launchctl. For more information, choose Help > Simulator Help and navigate to Troubleshoot Simulator > Insufficient resources. (31179087)
XCTAssert(xs.contains(4)) | | | | false 4 [1, 2, 3]
SwiftPowerAssertがそこそこ安定してきたのでバージョン0.1.0として公開しました。 https://github.com/kishikawakatsumi/SwiftPowerAssert 現時点でもTDDスタイルの開発などにけっこう便利に使えるような気がします。 簡単に試せるようにオンラインのPlaygroundも用意しました。 https://swift-power-assert.kishikawakatsumi.com/ 使ってみてうまく動かないパターンを教えてもらえると嬉しいです。fatalError()
や preconditionFailure()
, Optionalの!
やtry!
などで発生したエラー、すなわちSwiftのエラーの分類で言うところの**Universal...struct Example { let a: String let b: String // ... } let example = Example() XCTAssertTrue(example :> (a: "foo", b: "bar"))
(edited):>
を提供する感じですねa > b
を a < b
とかに書き換えたときに、ちゃんとテストが検知できるか、みたいなのを検査するイメージです (edited)Supported mutation operators Math: Add,Sub,Mul,Div: replaces + with -, - with +, * with /, / with *. Negate Condition: inverses conditions like A -> !A or == -> !=. Remove Void Function Call: removes a function which doesn't have a return value. Replace Call: replaces function calls with Int, Float and Double return values with number 42. Alpha state, not enabled by default. Scalar Value: replaces 0 with 1, other numbers with 0. Alpha state, not enabled by default. AND <-> OR: replaces && -> || and vice versa. Alpha state, not enabled by default.
func group: (id: Int) -> Observable<Group> init( group: @escaping ((_ id: Int) -> Observable<Group>) = { _ in Observable.never() } ) { self.group = group }
とかやれたら最高なんですけど (edited)protocol Animal { func group(id: Int) -> Observable<Group> func bark() -> String }
まずはAnimalを使ったモジュールのテストを書いていこうというとき、現状では↓のような汎用のスタブを作り struct StubAnimal { func group(id: Int) -> Observable<Group> { return _group(id) } private var group: ( id: Int) -> Observable<Group> func bark() -> String { return _bark() } private var _bark: () -> String init( group: @escaping ((_ id: Int) -> Observable<Group>) = { _ in Observable.never() } , bark: @escaping (() -> String) = { _ in "meow" } ) { _group = group _bark = bark } }
SUTに関係ない関数はデフォルト引数で済ませられるよう、また、何に関心を持っているか、何がSUTへの間接入力になりうるかが明確になるようにしています。 (edited)func testX() { let x: Float80 = 0 XCTAssertEqual(x, 0) // OK XCTAssertEqual(x, 0, accuracy: 1e-4) // EXC_BAD_INSTRUCTION }
XCTestこれだけでクラッシュしててひどいpreconditionFailure
? https://github.com/apple/swift/blob/master/stdlib/public/Darwin/XCTest/XCTest.swift#L391expectCrash
が欲しい…/// Asserts that an expression crashes. /// - Important: **The expression will not be evaluated if current process is being debugged.** /// - Parameters: /// - expression: An `expression` that can crash. /// - message: An optional description of the failure. /// - file: The file in which failure occurred. /// Defaults to the file name of the test case in which this function was called. /// - line: The line number on which failure occurred. /// Defaults to the line number on which this function was called. /// - signalHandler: An optional handler for signal that are produced by `expression`. /// - stdoutHandler: An optional handler for stdout that are produced by `expression`. /// - stderrHandler: An optional handler for stderr that are produced by `expression`. /// - Returns: A value of type `T?`, the result of evaluating the given `expression`. /// `nil` if expression crashed. @discardableResult public func XCTAssertCrash<T>( _ expression: @escaping @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, signalHandler: (Int32) -> Void = { _ in }, stdoutHandler: (String) -> Void = { _ in }, stderrHandler: (String) -> Void = { _ in } ) -> T?
(edited)XCTAssertCrashes
の方が良いだろうか。ThrowAssertion()
ってのがあるのね。 https://github.com/Quick/Nimble/blob/master/Sources/Nimble/Matchers/ThrowAssertion.swiftfatalError
になる挙動も真似したいとか。Nimble
は、僕が避けたMach exceptionsの仕組みを使うライブラリを利用してXcodeのlldbと共存出来てるのすごい。 https://github.com/mattgallagher/CwlPreconditionTesting (edited)XCTAssertUnrecoverable
はsigaction
が内部で使われてるから、僕のと同じ制限があるな。setjump
, longjump
を使わずに、別Thread
でクロージャを実行して、trap有無を確認してからThread.exit()
な感じにした。--parallel
を使うと全部別プロセスになるから、問題なくなる。 (edited)Thread.exit()
しない方が良いかも。
XCTAssertCrash()
を作ったとして、XCTAssertNoCrash()
は必要なのか? (edited)XCTAssertNoCrash()
は不要ぽい。swift api-digester
を使ってる。 https://github.com/apple/swift-nio/blob/master/scripts/check_no_api_breakages.sh#L100-L101 swift api-digester -sdk "$sdk" -diagnose-sdk \ --input-paths "$tmpdir/api-old/$f" -input-paths "$tmpdir/api-new/$f" 2>&1 \ > "$report" 2>&1
XCTAssertCrash()
をMac Exception仕様への書き換えがほぼ終わったので、iOSデバイス上のテストでどうなるか確認してみた。assert
系は実機だと全部ブレークポイントになってるから、デバッガで設定するブレークポイントと区別をつけるのが無理かも? (edited)assert
系は実機だと全部ブレークポイントになってるから、デバッガで設定するブレークポイントと区別をつけるのが無理かも? CustomTraversable
それっぽいものが!async
な API をテストするときのベストプラクティス的な方法ってありますか?↓ここに書かれている extension
? https://github.com/Quick/Quick/issues/1084#issuecomment-1007665281 (edited)func XCTAssertGreaterThanOrPrint(_ actual: Float, _ expected: Float, file: StaticString = #file, line: UInt = #line) { if actual > expected { print("actual: \(actual)") return } XCTAssertGreaterThan(actual, expected, file: file, line: line) }
こんな感じで print
付きのラッパーを作るとかですかね。 成功だけどエディタ上に表示する方法は知らないです、無い気がします。 (edited)Test Suite 'GSetTests' started at 2023-05-02 09:56:52.910 Test Case '-[LWWElementDictionaryTests.GSetTests testAppend]' started. Test Case '-[LWWElementDictionaryTests.GSetTests testAppend]' passed (0.001 seconds). Test Case '-[LWWElementDictionaryTests.GSetTests testCompare]' started. Test Case '-[LWWElementDictionaryTests.GSetTests testCompare]' passed (0.001 seconds). Test Case '-[LWWElementDictionaryTests.GSetTests testContains]' started. Test Case '-[LWWElementDictionaryTests.GSetTests testContains]' passed (0.000 seconds). Test Case '-[LWWElementDictionaryTests.GSetTests testMerge]' started. Test Case '-[LWWElementDictionaryTests.GSetTests testMerge]' passed (0.000 seconds). Test Case '-[LWWElementDictionaryTests.GSetTests testReplace]' started. Test Case '-[LWWElementDictionaryTests.GSetTests testReplace]' passed (0.000 seconds). Test Case '-[LWWElementDictionaryTests.GSetTests testSubscript]' started. /Users/katsumi/Developer/LWWElementDictionary/LWWElementDictionaryTests/GSetTests.swift:70: error: -[LWWElementDictionaryTests.GSetTests testSubscript] : XCTAssertNil failed: "a" Test Case '-[LWWElementDictionaryTests.GSetTests testSubscript]' failed (0.015 seconds). Test Suite 'GSetTests' failed at 2023-05-02 09:56:52.929. Executed 6 tests, with 1 failure (0 unexpected) in 0.018 (0.019) seconds
コマンドラインから実行したとき(xcodebuild test ~)は個々のテストケースのログが出なくなってる? Test suite 'GSetTests' started on 'My Mac - xctest (57795)' Test case 'GSetTests.testAppend()' passed on 'My Mac - xctest (57795)' (0.001 seconds) Test case 'GSetTests.testCompare()' passed on 'My Mac - xctest (57795)' (0.001 seconds) Test case 'GSetTests.testContains()' passed on 'My Mac - xctest (57795)' (0.000 seconds) Test case 'GSetTests.testMerge()' passed on 'My Mac - xctest (57795)' (0.001 seconds) Test case 'GSetTests.testReplace()' passed on 'My Mac - xctest (57795)' (0.008 seconds) Test case 'GSetTests.testSubscript()' failed on 'My Mac - xctest (57795)' (0.021 seconds)
-parallel-testing-enabled NO
にしたら出るようになったけどどういうことなんだろう?コンソール出力が混ざるのを恐れているっていうことなのかな。
defaults write com.apple.dt.xcodebuild ParallelTestRunnerStdoutMirroringEnabled -bool YES
このUser Defaultsを設定すると並列テストが有効でもログがちゃんと出るのをラボで教えてもらった。swift test
の出力結果の視認性が良くなったのは個人的にはかなり良かったです. XCTest の出力結果はよく目を凝らして読まないと成功したのか失敗したのかどのテストケースが失敗したのかがわからなくて辛かったので.@Test
の arguments
でテストケースを増やしてますが,0.10.0 時点ではこんな感じですね.(xcodebuild じゃないのでそこで違いはあるかもしれないですが) https://github.com/kkebo/zyphy/actions/runs/9456496307/job/26048489628#step:7:387 (edited)◇ Test run started. ↳ Testing Library Version: 69d59cfc76e5daf498ca61f5af409f594768eef9 ◇ Test "html5lib-tests" started. ◇ Test namedCharRef() started. ◇ Test basicHTML() started. (中略) ◇ Passing 1 argument testCase → \u000B (scriptData) to "html5lib-tests" ◇ Passing 1 argument testCase → \u000B (cdataSection) to "html5lib-tests" ✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → \u000B (plaintext) at HTML5LibTests.swift:45:5: Expectation failed: (tokenizer.sink.errors.count → 0) == (testCase.errors.count → 1) ✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → \u000B (rcdata) at HTML5LibTests.swift:45:5: Expectation failed: (tokenizer.sink.errors.count → 0) == (testCase.errors.count → 1) ✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → \u000B (scriptData) at HTML5LibTests.swift:45:5: Expectation failed: (tokenizer.sink.errors.count → 0) == (testCase.errors.count → 1) ◇ Passing 1 argument testCase → \u000C to "html5lib-tests" ◇ Passing 1 argument testCase → \u000C (plaintext) to "html5lib-tests" ✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → \u000B (rawtext) at HTML5LibTests.swift:45:5: Expectation failed: (tokenizer.sink.errors.count → 0) == (testCase.errors.count → 1) ◇ Passing 1 argument testCase → \u000C (scriptData) to "html5lib-tests" ◇ Passing 1 argument testCase → \u000C (rawtext) to "html5lib-tests" ✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → \u000B (cdataSection) at HTML5LibTests.swift:45:5: Expectation failed: (tokenizer.sink.errors.count → 1) == (testCase.errors.count → 2) ◇ Passing 1 argument testCase → \u000C (rcdata) to "html5lib-tests" ◇ Passing 1 argument testCase → \u000C (cdataSection) to "html5lib-tests" (中略) ✘ Test "html5lib-tests" failed after 0.174 seconds with 190 issues. ✘ Test run with 3 tests failed after 0.175 seconds with 190 issues.
@Test
の arguments
でテストケースを増やしてますが,0.10.0 時点ではこんな感じですね.(xcodebuild じゃないのでそこで違いはあるかもしれないですが) https://github.com/kkebo/zyphy/actions/runs/9456496307/job/26048489628#step:7:387 (edited)✘ Test "html5lib-tests" recorded an issue with 1 argument testCase → Truncated doctype start at HTML5LibTests.swift:44:5: Expectation failed: (tokenizer.sink.tokens → [Tokenizer.Token.comment(["!", "D", "O", "C"]), Tokenizer.Token.eof]) == (testCase.tokens → [Tokenizer.Token.comment(["D", "O", "C"]), Tokenizer.Token.eof]) ± inserted [Tokenizer.Token.comment(["!", "D", "O", "C"])], removed [Tokenizer.Token.comment(["D", "O", "C"])]
✘ Test namedCharRef() recorded an issue at HTMLEntitiesTests.swift:16:5: Expectation failed: (Metadata(duration: .seconds(0), resolution: .init(width: 1920, height: 1080)) → Metadata(duration: 0.0 seconds, resolution: Foundation.CGSize(width: 1920.0, height: 1080.0))) == (Metadata(duration: .seconds(90), resolution: .init(width: 3840, height: 2160)) → Metadata(duration: 90.0 seconds, resolution: Foundation.CGSize(width: 3840.0, height: 2160.0)))
CustomDebugStringConvertible
を真面目に実装しているかデフォルト実装ならちゃんと出るみたいですね. 逆に CustomDebugStringConvertible
の debugDescription
を "hoge"
とか返すようにしていると残念なことになっちゃいます.(Data
の比較とか) ✘ Test namedCharRef() recorded an issue at HTMLEntitiesTests.swift:23:5: Expectation failed: (Metadata(duration: .seconds(0), resolution: .init(width: 1920, height: 1080)) → hoge) == (Metadata(duration: .seconds(90), resolution: .init(width: 3840, height: 2160)) → hoge)
(edited)Data
は inserted と removed で Array
の時と同じくイテレートできる要素単位で教えてくれるっぽいです. ✘ Test namedCharRef() recorded an issue at HTMLEntitiesTests.swift:22:5: Expectation failed: ("hoge".data(using: .utf8)! → 4 bytes) == ("fuga".data(using: .utf8)! → 4 bytes) ± inserted [104, 111, 101], removed [102, 117, 97]
--experimental-event-stream-output
フラグ付きでテストバイナリをビルドするとテストバイナリがテスト中のイベントを named pipe に吐き出してくれるようになって,VS Code extension 側でその named pipe を監視するみたいな仕組みみたいですね.(他にも細々した工夫があるんでしょうけれど) https://github.com/swiftlang/vscode-swift/pull/775@Suite struct SomeTest { private static let dataSets: [(a: String, b: String, _ c: String)] = [ ("a", "b", "c"), ("d", "e", "f"), ] @Test(arguments: dataSets) // ← OK func doSomethingABC(_ a: String, _ b: String, c: String) { } @Test(arguments: dataSets.map { ($0.a, $0.b) }) // ← NG func doSomethingAB(_ a: String, _ b: String) { } @Test(arguments: dataSets.map { ($0.a, $0.c) }) // ← NG func doSomethingAC(_ a: String, _ c: String) { } }
(edited)@Suite struct SomeTest { private static let dataSets: [(a: String, b: String, c: String)] = [ ("a", "b", "c"), ("d", "e", "f"), ] @Test(arguments: dataSets) func doSomethingABC(_ a: String, _ b: String, _ c: String) { } @Test(arguments: dataSets) func doSomethingAB(_ a: String, _ b: String, _: String) { } @Test(arguments: dataSets) func doSomethingAC(_ a: String, _: String, _ c: String) { } }
(edited)scopeProvider
というスコープを生成するファクトリもある?#expect(exitsWith:)
で挟んだ領域を別プロセスで実行してステータスコードなどを見るらしい (edited)XCTAssertCrash
ってのを昔作ったことを思い出した。[https://github.com/norio-nomura/XCTAssertCrash]@Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello" }
@Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello" }
◇ Test run started. ↳ Testing Library Version: 6.2-dev (a7b5435933149b6) ↳ Target Platform: x86_64-unknown-linux-gnu ◇ Test helloWorld() started. ✘ Test helloWorld() recorded an issue at <stdin>:5:3: Expectation failed: (greeting → "Hello, world!") == "Hello" ✘ Test helloWorld() failed after 0.001 seconds with 1 issue. ✘ Test run with 1 test failed after 0.002 seconds with 1 issue.
swift
にJIT実行させるのではなく、swiftc
を呼び出して実行可能ファイルを生成してから実行する仕組みになっています。import Testing
やテストコードを呼び出す await Testing.__swiftPMEntryPoint() as Never
などのコードは無ければ勝手に追加してビルドします。__swiftPMEntryPoint
の中にあるってことですか?@Suite
や @Test
は実質的にリフレクションなのか@Foo
がついてるものを実行時に全部集めるの、自分でもやりたいことがあるimport Testing @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") } await Testing.__swiftPMEntryPoint() as Never
import Testing @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") } await Testing.__swiftPMEntryPoint() as Never
swift testing -
<stdin>:8:15: warning: will never be executed 4 | #expect(greeting == "Hello, world!") 5 | } 6 | await Testing.__swiftPMEntryPoint() as Never | `- note: a call to a never-returning function 7 | 8 | await Testing.__swiftPMEntryPoint() as Never | `- warning: will never be executed 9 | ◇ Test run started. ↳ Testing Library Version: 6.2-dev (a7b5435933149b6) ↳ Target Platform: x86_64-unknown-linux-gnu ◇ Test helloWorld() started. ✔ Test helloWorld() passed after 0.001 seconds. ✔ Test run with 1 test passed after 0.001 seconds.
import Testing @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") } await Testing.__swiftPMEntryPoint() as Never
swift -
exit status: 69 with ◇ Test run started. ↳ Testing Library Version: 6.2-dev (a7b5435933149b6) ↳ Target Platform: x86_64-unknown-linux-gnu ✔ Test run with 0 tests passed after 0.001 seconds.
main
を作るので見つけてくれる。 @swift-main swiftc import Testing @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") } await Testing.__swiftPMEntryPoint() as Never
main
を作るので見つけてくれる。 @swift-main swiftc import Testing @Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") } await Testing.__swiftPMEntryPoint() as Never
◇ Test run started. ↳ Testing Library Version: 6.2-dev (a7b5435933149b6) ↳ Target Platform: x86_64-unknown-linux-gnu ◇ Test helloWorld() started. ✔ Test helloWorld() passed after 0.001 seconds. ✔ Test run with 1 test passed after 0.001 seconds.
@Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") // Expectation failed: (greeting → "Hello, world!") == "Hello" }
@Test func helloWorld() { let greeting = "Hello, world!" #expect(greeting == "Hello, world!") // Expectation failed: (greeting → "Hello, world!") == "Hello" }
◇ Test run started. ↳ Testing Library Version: 6.2-dev (cabf4d419cf9c8c) ↳ Target Platform: x86_64-unknown-linux-gnu ◇ Test helloWorld() started. ✔ Test helloWorld() passed after 0.074 seconds. ✔ Test run with 1 test passed after 0.078 seconds.
swift repl
の起動時にはフレームワークサーチパス-F
が使われていないから、swift-testingがTesting.framework
で提供されてるmacOSではswift repl
でimport Testing
が動かないのね。$ swift repl -v Apple Swift version 6.1 (swiftlang-6.1.0.110.21 clang-1700.0.13.3) Target: arm64-apple-macosx15.0 /Applications/Xcode_16.3.app/Contents/Developer/usr/bin/lldb '--repl=-Xllvm -aarch64-use-tbi -enable-objc-interop -stack-check -sdk /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk -color-diagnostics -new-driver-path /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-driver -empty-abi-descriptor -resource-dir /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift -disable-clang-spi -target-sdk-version 15.4 -target-sdk-name macosx15.4 -external-plugin-path /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins#/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -external-plugin-path /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/local/lib/swift/host/plugins#/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -in-process-plugin-server-path /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/libSwiftInProcPluginServer.dylib -plugin-path /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/plugins -plugin-path /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/local/lib/swift/host/plugins' Welcome to Apple Swift version 6.1 (swiftlang-6.1.0.110.21 clang-1700.0.13.3). Type :help for assistance. 1>
swift repl
でimport Testing
を使う(動かないけど)には-F
をつける $ swift repl -F $(xcrun --show-sdk-platform-path)/Developer/Library/Frameworks Welcome to Apple Swift version 6.1 (swiftlang-6.1.0.110.21 clang-1700.0.13.3). Type :help for assistance. 1> import Testing 2> @Test func helloWorld() { 3. let greeting = "Hello, world!" 4. #expect(greeting == "Hello") // Expectation failed: (greeting → "Hello, world!") == "Hello" 5. } 6> await Testing.__swiftPMEntryPoint() as CInt Test run started. Testing Library Version: 124 Target Platform: arm64e-apple-macos14.0 Test run with 0 tests passed after 0.001 seconds. REPL requires a running target process. error: REPL process is no longer alive, exiting REPL
(edited)isRecursive
を使う場合は、そのトレイトは SuiteTrait
かつ TestTrait
でないといけない、違反するとノーヒントでランタイムクラッシュする (edited)SuiteTrait
かつ TestTrait
のトレイトが @Test
に付与されている場合、 scopeProvider
メソッドは SuiteTrait
としてと、 TestTrait
としてで、2回呼ばれ、testCase
引数が nil
かどうかで見分けられる (edited)isRecursive
を使うと、そのトレイトが、子孫の @Suite
と @Test
にも付与されたかのように振る舞う、これは test: Test
の .traits
プロパティで見ると実際に付与されている (edited)@Test
に対して、同じトレイトの scopeProvider
が2回呼ばれること (B) test
引数にも isSuite
プロパティがあり、 testCase
引数も nil
かどうかでスイートかどうかを表すが、これが冗長な理由 つまり test.isSuite = false && testCase == nil
が起こりうるのかどうか、もし起こるとしたら何を意味するか。 (edited)scopeProvider
メソッドを使わない(デフォルト実装で)で、 provideScope
メソッドだけ使う場合だと、isRecursive
なら、 @Test
の訪問時だけ、1回だけ呼ばれるようになります。 (edited)scopeProvider
のデフォルト実装が (B) のケースをカットする(nilを返す)からです@Suite struct MyTestSuite { init() { print("foo") } @Test func aaa() { } @Test func iii() { } }
これでswift test
すると、"foo"
が2回でるstruct MySuiteTrait: SuiteTrait & TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter suite scope") defer { print("leave suite scope") } try await function() } }
(edited)struct MyTestTrait: TestTrait & TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter test") defer { print("leave test") } try await function() } }
こうするとテストの開始・終了を挟める。struct MyRecursiveSuiteTrait: SuiteTrait & TestScoping { var isRecursive: Bool { true } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter test") defer { print("leave test") } try await function() } }
(edited)@Suite(.mySuite, .myRecursiveSuite)
って両方指定しないといけないから// a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() {} // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() {} }
func foo
に注目していました。test: Test
が違うから。testCase: Test.Case?
はどちらも nil
だから役に立たないけど。 (edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter test") defer { print("leave test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter test") defer { print("leave test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)enter test enter test bar leave test foo leave test
◇ Test run started. ↳ Testing Library Version: 6.2 (3fdabe5392108d8) ↳ Target Platform: aarch64-unknown-linux-gnu ◇ Suite MyTest started. ◇ Test foo() started. ◇ Test bar() started. ✔ Test bar() passed after 0.001 seconds. ✔ Test foo() passed after 0.001 seconds. ✔ Suite MyTest passed after 0.001 seconds. ✔ Test run with 2 tests in 1 suite passed after 0.001 seconds.
(edited)struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { ...
でなければいけないです (edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func scopeProvider(for test: Test, testCase: Test.Case?) -> MyRecursiveSuiteTrait? { print("scopeProvider called", test.name, testCase == nil ? "suite" : "test") return self } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func scopeProvider(for test: Test, testCase: Test.Case?) -> MyRecursiveSuiteTrait? { print("scopeProvider called", test.name, testCase == nil ? "suite" : "test") return self } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)scopeProvider called MyTest suite enter provideScope MyTest suite scopeProvider called bar() suite enter provideScope bar() suite scopeProvider called foo() suite enter provideScope foo() suite scopeProvider called bar() test enter provideScope bar() test bar leave provideScope bar() test scopeProvider called foo() test enter provideScope foo() test leave provideScope bar() suite foo leave provideScope foo() test leave provideScope foo() suite leave provideScope MyTest suite
◇ Test run started. ↳ Testing Library Version: 6.2 (3fdabe5392108d8) ↳ Target Platform: aarch64-unknown-linux-gnu ◇ Suite MyTest started. ◇ Test bar() started. ◇ Test foo() started. ✔ Test bar() passed after 0.001 seconds. ✔ Test foo() passed after 0.001 seconds. ✔ Suite MyTest passed after 0.001 seconds. ✔ Test run with 2 tests in 1 suite passed after 0.001 seconds.
(edited)scopeProvider called
が合計5回ある。import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestScoping { func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
enter provideScope MyTest suite bar foo leave provideScope MyTest suite
◇ Test run started. ↳ Testing Library Version: 6.2 (3fdabe5392108d8) ↳ Target Platform: aarch64-unknown-linux-gnu ◇ Suite MyTest started. ◇ Test bar() started. ◇ Test foo() started. ✔ Test bar() passed after 0.001 seconds. ✔ Test foo() passed after 0.001 seconds. ✔ Suite MyTest passed after 0.001 seconds. ✔ Test run with 2 tests in 1 suite passed after 0.001 seconds.
import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { func scopeProvider(for test: Test, testCase: Test.Case?) -> MyRecursiveSuiteTrait? { print("scopeProvider called", test.name, testCase == nil ? "suite" : "test") return self } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { func scopeProvider(for test: Test, testCase: Test.Case?) -> MyRecursiveSuiteTrait? { print("scopeProvider called", test.name, testCase == nil ? "suite" : "test") return self } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
(edited)scopeProvider called MyTest suite enter provideScope MyTest suite foo bar leave provideScope MyTest suite
◇ Test run started. ↳ Testing Library Version: 6.2 (3fdabe5392108d8) ↳ Target Platform: aarch64-unknown-linux-gnu ◇ Suite MyTest started. ◇ Test foo() started. ◇ Test bar() started. ✔ Test foo() passed after 0.001 seconds. ✔ Test bar() passed after 0.001 seconds. ✔ Suite MyTest passed after 0.001 seconds. ✔ Test run with 2 tests in 1 suite passed after 0.001 seconds.
(edited)import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
import Testing struct MyRecursiveSuiteTrait: SuiteTrait & TestTrait & TestScoping { var isRecursive: Bool { true } func provideScope( for test: Test, testCase: Test.Case?, performing function: () async throws -> Void ) async throws { print("enter provideScope", test.name, testCase == nil ? "suite" : "test") defer { print("leave provideScope", test.name, testCase == nil ? "suite" : "test") } try await function() } } extension Trait where Self == MyRecursiveSuiteTrait { static var mySuperSuite: Self { Self() } } // a. ここで Suite trait として1回呼ばれる @Suite(.mySuperSuite) struct MyTest { // b. ここで Suite trait として1回呼ばれる // c. ここで Test trait として1回呼ばれる @Test func foo() { print("foo") } // d. ここで Suite trait として1回呼ばれる // e. ここで Test trait として1回呼ばれる @Test func bar() { print("bar") } }
enter provideScope foo() test foo leave provideScope foo() test enter provideScope bar() test bar leave provideScope bar() test
◇ Test run started. ↳ Testing Library Version: 6.2 (3fdabe5392108d8) ↳ Target Platform: aarch64-unknown-linux-gnu ◇ Suite MyTest started. ◇ Test foo() started. ◇ Test bar() started. ✔ Test foo() passed after 0.001 seconds. ✔ Test bar() passed after 0.001 seconds. ✔ Suite MyTest passed after 0.001 seconds. ✔ Test run with 2 tests in 1 suite passed after 0.001 seconds.