일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- HIG
- combine
- MVVM
- UICollectionView
- map
- Observable
- Clean Code
- 리펙토링
- 클린 코드
- clean architecture
- Protocol
- uitableview
- swiftUI
- 리펙터링
- RxCocoa
- uiscrollview
- tableView
- 리팩토링
- SWIFT
- UITextView
- swift documentation
- Human interface guide
- Xcode
- ribs
- rxswift
- ios
- collectionview
- 애니메이션
- Refactoring
- 스위프트
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] Clean Code(클린 코드) - 8. Unit Test (단위 테스트) 본문
[iOS - swift] Clean Code(클린 코드) - 8. Unit Test (단위 테스트)
jake-kim 2021. 11. 19. 22:39Unit Test 코드가 중요한 이유
- 테스트 케이스가 있으면, 실제 코드를 변경하는 것이 두렵지 않은 장점이 존재
- 유연성, 유지보수성, 재사용성을 제공
- 테스트 케이스가 있으면, 실제 코드를 변경할 때 테스트 케이스를 사용하여 수정한 코드가 잘 돌아가는지 테스트할 수 있기 때문에 결함율이 낮아지는 효과 (= 유연성과 유지보수성)
- 테스트 케이스를 작성해 놓으면 해당 테스트 케이스는 계속 사용할 수 있으므로 재사용성 제공
- 아키텍처가 아무리 유연하고 설계를 아무리 잘 나누었더라도, 테스트 케이스가 없다면 변경에 주저할 수 밖에 없는 상황이 발생
- 쌓인 테스트 케이스, 즉 테스트 슈트는 설계와 아키텍처를 변경하기 쉬워지므로, 이런 것들을 최대한 깨끗하게 보존하는 열쇠
TDD의 개념
Test Driven Development
- TDD의 3가지 단계
1) 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는 것
2) 컴파일은 실패하지 않으면서 실패하는 정도로만 단위 테스트 작성
3) 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성
- 위 3가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 형성
- 실제 코드를 전부 테스트하는 테스트 케이스가 생성
- 실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제도 유발 > 테스트 케이스도 깨끗함을 유지하는 것이 중요
깨끗한 테스트 코드를 유지해야 하는 이유
- 지저분한 테스트 코드를 작성하느니 테스트 코드가 없는 편이 나을 정도의 영향
- 실제 코드가 변경되면 테스트 코드도 변경되어야 하는데, 테스트 코드가 복잡할수록 테스트 코드를 변경하기가 어려운 상황
- 실제 코드보다 테스트 코드를 변경하는 시간이 더욱 오래걸리는 현상
- 만약 쌓여있는 테스트 코드, 즉 `테스트 슈트`가 없다면 자신이 수정한 코드가 제대로 도는지 확인이 어려움으로 결함율이 높아지는 현상이 발생함으로 깔끔한 테스트 코드를 유지하는것이 중요
- 급한 상황이라도 테스트 코드를 실제 코드와 같이 깨끗한 코드로 유지하는 태도가 중요
깨끗한 테스트 코드를 유지하는 방법
- 가장 중요한점은 가독성
- 가독성은 실제 코드보다 테스트 코드에 더욱 중요
- 테스트 코드는 최소의 표현으로 많은 것을 나타내야함
ex) 중복되는 코드가 많은 경우
WRONG
- crawler에게 페이지를 주고, 해당 페이지에 값을 입력 후 예상하는 값을 얻어오는지 확인하는 테스트 코드
class ExTestCodeTests: XCTestCase {
var crawler: Crawler!
var request: Request!
override func setUpWithError() throws {
crawler = Crawler()
request = Request()
}
func test_whenNormally_thenGetPageHieratchyAsXml() {
crawler.addPage("root", "PageOne")
crawler.addPage("root", "PageTwo")
crawler.addPage("root", "PageThree")
request.setResource("root")
request.addInput("type", "page")
let responder = Responder()
let response = responder.makeResponse(request)
let xml = response.getContent()
XCTAssertEqual("test/xml", response.getContentType())
XCTAssertTrue("<name>PageOne</name>".contains(xml))
XCTAssertTrue("<name>PageTwo</name>".contains(xml))
XCTAssertTrue("<name>PageThree</name>".contains(xml))
}
}
RIGHT
ex) 불필요하게 중복되는 코드를 없앤 경우
- 위 코드와 비교하면 훨씬 가독성이 있는 코드로 변환
- 나중에 테스트 코드를 수정할 때도 더욱 변경하기 쉬운 코드
func test_whenNormally_thenGetPageHieratchyAsXml() {
makePage("pageOne", "pageOne.ChildOne", "pageTwo")
let response = submitRequest("root", "type:pages")
XCTAssert(response.isXml)
XCTAssertTrue(response.xml.contains("<name>PageOne</name>") && response.xml.contains("<name>PageTwo</name>") && response.xml.contains("<name>ChildOne</name>"))
}
ex2) 온도가 급겹하게 떨어지면, 경보, 온풍기, 송풍기가 모두 가동되는지 확인하는 코드
WRONG
- 반복되는 코드가 존재
- heaterState()라는 상태를 보고서는 왼쪽으로 눈길을 돌려 XCTAssertTrue를 읽어야 하는 번거로움 존재
func test_whenTurhnOnLowTempAlarmAtThreashold_thenOperatingDevice() {
hw.setTemp(WAY_TOO_COLD)
controller.tic()
XCTAssertTrue(hw.heaterState())
XCTAssertTrue(hw.blowerState())
XCTAssertFalse(hw.coolerState())
XCTAssertFalse(hw.highTempAlarm())
XCTAssertTrue(hw.lowTempAlaram())
}
RIGHT
- 반복되는 코드 리펙토링
- wayTooCold()라는 함수를 만들어 숨겨서 사용
- H 대문자는 켜짐, h 소문자는 꺼짐을 의미
func getStage() {
var state = ""
state += heater ? "H" : "h"
state += blower ? "B" : "b"
state += cooler ? "C" : "c"
state += hiTempAlarm ? "H" : "h"
state = loTempAlarm ? "L" : "l"
return state
}
func test_whenTurnOnCoolerAndBlowerIfTooHot_thenOperatingDevice() {
tooHot()
XCTAssertEqual("hBChl", hw.getState())
}
func test_whenTurnOnHeaterAndBlowerIfTooCold_thenOperatingDevice() {
tooCold()
XCTAssertEqual("HBchl", hw.getState())
}
func test_whenTurnOnHiTempAlarmAtThreshold_thenOperatingDevice() {
wayTooHot()
XCTAssertEqual("hBCHl", hw.getState())
}
func test_whenTurnOnLoTempAlarmAtThreshold_thenOperatingDevice() {
wayTooCold()
XCTAssertEqual("HBcHL", hw.getState())
}
테스트 함수 하나 당 assert 하나
- 위 예제 코드와 같이 테스트 함수 하나 당 assert 구문이 하나이면 코드를 이해하고 쉽고 빠른 장점이 존재
- 함수 이름을 given, when, then으로 나누어서 사용하여 가독성을 높일 것
WRONG
- 함수 내부 블록에 given - when - then으로 되어있지 않은 경우
func test_whenNormally_thenGetPageHieratchyAsXml2() {
makePage("pageOne", "pageOne.ChildOne", "pageTwo")
let response = submitRequest("root", "type:pages")
XCTAssert(response.isXml)
XCTAssertTrue(response.xml.contains("<name>PageOne</name>") && response.xml.contains("<name>PageTwo</name>") && response.xml.contains("<name>ChildOne</name>"))
}
RIGHT
- 함수 내부 안에 given - when - then으로 되어 있어서 이해하기 훨씬 쉬운 코드
func test_whenNormally_thenGetPageHierarchyAsXml() {
givenPages("pageOne", "pageOne.ChildOne", "pageTwo")
whenRequestIsIssued("root", "type: pages")
thenResponseSouldBeXML()
}
테스트 함수 하나 당 개념 하나
- 테스트 함수마다 한 개념만 테스트하는 의미
ex) 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수
WRONG
- 테스트 함수 하나에 여러개의 개념을 테스트하는 형태
- 아래와 같은 테스트 함수는 3개로 분리할 것
- assert 문 수가 많은 상태
func testAddMonths() {
let d1 = SerialDate.createInstance(31, 5, 2004)
let d2 = SerialDate.addMonths(1, d1)
XCTAssertEqual(30, d2.getDayOfMonth())
XCTAssertEqual(6, d2.getMonth())
XCTAssertEqual(2004, d2.getYYYY())
let d3 = SerialDate.addMonths(2, d1)
XCTAssertEqual(30, d3.getDayOfMonth())
XCTAssertEqual(6, d3.getMonth())
XCTAssertEqual(2004, d3.getYYYY())
let d4 = SerialDate.addMonths(2, SerialDate.addMonths(1, d1))
XCTAssertEqual(30, d4.getDayOfMonth())
XCTAssertEqual(6, d4.getMonth())
XCTAssertEqual(2004, d4.getYYYY())
}
Unit Test의 FIRST 규칙
- First
- 자주 실행시키는 테스트 함수가 오래걸린다면 생산성, 코드 정리를 꺼리게 되므로 빠르게 실행되는 코드로 설계
- Independent
- 각 테스트가 서로 종속적이면, 하나가 실패할 경우 다른것에도 영향을 미치므로 서로 의존하지 않도록 설정
- Repeatable
- 테스트는 어느 환경에서도 반복 가능하도록 설계 (네트워크 연결이 되지 않은 상황에서도 실행 되도록 설계)
- Self-Validating
- 테스트는 Bool값으로 결과를 내야하므로, log값을 읽고 해석하게 되면 주관적이므로 설계하지 말것
- Timely
- 테스트는 실제 코드를 구현하기 직전에 구현해야 적합
- 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어려운 경우가 많이 존재
* 참고: Clean Code (로버트 C. 마틴)
'Clean Code (클린 코드)' 카테고리의 다른 글
[iOS - swift] Clean Code(클린 코드) - 10. 시스템 (Abstract Factory 패턴, DI) (0) | 2021.11.23 |
---|---|
[iOS - swift] Clean Code(클린 코드) - 9. 클래스 (SRP, Cohesion) (0) | 2021.11.23 |
[iOS - swift] Clean Code(클린 코드) - 7. 소프트웨어의 경계 (0) | 2021.11.18 |
[iOS - swift] Clean Code(클린 코드) - 6. 오류 처리 (0) | 2021.11.17 |
[iOS - swift] Clean Code(클린 코드) - 5. 객체와 자료 구조 (객체지향, 절차지향 장단점) (0) | 2021.11.17 |