안녕하세요 코찐입니다. 패스트캠퍼스에서 진행하는 리팩토링 완독반 수강하고 있습니다.
매주 가이드 영상이 올라오는데 내용이 마음에 들지 않아서 책 내용을 swift로 포팅하면서 직접 해보기로 했습니다.
자바스크립트는 동적 타입을 사용하고 있어서 클래스를 json으로 생성하기 편하게 되어있습니다. Swift에서는 조금 불편하게 되어 있습니다. JSON을 Decodable 통해서 객체로 변환시키는 작업을 했습니다.
final class Producer: Decodable {
enum CodingKeys: String, CodingKey {
case name
case cost
case production
}
weak var province: Province?
let name: String
private(set) var cost: Int
private(set) var production: Int
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
cost = try values.decode(Int.self, forKey: .cost)
production = try values.decodeIfPresent(Int.self, forKey: .production) ?? 0
}
func cost(_ newValue: String) {
guard let cost = Int(newValue) else { return }
self.cost = cost
}
func production(amountStr: String) {
guard let amonut = Int(amountStr) else { return }
province?.totalProduction += amonut - production
production = amonut
}
}
책에서는 Mocha 라이브러리를 사용하는데, 문법이 Quick과 닮아 있어서 Quick을 사용하기로 했습니다.
아래와 같은 형태로 사용할 수 있습니다.
final class Chapter4Spec: QuickSpec {
private func sampleProvinceData() -> Data {
let json: [String : Any] = [
"name": "Asia",
"producers": [
["name": "Byzantium", "cost": 10, "production": 9],
["name": "Attalia", "cost": 12, "production": 10],
["name": "Byzantium", "cost": 10, "production": 6]
],
"demand": 30,
"price": 20
]
return try! JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
}
override func spec() {
describe("province") {
it("shortfall") {
let asia = try JSONDecoder().decode(Province.self, from: self.sampleProvinceData())
expect(asia.shortfall).to(equal(5))
}
}
}
}
테스트가 정상적으로 작동하고 있는 것을 체크하기 위해 일부러 오류를 주입해보기도 합니다.
오류를 주입하면 아래와 같이 실제값, 기대값을 확인할 수 있습니다.
테스트 코드를 하나 더 추가합니다.
describe("province") {
it("shortfall") {
let asia = try JSONDecoder().decode(Province.self, from: self.sampleProvinceData())
expect(asia.shortfall).to(equal(5))
}
it("profit") {
let asia = try JSONDecoder().decode(Province.self, from: self.sampleProvinceData())
expect(asia.profit).to(equal(230))
}
}
그러면 중복으로 사용되는 픽스처를 통합하고 싶은 마음이 생깁니다.
asia 라는 값을 공유해서 사용하지말고 각 테스트를 위해 새로 생성합니다.
그래야 독립적으로 테스트 되는 것을 보장할 수 있습니다.
describe("province") {
var asia: Province!
beforeEach {
asia = try! JSONDecoder().decode(Province.self, from: self.sampleProvinceData())
}
it("shortfall") {
expect(asia.shortfall).to(equal(5))
}
it("profit") {
expect(asia.profit).to(equal(230))
}
}
beforeEach 클로저에는 throws 처리가 안되어 있어서 try! 로 처리했습니다. beforeEach는 검증할 대상이 아니라고 판단해서 throws 처리가 안되어 있는 것으로 보입니다.
그리고 픽스처를 수정하는 예제입니다.
테스트 하나를 추가합니다.
it("change production") {
asia.producers[0].production(amountStr: "20")
expect(asia.shortfall).to(equal(-6))
expect(asia.profit).to(equal(292))
}
이 테스트를 추가해본 덕분에 포팅한 클래스의 로직에 문제가 있는 것을 발견하게 되었습니다!!
producers에게 province를 주입해주도록 클래스를 구성해뒀는데, 실제로는 주입해주지 않고 있었습니다.
demandCost 로직의 비교문도 반대로 되어 있었습니다.
로직을 수정하고 모든 테스트가 통과하는 것을 확인합니다.
이렇게 실제로 테스트를 작성해보니 단순한 포팅 작업에도 테스트가 도움이 되는 것을 경험할 수 있었습니다.
경계 조건 검사하기
컬렉션이 비어있을 때, 0 일때, 음수일 때 등 문제가 생길 가능성이 있는 부분을 테스트합니다.
describe("no producers") {
var noProducers: Province!
beforeEach {
let json: [String: Any] = [
"name": "No producers",
"producers": [],
"demand": 30,
"price": 20
]
let data = try! JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
noProducers = try! JSONDecoder().decode(Province.self, from: data)
}
it("shortfall") {
expect(noProducers.shortfall).to(equal(30))
}
it("profit") {
expect(noProducers.profit).to(equal(0))
}
}
자바스크립트에 대한 지식 없이 스위프트로 포팅해서 잘못된 부분도 있겠지만 직접 코드를 작성해보니 재밌었습니다.
테스트 코드의 필요성을 한 번더 느끼게 되었습니다.
'CS > Refactoring' 카테고리의 다른 글
[Refactoring] Chapter 6: 기본적인 리팩터링 (2) | 2021.04.26 |
---|---|
[Refactoring] Chapter 3, 4 과제 (0) | 2021.04.17 |
[Refactoring] Chapter 4. 테스트 구축하기 (0) | 2021.04.17 |
[Refactoring] Chapter 3 요약: 코드에서 나는 악취 (0) | 2021.04.16 |
[Refactoring] Chapter2 과제 (0) | 2021.04.13 |