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)