TDD란 무엇인가
A Unit test is very small function that your write to test some part of your code
- Unit Test는 가장 작은 함수(코드 중에 테스트하려는 부분)이다.
func is EmailVaild(email: String) → Bool {
// A function in the app that needs to be tested
}
Xcode에선 XCTest Framework를 통해 UnitTest를 할 수 있다.
성공하면 greenColor 실패하면 redColor로 나타내 보여준다.
Unit Test는 독립적이고 작은 테스트 코드를 작성하는 것을 말하기도 함.
UnitTest는 매우작고, 매우 빠름. DB를 접속하거나, Network 통신이 필요하다면 fake object를 넣어서 테스트를 함. UnitTest는 오직 코드가 잘 돌아가는지를 체크하기 때문임.
Testing Pyramid
- UI Tests: User의 클릭과 같은 행동을 자동으로 테스트함. 시뮬레이터를 통해 UITextField가 있는지 UI element가 제대로 되는지 swipe가 제대로 되는지 등을 테스트 가능함.
- Integration Tests: faking database or Mock HTTP말고 실제 DB와 네트워크를 연결해서 App code를 테스트를함.
- UnitTest: 네트워크나, DB통신 시 Mock,Fake 오브젝트를 사용하여 독립적이고 작은 코드 조각들을 테스트 함.
→ 아래로(UnitTest) 갈수록 양이 많아지나봄?
→ 어찌됏건 UnitTest먼저 진행해야함.
Unit Test 의 원칙
- Fast – Unit tests run fase
- Independent – Unit tests are independent
- Repeatable – Unit tests are repeatable
- Self-validationg – Unit test validates itself : 1개의 UnitTest 1개 이상의 Test가 있어야하나봄
- Thorough & Timely – Cover edge caes : 확장성 가능하게 테스트를 해야하나봄..
Test-Driven Development Lifecycle
- Red – Write Unit test that fails.
- Green – Write App code to make Unit test pass.
- Refactor – Clean up and improve Unit test and app code.
- Repeat – Repeat these steps untill all your app feature are build fast
UnitTest .swift 메서드에 대한 설명
- setupWithError → test1() → tearDownWithError()
- setupWithError → test2() → tearDownWithError()
setup, tearDown class 를 override하면 모든 테스트의 시작과 종료를 감지 할 수 있음.
//
// PhotoAppTests.swift
// PhotoAppTests
//
// Created by hoon on 2024/07/19.
//
import XCTest
// 왼쪽 다이아몬드를 통해 class안의 모든 테스트가 통과했는지 볼수있음.
final class PhotoAppTests: XCTestCase {
static var classInstanceCounter = 0
override class func setUp() {
super.setUp()
print("setUp is Call")
}
// 여기에서 사용할 class같은 것들을 미리 호출해서 사용할 수 있나봄.
override func setUpWithError() throws {
PhotoAppTests.classInstanceCounter += 1
print("setUpWithError is Call")
// Put setup code here. This method is called before the invocation of each test method in the class.
}
//tearDown: 분해 // 여기서 ㅔㅌ스트 메서드들이 새로운 오브젝트를 만드나봄?
//test가 완료 될 때까지 initiallize가 되지 않는다구 함
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
print("tearDownWithError is Call")
}
override class func tearDown() {
super.tearDown()
print("tearDown is Call")
}
// 얘는 그냥 empty임 이름을 수정해서 내가 원하는걸로 바꾸거나 그럼
func testExample1() throws {
print("Accessing class level information. Running from Instance # \\(PhotoAppTests.classInstanceCounter)")
}
func testExample2() throws {
print("Accessing class level information. Running from Instance # \\(PhotoAppTests.classInstanceCounter)")
}
// 왼쪽 다이아몬드를 통해 테스트가 통과했는지 안했는지 볼수 있음.
//
func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
}
setUp is Call
// testExample1 started.
setUpWithError is Call
Accessing class level information. Running from Instance # 1
tearDownWithError is Call
// testExample1 passed.
// testExample2 started.
setUpWithError is Call
Accessing class level information. Running from Instance # 2
tearDownWithError is Call
// testExample1 passed
tearDown is Call
→ 순서가 setUp → setupWithError → testExample1 → tearDownWithError → tearDown
setUp과 tearDown class 는 한번만 호출이됨.
addTeardonwBlock { }
- override class func tearDown() : 선언된 testClass에서 테스트가 종료 될 때, 한번호출됨.
- override func tearDownWithError(): testClass에 선언된 test메서드마다 종료 될 때 호출됨 3개있으면 3번 호출됨.
- addTeardownBlock: tearDownWithError 다음에 호출되나봄.
@testable import
→opne public class로 선언해야 test에서 가져쓸텐데 그렇게 하지 않아도 @testable로 import하면 사용 할 수 있다.
Unit Test를 실행하는 방법
→ 다이아몬드 클릭하면 실행됨.
→ Product → Test 를 클릭하면 모든 Test가 실행됨.
The Order of Unit Tests ( 테스트의 순서)
- Alphabetically(Lexical) ordered.
- Randomized,
- Executed in parallel
작성된 순서가 아니라 , 알파벳 순으로 동작함.
→ testA, testB, testC, testD 순으로 동작함.
final class TestCase: XCTestCase {
override func setUp() {
print("setUp")
}
override func tearDown() {
print("Tear down")
}
func testD() {
print("Running Test D")
}
func testB() {
print("Running Test B")
}
func testC() {
print("Running Test C")
}
func testA() {
print("Running Test A")
}
}
Unit Test Naming
Patten:
func test<SystemUnderTest>_<Condition Or State Change>_<Expected Result>() {
// <테스트할얘, 조건관련내용, 예상되는결과>
// <이녀석에서_어떤조건을줬을때/이런결과가 나온다>
}
Example:
testSignupFormModel_WhenInfomationProvided_PasswordShouldMatchRepeatPassword() {
}
func textColorIsRed() {
// Some code here
}
Disable 또는 Skip Unit Test
- BreakPoint Disable 하듯이 다이아몬드 버튼 왼쪽눌러서 Disable누르면됨.
- 아니면, Test 설정 쪽에서 체크해제 해주면됨.
- 아니면, 메서드 이름을 test로 시작하지 않으면됨 testD→ skip_testD()로 변경
Test 결과 보는 방법
뭐가 실패했는지 볼 수 있음.
Build 한 시간이나 Build 정보들을 볼 수 있음/ Log클릭시 실패한 내용을 볼 수 있음.
Code Coverage
Code coverage에 체크를 해줌.
어떤 클래스에 얼만큼 테스트가 적용되어있는지 확인 할 수있음.
- 빨강: 들어가보면 빨간색으로 unitTest를 하면서 한번도 호출 하지 않는 메서드를 표시해줌.
- 빗줄: 호출이 되서 테스트를 하려는데, if문 같은걸로 인해 한번도 테스트가 되지 않을 때.
TDD는 왜하는가?
- 좋은 테스트는 앱이 원하는대로 동작하는지 보장함.
- 다만, 모든 테스트가 “좋은” 테스트라고 할 수는 없음.
- TDD는 좋은 테스트를 만들 수 있는 방법임.
- 먼저 실패 가능성을 확인시켜줌(처음에)
- 신규 테스트를 만들기 전에, 기존에 테스트를 반복적으로 통과해야함.
- 각 테스트들은 빠르게 실행 가능함.
- 테스트가 있기 때문에 마음 놓고 리펙터링을 할 수 있음 → 테스트가 유지됨 → 유지보수 비용 감소
- 계속 TDD 할 수록, 코드는 테스트가 가능하게 리팩터링 됨
- 사용자에게 좋은 제품을 지속적으로, 빠르게 자주 제공할 수 있기 때문에, TDD가 유용함
- TDD를 한번만 하면 느립니다.
- 하지만, TDD는 누적이 될수록 훨씬 많은 비용을 줄여줍니다.
- 속도도 빨라지고
- 안정성도 올라가고
TDD 학습 후 정리
TDD 학습은 했는데 기존 프로젝트에 적용은 부담이 컸다.
디자인 패턴이 여러개 존재하고, 여러 로직들을 TDD로 만드는건 사실 상 불가능 하다.
TDD 와 BDD에 대해 추가적으로 찾아봤는데, 필요한 부분에만 TDD/BDD를 적용해보고자 한다.
TDD: add(1,2)이 2인지 확인
BDD: 사용자가 “=” 를 눌렀을 때, 1 + 1 의 값 2가 화면에 표시되는지 확인
BDD는 요구사항 테스트 케이스 이고, TDD는 모듈화 된 기능 테스트 라고 보면된다.
TDD는 로직 자체에 관심이 있고, BDD는 요구사항시나리오에 중점을 두고 있다.
그래서 BDD로 테스트하고, TDD로 검증을 하는 것이 좋아보인다.
결론. TDD의 과실
- 모든 동료로부터 좋은 피드백을 받기 쉬워짐. (개발하려는게 이게맞아? 에 대한 피드백을 받기 쉬워짐)
- 기획자, QA, 디자이너, 안드로이드 개발자로 부터도 피드백을 받을 수 있음.
- 코드리뷰를 받을 때도, 코드를 하나하나 보고 리뷰하는건 어려움. 다만 테스트 케이스 코드로 리뷰하는건 쉬움.
- 더 많은 피드백 → 더 빠른 성장
- 개발을 빠르게 할 수 있음.
- 앱을 켜서 로그인을 해서, 화면이동을 해서 뭘 눌러서 테스트를 하는 시간들이 확 아껴집니다.
- 처음엔 더 느리다고 느낄 수 있음.
- TDD를 하지않았다면 발견하지 않았을 버그들도 미리 발견할 것이기 때문.
추천 글: Moya 이해하기