1. Measure time interval code snippet πŸ‘©β€πŸ’»

κ°„λ‹¨ν•œ μ½”λ“œ ν…ŒμŠ€νŠΈ μ‹œ μœ μš©ν•œ λ°©λ²•μœΌλ‘œ, μ½”λ“œ μ‹€ν–‰ μ „ν›„μ˜ μ‹œκ°„μ°¨λ₯Ό 좜λ ₯ν•œλ‹€.

func measureTimeInterval(closure: () -> ()) -> TimeInterval {
    let start = CFAbsoluteTimeGetCurrent()
    closure()
    return CFAbsoluteTimeGetCurrent() - start
}

func testCode(_ closure: () -> ()) {
    print("Execution time: \(measureTimeInterval(closure: closure))")
}


testCode {
    _ = (1...10000).reduce(0) {
        $0 + $1
    }
}
Execution time: 0.057237982749938965

2. Perform Test using XCTest πŸ‘©β€πŸ’»

1. Example Cases

Swift Playground 같은 κ³³μ—μ„œ μ‚¬μš©ν•˜κΈ°μ—λŠ” μœ„ 방법이 쉽고 νŽΈν•˜λ‹€. ν•˜μ§€λ§Œ μœ„ 방법은 μ‹€μ œλ‘œ 앱을 κ°œλ°œν•˜λ©΄μ„œ μ‚¬μš©ν•  수 μžˆλŠ” 방법은 μ•„λ‹ˆλ‹€. μ•„λž˜ μ£Όμ‚¬μœ„μ˜ isEven을 Computed Properties둜 μ‚¬μš©ν•  λ•Œμ™€ Methods둜 μ‚¬μš©ν•  λ•Œ μ–΄λ–€ 차이가 μžˆλŠ”μ§€ XCTestλ₯Ό μ΄μš©ν•΄ μ„±λŠ₯ ν…ŒμŠ€νŠΈλ₯Ό ν•΄λ³΄λ„λ‘ν•˜μž.

struct Dice {
    @OneToSix var currentDice: Int
    var isEven: Bool {
        currentDice.isMultiple(of: 2)
    }
    
    mutating func rollDice() {
        currentDice = Int.random(in: 1...6)
    }
    
    func printDice() {
        print("The current numer of dice is \(currentDice), then is \(isEven ? "even" : "odd") number.")
    }
}

@propertyWrapper
struct OneToSix {
    private var number = 1
    var wrappedValue: Int {
        get { number }
        set {
            switch newValue {
            case 1...6: number = newValue
            default: fatalError("Out of range.")
            }
        }
    }
}

2. Make Sample Project

Playgroundκ°€ μ•„λ‹Œ Xcode Projectλ₯Ό μƒμ„±ν•˜λ„λ‘ν•œλ‹€. μ΄λ•Œ ν”„λ‘œμ νŠΈλ₯Ό App으둜 생성할 경우 μ•„λž˜μ™€ 같이 Include Testλ₯Ό μ„ νƒν•˜λ©΄ ν”„λ‘œμ νŠΈμ— Tests Groupκ³Ό UITests Group이 기본으둜 ν¬ν•¨λ˜λ―€λ‘œ 3. Add Test Target Groups 둜 κ±΄λ„ˆλ›΄λ‹€.

New Project App


ν•˜μ§€λ§Œ ν”„λ‘œμ νŠΈλ₯Ό Command Line Tool둜 생성할 경우 Include Testsλ₯Ό μ„ νƒν•˜λŠ” 것이 λΆˆκ°€λŠ₯ν•˜λ‹€. λ˜ν•œ App으둜 λ§Œλ“  ν”„λ‘œμ νŠΈλ”λΌλ„ Testsλ₯Ό ν¬ν•¨ν•˜μ§€ μ•Šκ³  μƒμ„±ν•œ ν”„λ‘œμ νŠΈμΌ κ²½μš°λŠ” 직접 Testsλ₯Ό μΆ”κ°€ν•΄μ€˜μ•Όν•œλ‹€.

New Project Command Line Tool


μ•„λž˜μ™€ 같이 Command Line Tool둜 ν”„λ‘œμ νŠΈλ₯Ό μ„ νƒν•˜κ³ , 이름은 Computed-Properties둜 μƒ˜ν”Œ ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•œλ‹€.

Create Sample Project


그리고 λ‹€μŒ μ½”λ“œλ₯Ό 각 Swift νŒŒμΌμ— μž‘μ„±ν•œλ‹€.

  • main.swift
import Foundation

struct App {

    func getValueFromComputedProperties() {
        let dice = DiceOne()
        (1...100000).forEach { _ in
            _ = dice.isEven
        }
    }

    func getValueFromClosures() {
        let dice = DiceTwo()
        (1...100000).forEach { _ in
            _ = dice.isEven()
        }
    }

}


  • DiceOne.swift
import Foundation

struct DiceOne {
    @OneToSix var currentDice: Int
    var isEven: Bool {
        currentDice.isMultiple(of: 2)
    }
    
    mutating func rollDice() {
        currentDice = Int.random(in: 1...6)
    }
    
    func printDice() {
        print("The current numer of dice is \(currentDice), then is \(isEven ? "even" : "odd") number.")
    }
}

@propertyWrapper
struct OneToSix {
    private var number = 1
    var wrappedValue: Int {
        get { number }
        set {
            switch newValue {
            case 1...6: number = newValue
            case ..<1: number = 1
            case 7...: number = 6
            default: fatalError("Maybe \(newValue) is not an integer value.")
            }
        }
    }
}


  • DiceTwo.swift
import Foundation

struct DiceTwo {
    @OneToSix var currentDice: Int
    func isEven() -> Bool {
        currentDice.isMultiple(of: 2)
    }

    mutating func rollDice() {
        currentDice = Int.random(in: 1...6)
    }

    func printDice() {
        print("The current numer of dice is \(currentDice), then is \(isEven() ? "even" : "odd") number.")
    }
}

3. Add Test Target Groups

ν”„λ‘œμ νŠΈ μ„€μ •μ—μ„œ +λ₯Ό μ΄μš©ν•΄ μΆ”κ°€λ₯Ό ν•˜κ±°λ‚˜ File-New-Target...λ₯Ό μ΄μš©ν•΄ μΆ”κ°€λ₯Ό ν•œλ‹€.

Add Target 1

(ν”„λ‘œμ νŠΈ μ„€μ •μ—μ„œ `+`λ₯Ό μ΄μš©ν•΄ μΆ”κ°€)

Add Target 2

(`File-New-Target...`λ₯Ό μ΄μš©ν•΄ μΆ”κ°€)


Unit Testing Bundle을 선택 ν›„ Computed-PropertiesTestsλΌλŠ” μ΄λ¦„μœΌλ‘œ μƒμ„±ν•œλ‹€.

Add Unit Testing Bundle 1

Add Unit Testing Bundle 2

일반적으둜 Tests Groups 이름은
Unit Testing Bundle 은 Project Name + Tests둜 λ§Œλ“€κ³ ,
UI Testing Bundle은 Project Name + UITests둜 λ§Œλ“ λ‹€.


μ•„λž˜μ™€ 같이 Computed-PropertiesTestsλΌλŠ” μ΄λ¦„μ˜ κ·Έλ£Ήκ³Ό XCTestCaseλ₯Ό μƒμ†ν•œ μƒ˜ν”Œ Classκ°€ μƒμ„±λœλ‹€.

Unit Testing Bundle Sample

4. Edit Scheme for Tests

Product-Scheme-Edit Scheme... λ˜λŠ” 단좕킀 ⌘ < λ₯Ό μ΄μš©ν•΄ ν”„λ‘œμ νŠΈμ˜ Scheme μˆ˜μ •μ— μ ‘κ·Όν•œλ‹€.

Edit Project Scheme


ν˜„μž¬ BuildλŠ” νƒ€κ²Ÿμ΄ μž‘ν˜€μžˆμ§€λ§Œ

Build Targets

TestλŠ” νƒ€κ²Ÿμ΄ μž‘ν˜€μžˆμ§€ μ•Šμ€ 것을 λ³Ό 수 μžˆλ‹€. 이것을 μž‘μ•„μ€˜μ•Όν•œλ‹€.

Non Test Targets


μ•„λž˜ 캑처λ₯Ό μ°Έκ³ ν•΄ 방금 μƒμ„±ν•œ Computed-PropertiesTests 그룹을 νƒ€κ²Ÿ 그룹으둜 μΆ”κ°€ν•œλ‹€.

Add Test Targets 1

Add Test Targets 2


이 섀정을 ν•˜μ§€ μ•ŠμœΌλ©΄ ν…ŒμŠ€νŠΈ μ‹€ν–‰ μžμ²΄κ°€ μ‹€νŒ¨λœλ‹€.

5. Add Target Memberships

ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ λͺ¨λ“  Swift νŒŒμΌμ„ Computed-PropertiesTests의 Target Membership으둜 λ“±λ‘ν•œλ‹€.

Add Target Membership 1

Add Target Membership 2

ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ λͺ¨λ“  Swift νŒŒμΌμ€ μœ„μ™€ 같이 ν…ŒμŠ€νŠΈ 그룹을 Target Membership에 체크해 λ“±λ‘ν•΄μ•Όν•œλ‹€. λ”°λΌμ„œ, DiceTwo.swift νŒŒμΌλ„ λ™μΌν•œ μž‘μ—…μ„ 해주도둝 ν•œλ‹€.


μœ„ κ²½μš°λŠ” 기쑴의 ν”„λ‘œμ νŠΈκ°€ μ§„ν–‰λ˜λŠ” 쀑간에 ν…ŒμŠ€νŠΈ 그룹을 μΆ”κ°€ν–ˆκΈ° λ•Œλ¬Έμ΄κ³ , ν…ŒμŠ€νŠΈ 그룹을 μƒμ„±ν•œ 이후 μƒˆ Swift νŒŒμΌμ„ 생성할 경우, μ•„λž˜μ™€ 같이 Target Membership을 미리 μ„€μ •ν•  수 μžˆλ‹€.

Add Target Membership 3


이 섀정을 ν•˜μ§€ μ•ŠμœΌλ©΄ ν…ŒμŠ€νŠΈλŠ” μ‹€ν–‰λ˜μ§€λ§Œ, Computed-Properties 그룹의 Classes, Structures, Protocols 같은 것듀을 찾지 λͺ» ν•œλ‹€(ν•΄λ‹Ή μ½”λ“œκ°€ ν¬ν•¨λœ Swift νŒŒμΌμ„ 찾지 λͺ» ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€).

6. Write Test Cases

  • setUpWithError : 클래슀 λ‚΄ 각 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œκ°€ μ‹€ν–‰λ˜κΈ° 전에 ν˜ΈμΆœν•  μ½”λ“œ
  • tearDownWithError : 클래슀 λ‚΄ 각 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œκ°€ μ‹€ν–‰λœ ν›„ ν˜ΈμΆœν•  μ½”λ“œ
  • testExample : Unit Testsν•  μ½”λ“œλ₯Ό μž‘μ„±ν•œλ‹€. 항상 testλΌλŠ” prefix λ₯Ό λΆ™μ—¬μ•Ό 정상적인 ν…ŒμŠ€νŠΈλ‘œ μΈμ‹ν•œλ‹€.
  • testPerformanceExample : μ„±λŠ₯ 츑정을 μœ„ν•œ λ©”μ„œλ“œλ‘œ, testλΌλŠ” prefix λ₯Ό 뢙이고, measure Closures μ•ˆμ— μ½”λ“œλ₯Ό μž‘μ„±ν•œλ‹€.


Unit TestsλŠ”

  1. Arrange : ν•„μš”μ— 따라 ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ 데이터λ₯Ό 생성 및 λ‚˜μ—΄ν•œλ‹€.
  2. Act : ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•œλ‹€.
  3. Assert : ν…ŒμŠ€νŠΈλ₯Ό κ²€μ¦ν•œλ‹€.

μˆœμ„œλ‘œ μž‘μ„±ν•œλ‹€.


  • Test
func testInitialValue_Is_Number_One() throws {
    // Arrange
    let dice = DiceOne()
    let currentDice:Int = dice.currentDice
    
    // Act
    
    // Assert
    XCTAssertEqual(currentDice, 1, "The initial value of the 'currentDice' is should be 1.")
    
}
func testCurrentDice_Is_Greater_Than_Or_Equal_To_One() throws {
    // Arrange
    var dice = DiceOne()
    
    // Act
    dice.currentDice = 0
    
    // Assert
    XCTAssertEqual(dice.currentDice, 1, "The 'currentDice' is greater than or equal to 1.")
}

func testCurrentDice_Is_Less_Than_Or_Equal_To_Six() throws {
    // Arrange
    var dice = DiceOne()
    
    // Act
    dice.currentDice = 7
    
    // Assert
    XCTAssertEqual(dice.currentDice, 6, "The 'currentDice' is less than or equal to 6.")
}


  • Test Performance
func testPerformance_GetValueFromComputedProperties() throws {
    measure {
        App().getValueFromComputedProperties()
    }
}

func testPerformnace_GetValueFromClosures() throws {
    measure {
        App().getValueFromClosures()
    }
}

λ₯Ό μΆ”κ°€ν•œλ‹€.

ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” μœ„μ™€ 같이 νŠΉμ • μΌ€μ΄μŠ€μ— λŒ€ν•΄ μ»€λ“œλ₯Ό 직접 μž‘μ„±ν•΄ ν…ŒμŠ€νŠΈ ν•  μˆ˜λ„ 있고, 이미 μ‘΄μž¬ν•˜λŠ” ν•¨μˆ˜λ‚˜ λ©”μ„œλ“œμ— argumentsλ₯Ό μΌ€μ΄μŠ€ λ³„λ‘œ μž‘μ„±ν•΄ β€˜μ„±κ³΅β€™/β€™μ—λŸ¬ μ²˜λ¦¬β€™λ₯Ό ν…ŒμŠ€νŠΈ ν•  μˆ˜λ„ μžˆλ‹€.

7. Run Tests

Run Test

μœ„ λ²„νŠΌμ„ 길게 눌러 Testλ₯Ό μ‹€ν–‰ν•˜κ±°λ‚˜, 단좕킀 ⌘ Uλ₯Ό μ‚¬μš©ν•΄ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•œλ‹€.


Test Result 1 Pass

(1 Pass)

Test Result 10000 Pass

(100,000 Pass)

Show the Report navigator 탭을 λˆ„λ₯΄κ³  ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό ν™•μΈν•œλ‹€.

μœ„ 예제의 경우 3개의 ν…ŒμŠ€νŠΈ μ½”λ“œκ°€ μ„±κ³΅μ μœΌλ‘œ ν†΅κ³Όν–ˆλ‹€.

  1. μ£Όμ‚¬μœ„ μ΄ˆκΉƒκ°’μ€ 1
  2. μ£Όμ‚¬μœ„ μ΅œμ†Ÿκ°’μ€ 1
  3. μ£Όμ‚¬μœ„ μ΅œλŒ“κ°’μ€ 6

λ˜ν•œ 2개의 μ„±λŠ₯ ν…ŒμŠ€νŠΈκ°€ μ„±κ³΅ν–ˆλ‹€.

  • Time을 보면 λ°˜λ³΅μ„ ν•˜μ§€ μ•Šμ„ 경우 Closuresκ°€ κ·Όμ†Œν•˜κ²Œ λΉ λ₯΄μ§€λ§Œ,
  • κ°’ 쑰회λ₯Ό 100,000번 ν•  경우 Computed Propertiesκ°€ κ·Όμ†Œν•˜κ²Œ λΉ λ₯Έ 것을 확인할 수 μžˆλ‹€.
    (measure λ₯Ό μ‚¬μš©ν•  경우 Duration 은 measure λ₯Ό μž‘λ™ν•˜κΈ° μœ„ν•œ λ³„λ„μ˜ λΉŒλ“œ μ‹œκ°„μ„ ν¬ν•¨ν•˜λŠ” λ“― ν•˜λ‹€. λ©”μ„œλ“œλ₯Ό κ°œλ³„μ μœΌλ‘œ λŒλ €λ³΄κ±°λ‚˜ κ°€λ³κ²Œ λŒλ €λ΄λ„ 항상 5초 κ°€κΉŒμš΄ μ‹œκ°„μ΄ λ‚˜μ˜€λ©°, μ‹€μ œλ‘œ 반볡 νšŸμˆ˜μ— 영ν–₯을 λ°›λŠ” 값은 Time이닀.)

μœ„ ν…ŒμŠ€νŠΈμ˜ 경우 Closures λ˜λŠ” Computed Properties의 계산 μžμ²΄κ°€ 맀우 가볍기 λ•Œλ¬Έμ— 큰 차이가 μ—†μ§€λ§Œ 무거운 계산이 포함될 경우 μˆ˜μ • λ³΄λ‹€λŠ” μ ‘κ·Ό νšŸμˆ˜κ°€ λ§Žλ‹€λ©΄ Closures λ³΄λ‹€λŠ” Computed Propertiesλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μΊμ‹±μœΌλ‘œ 인해 μ„±λŠ₯에 이점을 κ°€μ§ˆ 것이닀.


ν…ŒμŠ€νŠΈ μ‹€ν–‰ μžμ²΄κ°€ μ‹€νŒ¨ν•˜λŠ” 경우 4. Edit Scheme for Tests λ₯Ό ν™•μΈν•˜κ³ ,
ν…ŒμŠ€νŠΈλŠ” μ‹€ν–‰λ˜μ§€λ§Œ Classes, Structures, Protocols 같은 것듀을 찾지 λͺ» ν•˜λŠ” 경우 5. Add Target Memberships λ₯Ό ν™•μΈν•œλ‹€.


3. Summary πŸ‘©β€πŸ’»

κ°’μ˜ 변경이 없어도 맀번 κ³„μ‚°ν•˜λŠ” Closures의 경우 O(N)+의 λ³΅μž‘λ„λ₯Ό κ°–μ§€λ§Œ, Computed Propertiesλ₯Ό μ‚¬μš©ν•˜λŠ” 경우 캐싱에 μ˜ν•΄ O(1)의 λ³΅μž‘λ„λ₯Ό κ°–κΈ° λ•Œλ¬Έμ— 변경이 적고 μ‘°νšŒκ°€ λ§Žμ€ 값일 λ•Œ μœ λ¦¬ν•˜λ‹€.

단, λ‹€μŒ μΌ€μ΄μŠ€λŠ” Computed Properties μ‚¬μš©μ˜ λ‚˜μœ 예둜 ν”Όν•˜λ„λ‘ ν•œλ‹€.

  • Random values
  • today’s date
  • a value from another object or singleton
  • formatting a date
  • fetching from server




Reference

  1. Aaina, jain. β€œFunctions vs Computed property β€” What to use?.” Medium, last modified Nov. 22, 2018, Functions vs Computed property.
  2. β€œUni testing best practices.” Microsoft Learn .NET, Nov. 4, 2022, Uni testing best practices.