테스트코드를 짜는데 도움을주는 라이브러리를
공부하면서 간단하게 알아보려고해요
제일유명한게
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")
}
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을통해서 정의해줬구요
안하면 에러나니까 참고하시구요!
'iyOmSd > Title: Swift' 카테고리의 다른 글
[Swift] Combine의기본 Publisher이란? (0) | 2022.06.13 |
---|---|
[Swift] Xcode 템플릿 만들기 (0) | 2022.04.26 |
[Swift] iCloud 연동 CloudKit 사용하기(2/2) - 데이터 연결 (0) | 2022.02.16 |
[Swift] iCloud 연동 CloudKit 사용하기(1/2) - 추가 및 설정 (0) | 2022.02.15 |
[Swift] Realm 데이터 공유 (Widget과 Main앱 데이터 공유) AppGroup (0) | 2022.02.11 |