Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

良いテストコードのために悪いテストコードを理解する - 不安定なテスト編: iOSアプリ開発...

yimajo
June 24, 2024

良いテストコードのために悪いテストコードを理解する - 不安定なテスト編: iOSアプリ開発ユニットテストの場合

「モバイルアプリ開発における良いテストコードの考え方」の発表資料です。
https://trident-qa.connpass.com/event/320151/

yimajo

June 24, 2024
Tweet

More Decks by yimajo

Other Decks in Programming

Transcript

  1. Ҿ༻: Google Testing Blog / Test Size https://testing.googleblog.com/2010/12/test-sizes.html ಛ௃ Small

    Medium Large ωοτϫʔΫΞΫηε ͍͍͑ ϩʔΧϧϗετͷΈ ͸͍ σʔλϕʔε ͍͍͑ ͸͍ ͸͍ ϑΝΠϧγεςϜ΁ͷΞΫηε ͍͍͑ ͸͍ ͸͍ ֎෦γεςϜΛ࢖༻͢Δ ͍͍͑ ਪ঑͞Εͳ͍ ͸͍ ෳ਺ͷεϨου ͍͍͑ ͸͍ ͸͍ Sleep໋ྩ ͍͍͑ ͸͍ ͸͍ γεςϜϓϩύςΟ ͍͍͑ ͸͍ ͸͍ ੍ݶ࣌ؒ(ඵ) 60 300 900 ఆٛ: Small, Medium, Large Testͷఆٛ 🐲
  2. Ҿ༻: Google Testing Blog / Test Size https://testing.googleblog.com/2010/12/test-sizes.html ಛ௃ Small

    Medium Large ωοτϫʔΫΞΫηε ͍͍͑ ϩʔΧϧϗετͷΈ ͸͍ σʔλϕʔε ͍͍͑ ͸͍ ͸͍ ϑΝΠϧγεςϜ΁ͷΞΫηε ͍͍͑ ͸͍ ͸͍ ֎෦γεςϜΛ࢖༻͢Δ ͍͍͑ ਪ঑͞Εͳ͍ ͸͍ ෳ਺ͷεϨου ͍͍͑ ͸͍ ͸͍ Sleep໋ྩ ͍͍͑ ͸͍ ͸͍ γεςϜϓϩύςΟ ͍͍͑ ͸͍ ͸͍ ੍ݶ࣌ؒ(ඵ) 60 300 900 ఆٛ: Small, Medium, Large Testͷఆٛ 🐲
  3. Ҿ༻: Google Testing Blog / Test Size https://testing.googleblog.com/2010/12/test-sizes.html ಛ௃ Small

    Medium Large ωοτϫʔΫΞΫηε ͍͍͑ ϩʔΧϧϗετͷΈ ͸͍ σʔλϕʔε ͍͍͑ ͸͍ ͸͍ ϑΝΠϧγεςϜ΁ͷΞΫηε ͍͍͑ ͸͍ ͸͍ ֎෦γεςϜΛ࢖༻͢Δ ͍͍͑ ਪ঑͞Εͳ͍ ͸͍ ෳ਺ͷεϨου ͍͍͑ ͸͍ ͸͍ Sleep໋ྩ ͍͍͑ ͸͍ ͸͍ γεςϜϓϩύςΟ ͍͍͑ ͸͍ ͸͍ ੍ݶ࣌ؒ(ඵ) 60 300 900 ఆٛ: Small, Medium, Large Testͷఆٛ ʮ͜ͷࢿྉͷ࿩͸ Small/Meidum ʹ͍ͭͯͷςετͷ࿩Ͱ͢ʯ 🐲
  4. ςετͷ۩ମతͳϝϦοτ • ςετΛॻ͘ͱઃܭ͕ྑ͘ͳΔ • ʢϓϩμΫτΛʣ࣋ଓՄೳͳ΋ͷʹ͢Δ • σόοά࣌ؒͷݮগ • มߋ΁ͷ৴པͷ૿େ •

    υΩϡϝϯςʔγϣϯͷվળ • ϨϏϡʔͷ୯७Խ • ࢥྀʹ෋Ήઃܭ • ߴ଎Ͱߴ඼࣭ͳϦϦʔε 🤖
  5. ςετͷ۩ମతͳϝϦοτ • ςετΛॻ͘ͱઃܭ͕ྑ͘ͳΔ • ʢϓϩμΫτΛʣ࣋ଓՄೳͳ΋ͷʹ͢Δ • σόοά࣌ؒͷݮগ • มߋ΁ͷ৴པͷ૿େ •

    υΩϡϝϯςʔγϣϯͷվળ • ϨϏϡʔͷ୯७Խ • ࢥྀʹ෋Ήઃܭ • ߴ଎Ͱߴ඼࣭ͳϦϦʔε ʮςετΛॻ࣌ؒ͘΍࣮ߦ࣌ؒͷσϝϦοτ͸ແࢹͳͷʁʯ 🤖
  6. ࠓճ࿩͢ෆ҆ఆͳςετͷݪҼ෼ྨ • ݪҼ • ݪҼ1:࣮ߦॱংʹґଘ͢Δςετ • ݪҼ2: ࣮ߦ࣌·Ͱ஋͕ܾఆ͠ͳ͍࠶ݱੑͷͳ͍ςετ • ݪҼ3:

    ϑϨʔϜϫʔΫཧղෆ଍ • ݪҼ͕ද໘Խ͠΍͍͢: • CIͳͲͷςετ࣮ߦϚγϯͷϦιʔεෆ଍ Swift 5.10Ͱݕূ
  7. ෆ҆ఆͳݪҼ: ࣮ߦॱংʹґଘ͢Δςετ class Singleton { static let shared = Singleton()

    private init() {} var value: Int = 0 var unit: String = "g" } ———————————————————————-———————————————————————- final class SampleTests: XCTestCase { func test_value͕1000Ͱunit͕g͸_1000gͱදࣔ͞ΕΔ() { Singleton.shared.value = 1000 ɹɹɹɹ // ͜͜Ͱ୅ೖ͠Θ͢Ε͍ͯΔ͕ॳظ஋͕Χόʔͯ͠͠·ͬͯΔ XCTAssertEqual(Singleton.shared.value, 1000) XCTAssertEqual(Singleton.shared.unit, “g") // ԼͷςετΑΓઌʹ࣮ߦ͞ΕΔͱॳظ஋͕࢖ΘΕςετ͸੒ޭ } func test_value1Ͱunit͕kg͸_1kgͱදࣔ͞ΕΔ() { Singleton.shared.value = 1 Singleton.shared.unit = "kg" XCTAssertEqual(Singleton.shared.value, 1) XCTAssertEqual(Singleton.shared.unit, "kg") } } 🐲 🤖
  8. ෆ҆ఆͳݪҼ: ࣮ߦॱংʹґଘ͢Δςετ class Singleton { static let shared = Singleton()

    private init() {} var value: Int = 0 var unit: String = "g" } ———————————————————————-———————————————————————- final class SampleTests: XCTestCase { func test_value͕1000Ͱunit͕g͸_1000gͱදࣔ͞ΕΔ() { Singleton.shared.value = 1000 ɹɹɹɹ // ͜͜Ͱ୅ೖ͠Θ͢Ε͍ͯΔ͕ॳظ஋͕Χόʔͯ͠͠·ͬͯΔ XCTAssertEqual(Singleton.shared.value, 1000) XCTAssertEqual(Singleton.shared.unit, “g") // ԼͷςετΑΓઌʹ࣮ߦ͞ΕΔͱॳظ஋͕࢖ΘΕςετ͸੒ޭ } func test_value1Ͱunit͕kg͸_1kgͱදࣔ͞ΕΔ() { Singleton.shared.value = 1 Singleton.shared.unit = "kg" XCTAssertEqual(Singleton.shared.value, 1) XCTAssertEqual(Singleton.shared.unit, "kg") } } ʮγϯάϧτϯͳΜͯ࢖ Θͳ͚Ε͹͍͍ͷʹ…ʯ 🐲 🤖
  9. ෆ҆ఆͳݪҼ: ࣮ߦॱংʹґଘ͢Δςετ class Singleton { static let shared = Singleton()

    private init() {} var value: Int = 0 var unit: String = "g" } ———————————————————————-———————————————————————- final class SampleTests: XCTestCase { func test_value͕1000Ͱunit͕g͸_1000gͱදࣔ͞ΕΔ() { Singleton.shared.value = 1000 ɹɹɹɹ // ͜͜Ͱ୅ೖ͠Θ͢Ε͍ͯΔ͕ॳظ஋͕Χόʔͯ͠͠·ͬͯΔ XCTAssertEqual(Singleton.shared.value, 1000) XCTAssertEqual(Singleton.shared.unit, “g") // ԼͷςετΑΓઌʹ࣮ߦ͞ΕΔͱॳظ஋͕࢖ΘΕςετ͸੒ޭ } func test_value1Ͱunit͕kg͸_1kgͱදࣔ͞ΕΔ() { Singleton.shared.value = 1 Singleton.shared.unit = "kg" XCTAssertEqual(Singleton.shared.value, 1) XCTAssertEqual(Singleton.shared.unit, "kg") } } ʮγϯάϧτϯͳΜͯ࢖ Θͳ͚Ε͹͍͍ͷʹ…ʯ ʮ͜͏͍͏৔߹͸setUp() Λ࢖ͬͯॳظԽʯ 🐲 🤖
  10. class TimeUtils { // ࠓճͷςετର৅ // ݱࡏ࣌ࠁ͔ΒҾ਺ͷDateͱͷࠩΛTimeintervalͰฦ͢ϝιου func timeElapsedSince(_ date:

    Date) -> TimeInterval { Date().timeIntervalSince(date) } } ———————————————————————-———————————————————————————————————————- final class TimeUtilsTests: XCTestCase { let timeUtils = TimeUtils() func test_TimeElapsedSince() { let date = Date(timeIntervalSinceNow: -6) // 6ඵલͷDate // Action …….. ͜͜Βล͕ΊͪΌͪ͘Ό͔͔࣌ؒͬͨΒ૝ఆ͔Β͸ͣΕΔ let elapsed = timeUtils.timeElapsedSince(date) // Assertion XCTAssertEqual( elapsed, 6, accuracy: 0.1 // ڐ༰͢Δޡࠩൣғ ) } } ෆ҆ఆͳݪҼ: ࣮ߦ࣌ʹ஋͕ܾఆ͢Δಈతͳॲཧͷςετ 🤖
  11. class TimeUtils { // ࠓճͷςετର৅ // ݱࡏ࣌ࠁ͔ΒҾ਺ͷDateͱͷࠩΛTimeintervalͰฦ͢ϝιου func timeElapsedSince(_ date:

    Date) -> TimeInterval { Date().timeIntervalSince(date) } } ———————————————————————-———————————————————————————————————————- final class TimeUtilsTests: XCTestCase { let timeUtils = TimeUtils() func test_TimeElapsedSince() { let date = Date(timeIntervalSinceNow: -6) // 6ඵલͷDate // Action …….. ͜͜Βล͕ΊͪΌͪ͘Ό͔͔࣌ؒͬͨΒ૝ఆ͔Β͸ͣΕΔ let elapsed = timeUtils.timeElapsedSince(date) // Assertion XCTAssertEqual( elapsed, 6, accuracy: 0.1 // ڐ༰͢Δޡࠩൣғ ) } } ෆ҆ఆͳݪҼ: ࣮ߦ࣌ʹ஋͕ܾఆ͢Δಈతͳॲཧͷςετ ʮݱࡏ࣌ࠁʹཔΔͱςετ ɹ͢ΔCIͷϚγϯύϫʔʹ΋ ɹґଘ͢Δͷʹ…ʯ 🤖
  12. class TimeUtils { func timeElapsedSince(date: Date, currentDate: Date = Date())

    -> TimeInterval { // ςετ͢Δର৅ͷॲཧ͸࣮ߦ࣌ʹܾ·Βͳ͍Α͏ʹ͠࠶ݱੑΛߴΊΔ currentDate.timeIntervalSince(date) } } ———————————————————————-———————————————————————-———————————————————————- final class TimeUtilsTests: XCTestCase { let timeUtils = TimeUtils() // ࠓճͷςετର৅ func test_TimeElapsedSince2() { let currentDate = Date(timeIntervalSince1970: 0) // 1970/1/1ͷ0࣌ let date = Date(timeIntervalSince1970: -6) // 1970/1/1ͷ0͔࣌Β6ඵલ // Action let elapsed = timeUtils.timeElapsedSince( date: date, currentDate: currentDate ) // Assertion ɹ XCTAssertEqual( elapsed, 6, accuracy: 0.1 // ڐ༰͢Δޡࠩൣғ ) } } ෆ҆ఆͳݪҼ: ࣮ߦ࣌ʹ஋͕ܾఆ͢Δಈతͳॲཧͷςετ
  13. ิ଍: ࣮ߦ࣌ʹಈతʹ஋͕ܾఆ͢Δ΍ͭΒ • Foundation.Date / Calendar / Locale / UUID

    • ੔਺ͳͲͷཚ਺ੜ੒ https://github.com/pointfreeco/swift-dependencies/tree/main/ Sources/Dependencies/DependencyValues ࢀߟ: pointfreeco/swift-dependencies ෆ҆ఆͳݪҼ: ࣮ߦ࣌ʹ஋͕ܾఆ͢Δಈతͳॲཧͷςετ
  14. @globalActor actor DBActor { static let shared = DBActor() }

    class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) } } ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ جૅ: globalActorࢦఆແ͠ͰActorݺͼग़͢ͱ࣮ߦ εϨου͸ঢ়گʹࠨӈ͞ΕΔʢεϨουϓʔϧ͔Βબ͹ΕΔʣ 🐲
  15. @globalActor actor DBActor { static let shared = DBActor() }

    class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) } } ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ جૅ: globalActorࢦఆແ͠ͰActorݺͼग़͢ͱ࣮ߦ εϨου͸ঢ়گʹࠨӈ͞ΕΔʢεϨουϓʔϧ͔Βબ͹ΕΔʣ 🐲
  16. @globalActor actor DBActor { static let shared = DBActor() }

    class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) } } ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ εϨουA,B,CͷՄೳੑ͸ A == B, B== C || A == B, B!= C || A != B, B== C || A == C, C !== B || جૅ: globalActorࢦఆແ͠ͰActorݺͼग़͢ͱ࣮ߦ εϨου͸ঢ়گʹࠨӈ͞ΕΔʢεϨουϓʔϧ͔Βબ͹ΕΔʣ 🐲
  17. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor DBActor { static let

    shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } 🐲
  18. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor DBActor { static let

    shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } 🐲
  19. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor DBActor { static let

    shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } 🐲
  20. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor DBActor { static let

    shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } 🐲
  21. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ εϨουA==Cͷ৔߹ Realm͸ಉҰεϨουͰར༻ ✅ ੒ޭ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor

    DBActor { static let shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } 🐲
  22. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ εϨουA==Cͷ৔߹ Realm͸ಉҰεϨουͰར༻ ✅ ੒ޭ ྫ: ҙਤͤͣRealm͕εϨουؒΛӽ͑ͯ͠·͏ 🐲 @globalActor actor

    DBActor { static let shared = DBActor() } class ThreadTests2: XCTestCase { // MainActorʢglobalActorࢦఆʣແ͠ func test_MainActorͰͳ͘async() async { print("A:", Thread.current) let otherRealm = RLMRealm.default() let task = Task { @DBActor in try! await Task.sleep(nanoseconds: 1_000_000_000 * 5) print("B:", Thread.current) } await task.value print("C:", Thread.current) XCTAssertNotNil( RealmObject.object(in: otherRealm, forPrimaryKey: "1") ) } } εϨουA != Cͷ৔߹ Realm͕ผεϨουͰར༻͞ΕΔ 🚫 ࣦഊ 🐲
  23. ෆ҆ఆͳݪҼ:OSSϑϨʔϜϫʔΫཧղෆ଍ ྫ: ผεϨουͷRealmͷॻ͖ࠐΈΛ଴͚ͭͩͰ͸ ·ͩಉظ͸׬͍ྃͯ͠ͳ͍ ಉظ͸λΠϛϯά͸ खಈSFGSFTI ·ͨ͸࣌ؒܦա ·ͨ͸ BVUPSFGSFTIʢϝΠϯεϨουͷσϑΥϧτʣ ·ͨ͸

    ͓ͦΒͦ͘ͷଞ͍Ζ͍ΖͳλΠϛϯά ͦΕͱ͸ผ εϨουͷ Realm ผεϨουͷ Realm Realmͷॻ͖ࠐΈΛݕূ͢Δ݁Ռ͕ ෆ҆ఆͳͱ͖͸ ಈ࡞͍ͯ͠ΔεϨουΛௐࠪ͠ɺ RealmͷυΩϡϝϯτͰ realm.refresh()Λௐ΂ͯΈ͍ͨɻ 🐲