iyOmSd/Title: Swift

[Swift] 테스트코드 Quick, Nimble, Mockingbird

냄수 2022. 3. 25. 19:18
반응형

테스트코드를 짜는데 도움을주는 라이브러리를

공부하면서 간단하게 알아보려고해요

 

제일유명한게

Quick과 Nimble이 있죠

 

Nimble

이 라이브러리의 장점은 엑스코드 XCTAssert에서 제공하는것 보다 많은 Assert들을 제공해요

expect 함수를 사용하구요

 

// Passes if 'actual' is not nil, true, or an object with a boolean value of true:
expect(actual).to(beTruthy())

// Passes if 'actual' is only true (not nil or an object conforming to Boolean true):
expect(actual).to(beTrue())

// Passes if 'actual' is nil, false, or an object with a boolean value of false:
expect(actual).to(beFalsy())

// Passes if 'actual' is only false (not nil or an object conforming to Boolean false):
expect(actual).to(beFalse())

// Passes if 'actual' is nil:
expect(actual).to(beNil())

// Passes if 'instance' is an instance of 'aClass':
expect(instance).to(beAnInstanceOf(aClass))

// Passes if 'instance' is an instance of 'aClass' or any of its subclasses:
expect(instance).to(beAKindOf(aClass))

nil과 Bool값을 비교하거나

클래스 타입을 검사하는 함수가 제공되죠

 

 

또한 가독성 좋은 메시지를 제공해요

    func test_더하기() {
        var given = 100
        given += 1
        
        expect { given }.to(equal(101), description: "더하기 테스트1")
        expect { given }.to(equal(100), description: "더하기 테스트2")
        
        XCTAssertEqual(given, 100, "더하기 테스트3")
    }

XCTAssert
Nimble

 

 

Quick

BDD하도록 도와주는 프레임워크에요(Behavior - Driven - Development)

describe - 시나리오 정의에 해당하는 given 

context - 시나리오 조건에 해당하는 when

it - 시나리오 완료시 보장되는 결과에 해당하는 then

 

크게 이렇게 3개를 사용해요

그리고

beforeEach가 있는데

초기화블럭이라고 생각하면되요

각 테스트케이스마다 신선한 객체를 사용하기위해 쓰여요

 

 

사용방법은

클래스에 QuickSpec을 상속하고

spec함수를 오버라이드해서 사용하면 끝이에요

final class TestSpec: QuickSpec {
    override func spec() {
       
    }
}

 

아래처럼 사용할 클래스를 정의하고 이를 토대로 테스트 해볼거에요

enum State {
    case on, off, update
}

protocol Computer {
    var state: State { get set }
    func power(isOn: Bool)
    func update()
    func containUser(_ user: String) -> Bool
}

final class MacBookPro: Computer {
    var state: State = .off
    
    func power(isOn: Bool) {
        state = isOn ? .on : .off
    }
    
    func update() {
        state = .update
    }
    
    func containUser(_ user: String) -> Bool {
        return true
    }
}

 

final class TestSpec: QuickSpec {
    override func spec() {
        var mac: MacBookPro!
        describe("given에 해당 테스트할 대상: computer state") {
            beforeEach {
                mac = MacBookPro()
                print("mac 생성")
            }
            
            context("when 업데이트시") {
                beforeEach {
                    mac.update()
                }
                it("state = .update로 변경되야함") {
                    expect { mac.state }.to(equal(.update))
                }
            }
            
            context("when 전원on시") {
                beforeEach {
                    mac.power(isOn: true)
                }
                it("state = .on로 변경되야함") {
                    expect { mac.state }.to(equal(.on))
                }
            }
            
            context("when 전원 off시") {
                beforeEach {
                    mac.power(isOn: false)
                }
                it("state = .update로 변경되야함") {
                    expect { mac.state }.to(equal(.off))
                }
            }
        }
    }
}
Test Suite 'All tests' started at 2022-03-25 15:11:13.303
Test Suite 'prac_UnitTest_QuickTests.xctest' started at 2022-03-25 15:11:13.306
Test Suite 'TestSpec' started at 2022-03-25 15:11:13.306
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_업데이트시__state____update로_변경되야함]' started.
💻mac 생성
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_업데이트시__state____update로_변경되야함]' passed (0.008 seconds).
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_전원on시__state____on로_변경되야함]' started.
💻mac 생성
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_전원on시__state____on로_변경되야함]' passed (0.002 seconds).
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_전원_off시__state____update로_변경되야함]' started.
💻mac 생성
Test Case '-[prac_UnitTest_QuickTests.TestSpec given에_해당_테스트할_대상__computer_state__when_전원_off시__state____update로_변경되야함]' passed (0.001 seconds).
Test Suite 'TestSpec' passed at 2022-03-25 15:11:13.320.
	 Executed 3 tests, with 0 failures (0 unexpected) in 0.011 (0.014) seconds
Test Suite 'prac_UnitTest_QuickTests.xctest' passed at 2022-03-25 15:11:13.321.
	 Executed 3 tests, with 0 failures (0 unexpected) in 0.011 (0.015) seconds
Test Suite 'All tests' passed at 2022-03-25 15:11:13.326.
	 Executed 3 tests, with 0 failures (0 unexpected) in 0.011 (0.022) seconds

 

로그를 보고 beforeEach의 역할이 무엇인지 좀 느껴지시나요

describe안에 정의된 beforeEach라면

각 context가 실행되기 전에 실행되는 초기화 블럭이에요

객체를 전에 변경한 값에 방해받지않도록 새객체를 만들어서 신선하게 테스트하죠!

beforeEach를 context안에 또는 it안에도 정의할 수있어요

 

 

Mockingbird

위에서 containUser함수는 테스트하지 않았어요

유저리스트를 불러와서 해당유저가 포함되있는지를 알려주는 기능의 함수에요

통신이나 불러오는 작업이 있는 함수는 테스트하기 까다롭기때문에 가짜 객체 목업이라고 많이 부르는 가짜객체를 만들어서

통신결과 값이나 불러온 값을 임의로 지정해서 사용하죠

이러한 작업을 간편하게 해줄 수 있는 라이브러리에요

 

protocol, class(final제외)로 선언되있다면 자동으로 Mock파일로 만들어줘요

만들어진 타입은 mock()함수를 이용해서 사용할 수 있어요

 

given이라는 함수가 있는데 이함수가

함수의 매개변수에 따른 리턴값을 임의로 지정할 수 있도록 도와주고

 

verify라는 함수는

함수가 불렸는지를 검사해줘요

원하는 매개변수로 해당함수가 불렸는지까지 검사할 수 있어요

 

 

SPM으로 패키지를 가져오고

 

설정하는 방법이조금 어려울 수 있어요 문서설명이 빈약해서...

 

엑스코드 런 스크립트로 실행해도되고

터미널로해도 될거에요

우선 문서에있는데로

DERIVED_DATA="$(xcodebuild -showBuildSettings | sed -n 's|.*BUILD_ROOT = \(.*\)/Build/.*|\1|p')"

"${DERIVED_DATA}/SourcePackages/checkouts/mockingbird/mockingbird" configure "prac_UnitTest_QuickTests" -- --target "prac_UnitTest_Quick"

이 동작을해줘요

자동으로 필요한 파일들을 만들어주는 명령어에요

그러면

엑스코드에 직접 정의했던 스크립트

아래에 새로운 스크립트가 생긴걸 볼 수 있어요

기존꺼를 지우고

필요한 옵션들을 수정해서 이거를 사용하면되요

 

--outputs은 생성된 목파일경로를 지정하는거구요

"${SRCROOT}/MockingbirdMocks/Mocks.generated.swift"

이경로로 수정하면 

프로젝트안에 생성된걸 볼 수 있죠

 

테스트코드에서

mock(Computer.self)

를 입력하고

빌드한번 해주면 방금만들어진 Mocks.generated파일에

해당 클래스의 Mock코드가 생성되요

 

생성되는 범위지정은 문서에서 Thunk Pruning이라고 해요

 

 

테스트에 적용해볼게요

함수가 호출됬는지 여부를 확인하기위해

noBattery라는 함수를 추가했구요

이 두함수를 테스트 해볼거에요

func containUser(_ user: String) -> Bool

func noBattery()

 

코드보면서 설명할게요!

위에서 사용한 타입을 먼저 변경해야해요

MacBookPro타입 -> ComputerMock 타입으로 만들어진 mock타입을 써야해요

기존에사용했던 MacBookPro클래스는 이제 무의미하죠

알아서 만들어주는 Mock객체를 사용할거기때문에!

var mac: ComputerMock!

describe("유저가 있는지 검사") {
    beforeEach {
        mac = mock(Computer.self)
        print("💻mac 생성")
    }
    
    context("when 다양한 유저") {
        var user: [String]!
        beforeEach {
            user = ["김남수", "namsoo", "ns"]
            given(mac.containUser(any())).willReturn(false)
            given(mac.containUser("남수")).willReturn(true)
        }
        
        it("남수가아니면 false") {
            let result: Bool = mac.containUser(user[0])
            let result2: Bool = mac.containUser(user[1])
            let result3: Bool = mac.containUser(user[2])
            expect { [result, result2, result3] }.to(equal([false, false, false]))
        }
        
        it("남수면 true") {
            let result: Bool = mac.containUser("남수")
            expect { result }.to(equal(true))
        }
    }
}

 

var mac: ComputerMock! - 자동으로 생성해주는 타입!  타입이름은 클래스 + Mock 형식으로 생성되요

 

mac = mock(Computer.self) - 해당타입에 대한 목객체를 생성해서 넣어주는 과정!

 

 

given(mac.containUser(any())).willReturn(false) - mac.containUser(아무 매개변수)인경우에 false를 리턴한다
given(mac.containUser("남수")).willReturn(true) - mac.containUser("남수")인경우에 true를 리턴한다

순서가 중요해요

반대로하면 전체에 적용되기때문에 남수의 값은 사라지겠죠

 

 

마지막으로 함수의 호출여부를 검사해볼거에요 

describe("맥북 배터리 없을때 전원 종료여부") {
    var macPro: MacBookPro!
    beforeEach {
        macPro = MacBookPro(core: mac)
    }
    
    context("when 배터리가 없다면") {
        beforeEach {
            macPro.noBattery()
            given(mac.power(isOn: false)).willReturn()
        }
        
        it("power off함수가 호출됨") {
            verify(mac.power(isOn: false)).wasCalled()
        }
    }
}

final class MacBookPro {
    let computer: Computer
    
    init(core: Computer) {
        self.computer = core
    }
    
    func power(isOn: Bool) {
        computer.power(isOn: isOn)
    }
    
    func update() {
        computer.update()
    }
    
    func containUser(_ user: String) -> Bool {
        computer.containUser(user)
    }
    
    func noBattery() {
        power(isOn: false)
    }
}

간단하게 noBattery호출시

내부적으로 power함수를 호출하게해두고

이를 검사하는 테스트 로직을 구현한거에요

 

verify(mac.power(isOn: false)).wasCalled() - mac의 power(isOn: false)함수가 호출되었는가?

만약 매개변수가 isOn: true라면 틀린케이스가 되요

 

모킹버드의 메서드를 사용하려면 무조건 given을 통해서 초기화를 해줘야해요 

여기서는 내부에 power를 사용하니까

power를 given을통해서 정의해줬구요

안하면 에러나니까 참고하시구요!

 

 

 

 

반응형