-
TDD가 뭔지 한번 해보기Apple🍎/Test 2023. 12. 15. 23:15
TDD 워크 플로우 - Red, Green, Refactor
Red
- 요구사항에 대해 실패하는 테스트를 작성하는 것으로 시작
- 원하는 기능과 엣지 케이스 대해 아직 구현하지 않은 상태
- 돌려서 IDE에서 빨간불 확인하기
- 작성한 테스트가 처음에는 실패하는지 확인하는 것은 테스트가 정상적으로 작동하는지를 확인하는 중요한 절차이다.
- 항상 성공하는 의미없는 테스트거나 이미 원하는 기능이 구현되어 있지는 않은지 확인하는 절차.
Green
- 테스트를 간신히 통과할 수 있을 정도로만 간단하게 코드를 작성.
- 이 단계에서는 코드의 퀄리티를 신경쓰지 않고 일단 기능 구현에만 집중.
- IDE에서 파란불 확인할 때까지 진행.
- 테스트 통과가 가능한 가장 간단한 코드를 작성함으로써 기능 구현을 위해 진짜 필요한 부분만 작성하게 된다.
- 읽기 쉽고, 이해가 어렵지 않으며, 수정하기에 쉬운 코드 -> 새로 온 동료가 봐도 뭘 하려고 했는지 알 정도로
- 원하는 기능이 모두 구현되지 않았는데 테스트가 통과되었다. -> 별도의 다른 테스트가 필요하다.
Refactor
- Green 단계에서 모든 테스트를 통과했다면 이제 코드를 다듬는 작업이 필요하다.
- 중복제거, 공통 로직 추출 등등 코드를 개선한다.
- 테스트의 존재는 코드를 다듬는 동안 구현한 기능이 망가지지 않게 도와준다.
TDD는 이런식으로 진행된다.
- 블로깅 앱을 만든다고 가정해보자.
- 요구사항: 새로운 포스트를 쓸 때 제목의 모든 단어의 첫글자는 대문자로 시작해야한다.
- TDD 워크 플로우의 첫 단계는 RED로 실패 케이스가 필요하다.
- 실패케이스를 작성하기 위해서는 다음 사항을 고려해보자.
실패케이스 작성시 고려사항
- Precondition : 해당 메서드를 호출하기 전에 시스템의 상태가 어떠한지를 고려하자.
- Invocation: 메서드의 시그니쳐가 어떻게 되야할지? 메서드의 파라미터는 어떻게 될지를 고려하자.
- Assertion : 메서드 호출결과로 기대되는 값이 무엇이 될지를 고려하자.
- 블로깅 제목 작성 메서드에대해서 실패케이스 고려
- Precondition: X
- Invocation: String을 받아 String을 반환한다. 메서드 이름으로는 makeHeadLine가 적당할것이다.
- Assertion : 반환되는 String은 모든 단어의 첫글자가 대문자이어야한다.
makeHeadline - Red
- FirstDemoTests 폴더 | FirstDeomTests.swift 파일 | FirstDeomTests 클래스에 테스트를 작성해보자.
- Give_When_Then 으로 테스트 메서드 이름 작성: 상황X _ 제목을 작성할때 _ 입력된 단어들의 첫문자가 대문자여야한다.
// FirstDeomTests.swift func test_makeHeadLine_shouldCaptialisePassedInString( ) { }
- 블로그 제목을 만들려면 일단 블로그를 작성하는
블로거
가 있어야지.
// FirstDeomTests.swift func test_makeHeadLine_shouldCaptialisePassedInString( ) { let blogger = Blogger( ) }
- 컴파일러가
Cannot find 'Blogger' in scope
를 띄운다.
- Blogger.swift를 main Target에 작성한다.
- 테스트를 작성하다보니 Blogger 객체가 필요하므로 Blogger struct를 만들자.
// Blogger.swift import Foundation struct Blogger { }
- 블로거 객체를 만들었니 블로거가 제목을 만드는 동작을 테스트에 추가한다.
// FirstDeomTests.swift func test_makeHeadLine_shouldCaptialisePassedInString( ) { let blogger = Blogger( ) let headline = blogger.makeHeadline(from : "the Accessibility inspector") }
- 컴파일러가
Value of type 'Blogger' has no member 'makeHeadline'
을 띄운다.
makeHeadline
메서드를Blogger
struct에 추가한다.
- 테스트를 작성하다보니 Blogger struct에 makeHeadline 메서드가 필요하니 추가한다.
- 메서드 시그니쳐만 작성
테스트 코드 작성 과정으로부터 필요한 것들을 메인코드에 작성할 때는 딱 컴파일러가 불평 안 할 정도로만 추가한다.
// Blogger.swift import Foundation struct Blogger { func makeHeadline(from input: String) -> String { return "" } }
앞선 과정들에서 느낄 수 있다시피 TDD 방식의 개발은 다음과 같이 진행된다.
요구사항을 검증하기 위한 테스트코드를 먼저 작성한다.
테스트코드를 작성하는 과정에서 기능구현을 위해 필요한 것들이 명확해진다. 이를 바탕으로 프로덕션 코드를 작성한다.- 검증하려는 동작(makeHeadline)까지 작성완료했으므로 해당 동작시 기대하는 결과와 같은지를 검증하는 코드를 추가한다.
- Test 메서드에
XCTAssertEqual
func 을 추가한다.
// FirstDeomTests.swift func test_makeHeadLine_shouldCaptialisePassedInString( ) { let blogger = Blogger( ) let headline = blogger.makeHeadline(from : "the Accessibility inspector") XCTAssertEqual(headline, "The Accessibilityinspector") }
- COMMAND + U를 이용해 테스트를 돌리면 당연히 Fail이 나온다.
- 현재 makeHealine 메서드는 String을 받아 “” 을 반환하고 있으므로 예상 값과 당연히 다르다.
- 여기까지 해서 실패케이스를 작성하는 RED 단계를 완료했다.
makeHeadline - Green
- Red에서 확인한 것 처럼 현재는 “the Accessibility inspector”를 넣으면 빈문자열이 나온다.
- 현재 테스트를 통과하기 위해서는 “the Accessibility inspector”를 넣으면 “The Accessibility Inspector”가 나와야한다.
- TDD에 Green 단계에서는 간신히 테스트만 넘길정도로만 코드를 짠다.
func makeHeadline(from input: String) -> String { return "The Accessibility Inspector" }
- 위와 같이
makeHeadline
코드를 수정하면 “뭔 저런 짓을 하고 있나” 싶지만 어쨋든 테스트는 통과가 가능하다. -> 멍청해도 간단했다!! - 우리가 원하는 기능(각 단어 첫글자 대문자로 만들기)은 수행하지 못하고 테스트만 간신히 통과하는 쓸모없는 코드 이지만 이 코드를 작성하는 것을 통해 우리는 다른 테스트가 더 필요하다는 것을 알 수 있다.
makeHeadline - Refactor
- 원하는 기능을 구현하기 위한 다음 테스트를 작성하기전에 먼저 현재까지 짠 코드를 리팩터링하는 시간을 갖자.
- 리펙터링 전
// FirstDeomTests.swift func test_makeHeadLine_shouldCaptialisePassedInString( ) { let blogger = Blogger( ) let headline = blogger.makeHeadline(from : "the Accessibility inspector") XCTAssertEqual(headline, "The Accessibilityinspector") }
- 리팩터링 후
- input, result, expected 변수를 도입, 변수의 이름을 통해 이 테스트의 목적을 명확히 할 수 있다.
func test_makeHeadline_shouldCapitalisePassedInString() { let blogger = Blogger() let input = "the Accessibility inspector" let result = blogger.makeHeadline(from: input) let expected = "The Accessibility Inspector" XCTAssertEqual(result, expected) }
- 리펙터링을 한 후에도 여전히 테스트는 통과하는지를 확인한다.
테스트가 더 필요한지 확인하기.
- 현재 원하는 기능이 전혀 구현되어 있지만 테스트가 통과하고 있다. -> 다른 테스트가 필요하다.
- TDD에서 테스트 작성의 첫번째는 실패케이스 작성이다. -> 빨간불이 먼저 들어와야한다.
- 그러면 원하는 기능에 맞는 테스트를 위해 실패케이스를 먼저 만들자.
- makeHeadline을 return 값을 다음과 같이 수정하면
func makeHeadline(from input: String) -> String { return "The Accessibility" }
- 같은 테스트가 통과하지 못한다. (Inspector 부분이 없음)
func test_makeHeadline_shouldCapitalisePassedInString() { let blogger = Blogger() let input = "the Accessibility inspector" let result = blogger.makeHeadline(from: input) let expected = "The Accessibility Inspector" XCTAssertEqual(result, expected) }
- 실패 케이스를 확인했으므로 새로운 테스트를 작성한다.
makeHeadline2
- 이전에 작성한 makeHeadline 메서드는 하나의 문자열에 대해서만 테스트 통과가 가능하다.
- 우리가 원하는 기능은 가능한 모든 문자열에 대해서 단어의 첫글자가 대문자가 되게 하는 것이다.
makeHeadline2 - RED
- 문자열을 바꾼 새로운 테스트 작성
- makeHeadline 메서드를 바꾸지 않았으므로 테스트 돌렸을 때 당연히 통과 X
// FirstDemoTests.swift func test_makeHeadline_shouldCapitalisePassedInString_2() { let blogger = Blogger() let input = "The contextual action menu" let result = blogger.makeHeadline(from: input) let expected = "The Contextual Action Menu" XCTAssertEqual(result, expected) }
makeHeadline2 - Green
- Blogger.swift에 makeHeadline 메서드 수정하기
// Blogger.swift func makeHeadline(from input: String) -> String { let words = input.components(separatedBy: " ") var headline = "" for var word in words { let firstCharacter = word.remove(at: word.startIndex) headline += "\(String(firstCharacter).uppercased())\(word) " } headline.remove(at: headline.index(before: headline.endIndex)) return headline }
words
: [String] : “ ” (빈칸)을 기준으로 문자열을 쪼개서 String 배열로 만듬 -> 단어 단위로 자르기headline
: 반환할 문자열 변수for var word in words
: words에 있는 단어 하나씩 꺼내서 word로 가져온다으멩word.remove(at: word.startIndex)
: word의 첫번째 문자 POPhealdline += ~~~
: POP한 첫번째 문자 대문자로 만든다음에 word랑 다시 합치기headline.index(before: headline.endIndex)
: endIndex (마지막 인덱스 + 1) , before 하나전 -> 마지막 공백은지우기- makeHeadline을 수정함으로써 새로운 테스트가 통과하였다.
makeHeadline2 - Refactor
- 현재 FirstDemoTests 클래스에는 두개의 테스트 메서드가 있다.
- 두 테스트 코드 모두 Blogger 객체를 중복해서 사용하고 있다.
- Blogger를 클래스의 프로퍼티로 빼고 각 메서드는 해당 프로퍼티를 접근해서 사용하도록하자.
- 테스트에 필요한 인스턴스 Blogger의 주입과 정리는 setUpWithError와 tearDownWithError를 통해 한다. ( 각 테스트가 시작하기전, 종료한 후 호출되는 메서드)
makeHeadline(from: )
코드를 objective - C 스타일에서 swift 스타일로 바꾸기input.capitalized
: 문자열의 각 단어 첫번째 글자 대문자로 변경해줌
// Blogger.swift func makeHeadline(from input: String) -> String { return input.capitalized }
TDD는 각 단계에서 해야할 일이 명확하고 쉽다. 따라서 각 단계에서 “이제는 뭘 해야하지?” 하면서 하는 시간낭비를 줄일 수 있다. Red, Green, Refactor 3단계만 기억하면 된다.
Xcode에서 테스트 관련한 정보 찾기
Test navigator
- 화면 왼쪽에서 다이몬드를 찾거나 command + 6으로 Test naviagtor를 열수 있다.
- Test navigator에서는 프로젝트 또는 워크페이스의 모든 테스트를 볼 수 있다.
- Test navigator의 아래쪽에서는 필터나 검색어를 통해 원하는 테스트만을 볼 수 잇다.
Test overview
- Report navigator에서는 이전에 수행했던 테스트의 목록을 볼 수 있다.
Test 돌리는 방법
하나만 지정해서 돌리기
- 테스트 코드 옆에 다이아몬드를 누르면 프로덕션 코드를 컴파일하고 시뮬레이터 또는 디바이스를 실행해서테스트 코드를 실행 시킬 수 있다.
- 테스트 네비게이터 옆에 있는 다이아몬드로 개별 테스트를 실행할 수 있다.
테스트 그룹으로 실행하기
- 일정한 그룹을 지정해서 해당 그룹에 대해서만 테스트를 실행하고자할 때는
- Edit Scheme에 들어가서
- 테스트하고자 하는 것만 체크하기
테스트코드 디버깅하기
- 때때로 테스트를 디버깅 해야할 때가 있을 수도 있다.
- 일반코드와 마찬가지로 브레이킹 포인트를 찍으면 디버거는 해당 지점에서 실행을 멈춘다.
- 빠진게 있나 , 진짜로 잘 작동하나 확인하고자 할때는 브레이킹 포인트를 찍어서 확인할 수 있다.
- 이전 코드를 예로 들어서 디버깅을 어떻게 하는지 봐보자.
- l을 I 로 수정해서 테스트가 실패하게 만들었다.
- 해당 지점에 브레이킹 포인트를 찍고 다시 테스트를 실행하면
- 콘솔창에 디버거가 나타나고
- ( lldb) 로 시작하는 입력창이 나온다.
- 여기에 po (print object) 명령어를 이용해 원하는 변수의 객체 상태를 볼 수 있다.
- 파란색 브레이킹 포인트를 다시 누르면 브레이킹 포인트 해제가 가능하다.
더 많은 디버거 명령어에 대해서 알려고 할 대 애플 문서에
lldb
키워드로 검색해보자.'Apple🍎 > Test' 카테고리의 다른 글
IOS 단위 테스트 살짝 맛보기 (0) 2023.12.13 TDD를 포기하는 이유와 그럼에도 해야하는 이유 (0) 2023.12.13