티스토리 뷰
이전에 포스팅했던 UnitTest 예제는 Quick과 Nimble을 사용하지 않고 XCTest를 사용했었다.
[Swift] - UnitTest 예제(Given-When-Then 패턴)
UnitTest를 공부하던 중 test코드를 어떻게 작성해야하는지 감이 안 잡혔다. 코드의 구조를 명확하게 파악할 수 있도록 구현하고 싶은데 이 부분이 어려웠다. 그러던 중 테스트 코드를 표현하는 방
steelbeartaeng2.tistory.com
이전 포스팅 글과 같은 테스트 시나리오를 Quick과 Nimble을 사용해서 구현해보려고 한다.
1. Quick과 Nimble이란?
Quick이란?
Quick은 테스트를 상황별로 나눠 테스트의 구성요소를 더 쉽게 파악할 수 있도록 한다.
XCTest로 구현하는 경우 다음과 같이 데스트 클래스가 XCTestCase를 상속받는 걸 알 수 있다.
import XCTest
@testable import Ex
final class ExTests: XCTestCase {
override func setUpWithError() throws {
}
override func tearDownWithError() throws {
}
func testExample() throws {
}
func testPerformanceExample() throws {
self.measure {
}
}
}
하지만 Quick을 사용하면 QuickSpec을 상속받는다.
describe, context, it을 사용해서 테스트의 "대상", "상황", "기대결과"를 명확히 구분할 수 있게 된다.
- describe: 테스트 대상(클래스, 메서드, 기능)을 그룹화.
- context: 특정 상황이나 조건을 설명.
- it: 특정 동작이나 기대하는 결과를 정의.
import Quick
import Nimble
class TestSpec: QuickSpec {
override class func spec() {
describe("사용자를 추가할 때") {
context("유효한 데이터로 추가할 때") {
it("정상적으로 추가된다") {
...
}
}
}
}
}
XCTest를 사용할 때 Given, When, Then을 주석으로 표시하곤 했었는데 Quick은 describe, context, it을 제공해 주니 좀 더 테스트 상황별로 확인하기 쉬운 것 같다.
Nimble이란?
Nimble은 Quick과 함께 사용되는 검증 도구로, 테스트의 기대치를 간결하고 직관적으로 표현할 수 있게 도와준다고 한다.
테스트 결과를 확인하는 경우 XCTest를 사용하면 다음과 같이 XCTAssert 계열의 함수를 사용했었다.
XCTAssertThrowsError(try userManager.addUser(username: username, email: invalidEmail)) {
thrownError = $0
}
XCTAssertEqual(thrownError as? UserError, UserError.invalidEmail)
근데 Nimble을 사용하면 XCTAssert 계열의 함수 대신 expect를 사용한다.
let action = { try userManager.addUser(username: username, email: invalidEmail) }
expect(action).to(throwError(UserError.invalidEmail))
XCTest를 사용할 때보다 가독성이 높아진 것 같고, 비동기 작업의 결과를 기다리고 검증하는 것도 가능하다고 한다.
🔎 Quick과 Nimble을 함께 사용하는 이유
Quick은 테스트를 구조적으로 작성하고, Nimble은 테스트의 검증을 표현하는 도구다.
둘을 함께 사용하면 다음과 같은 이점이 있다.
- Quick: 테스트 시나리오를 명확히 그룹화.
- Nimble: 테스트 결과를 간결하고 자연스럽게 검증.
2. Quick과 Nimble 사용하여 테스트 코드 작성
Quick과 Nimble import하고 QuickSpec을 상속받은 후 코드의 기본 구조는 다음과 같다.
import Quick
import Nimble
final class Quick_Nimble: QuickSpec {
override class func spec() {
var userManager: UserManager!
beforeEach {
userManager = UserManager()
}
afterEach {
userManager = nil
}
}
}
- beforeEach: 각 테스트(it 블록)가 실행되기 직전에 실행되는 코드 블록을 정의
- afterEach: 각 테스트(it 블록)가 실행된 후에 실행되는 코드 블록을 정의
다음과 같은 코드가 있을 때 beforeEach과 afterEach의 실행 순서는 어떨까??
describe("테스트 그룹") {
beforeEach { print("beforeEach 실행") }
afterEach { print("afterEach 실행") }
it("첫 번째 테스트") {
print("첫 번째 테스트 실행")
}
it("두 번째 테스트") {
print("두 번째 테스트 실행")
}
}
출력해 보면 실행 순서를 파악할 수 있다. 정말 it 실행 전과 후로 beforeEach와 afterEach가 실행되는 걸 확인했다.
beforeEach 실행
첫 번째 테스트 실행
afterEach 실행
beforeEach 실행
두 번째 테스트 실행
afterEach 실행
XCTest는 setUp과 tearDownWithError를 사용해 상태를 초기화했었는데 Quick은 beforeEach과 afterEach를 사용하여 초기화하는 걸 알게 되었다.
여기서 오해하면 안되는 점은 beforeEach과 afterEach는 Quick에서 제공되는 코드이다.
시나리오는 총 3개이다.
- 시나리오 1: 유효한 사용자를 추가할 때
- 시나리오 2: 잘못된 이메일을 가진 사용자를 추가할 때
- 시나리오 3: 중복된 사용자를 추가할 때
시나리오 총 3개를 하나로 묶으면 "사용자 추가기능"을 의미한다.
그래서 코드 구조를 다음과 같이 잡을 수 있다.
- describe는 테스트의 큰 틀을 제공하며, 테스트할 기능이나 모듈을 묶는 역할을 한다.
- context는 describe 안에서 특정 조건이나 상황을 설명하며, 테스트를 더 세부적으로 나눌 수 있도록 돕는다.
- it은 각 상황에서 기대되는 결과를 명확히 정의한다.
describe("사용자 추가 기능") {
context("유효한 사용자를 추가할 때") {
it("사용자가 성공적으로 추가되고 검색할 수 있어야 한다") {
}
}
context("잘못된 이메일을 가진 사용자를 추가할 때") {
it("잘못된 이메일 오류를 던지고 사용자가 추가되지 않아야 한다") {
}
}
context("중복된 사용자를 추가할 때") {
it("userAlreadyExists 오류를 던지고 기존 사용자가 유지되어야 한다") {
}
}
}
시나리오 1: 유효한 사용자를 추가할 때
Given: 유효한 사용자 이름과 이메일.
When: addUser 메서드 호출로 사용자 추가 시도.
Then: 1. 추가된 사용자는 유효한 id, username, email 값을 가져야 함.
2. 검색된 사용자 정보는 추가된 사용자와 동일해야 함.
- XCTest 사용
func testAddValidUser() {
// Given
let username = "testuser"
let email = "test@example.com"
// When
let newUser: User
do {
// 유효한 사용자 추가를 시도하고, 성공 시 newUser에 저장
newUser = try userManager.addUser(username: username, email: email)
} catch {
// 오류 발생 시 테스트 실패로 처리하고, 해당 에러 메시지를 출력
XCTFail("Valid user addition threw an error: \(error)")
return
}
// Then
XCTAssertEqual(newUser.id, 1)
XCTAssertEqual(newUser.username, username)
XCTAssertEqual(newUser.email, email)
// 추가된 사용자가 userManager에서 정상적으로 검색되는지 확인
let retrievedUser = userManager.getUser(byUsername: username)
XCTAssertNotNil(retrievedUser) // 사용자가 정상적으로 검색되었는지 확인
XCTAssertEqual(retrievedUser?.id, newUser.id) // 검색된 사용자의 ID가 일치하는지 확인
}
- Quick과 Nimble 사용
context("유효한 사용자를 추가할 때") {
it("사용자가 성공적으로 추가되고 검색할 수 있어야 한다") {
// Given
let username = "testuser"
let email = "test@example.com"
// When
let newUser = try? userManager.addUser(username: username, email: email)
// Then
// 추가된 사용자 검증
expect(newUser).notTo(beNil())
expect(newUser?.id).to(equal(1))
expect(newUser?.username).to(equal(username))
expect(newUser?.email).to(equal(email))
// 사용자 검색 검증
let retrievedUser = userManager.getUser(byUsername: username)
expect(retrievedUser).notTo(beNil())
expect(retrievedUser?.id).to(equal(newUser?.id))
}
}
같은 시나리오를 XCTest와 Quick/Nimble을 사용한 코드를 확인해 보니 확실히 Quick/Nimble을 사용한 게 가독성이 좋은 것 같다. 다른 차이점도 확인해 보자.
1️⃣ XCTest
- Given-When-Then 구분: XCTest 코드에서도 명확하게 Given-When-Then으로 구분을 했지만, 테스트의 흐름이 do-catch 구문을 사용해서 예외 처리를 직접 관리한다.
- 결과 검증: XCTAssertEqual, XCTAssertNotNil 등을 사용하여 검증하고 있다. 이 방식은 조금 더 명시적이고 세부적인 처리가 필요하다.
- 에러 처리: catch를 통해 직접 에러 처리를 하며, 실패한 경우 XCTFail을 호출하여 테스트를 실패로 처리한다.
2️⃣ Quick/Nimble
- Given-When-Then 구분: Quick/Nimble에서는 context와 it을 사용하여 BDD 스타일로 작성한다. context는 테스트할 조건을 설명하고, it은 그 조건에 대한 기대 결과를 설명한다.
- 결과 검증: expect와 to를 사용하여 조건을 검증한다. Nimble의 expect 구문은 읽기 쉽고 직관적이며, 결과를 체이닝하여 한 줄로 깔끔하게 검증할 수 있다.
- 에러 처리: try? 구문을 사용하여 에러를 자동으로 처리하고, 오류가 발생하면 해당 값을 nil로 처리할 수 있다. 에러를 명시적으로 다루는 대신 옵셔널 처리로 에러를 회피하는 스타일이다.
시나리오 2: 잘못된 이메일을 가진 사용자를 추가할 때
Given: 잘못된 형식의 이메일과 유효한 사용자 이름.
When: addUser 메서드 호출 시도.
Then: 1. UserError.invalidEmail 오류가 발생해야 함.
2. 데이터베이스에 사용자가 추가되지 않아야 함.
- XCTest 사용
func testAddUserWithInvalidEmail() {
// Given
let username = "testuser"
let invalidEmail = "invalidemail" // 이메일 형식이 유효하지 않음
// When
var thrownError: Error?
XCTAssertThrowsError(try userManager.addUser(username: username, email: invalidEmail)) {
thrownError = $0
}
// Then
XCTAssertEqual(thrownError as? UserError, UserError.invalidEmail)
// 사용자 추가가 실패했으므로 userManager에서 사용자를 찾을 수 없어야 함
let retrievedUser = userManager.getUser(byUsername: username)
XCTAssertNil(retrievedUser) // 사용자가 저장되지 않았음을 확인
}
- Quick과 Nimble 사용
context("잘못된 이메일을 가진 사용자를 추가할 때") {
it("잘못된 이메일 오류를 던지고 사용자가 추가되지 않아야 한다") {
// Given
let username = "testuser"
let invalidEmail = "invalidemail"
// When
let action = { try userManager.addUser(username: username, email: invalidEmail) }
// Then
expect(action).to(throwError(UserError.invalidEmail))
expect(userManager.getUser(byUsername: username)).to(beNil())
}
}
시나리오 3: 중복된 사용자를 추가할 때
Given: 동일한 사용자 이름으로 첫 번째 사용자를 추가한 후, 다른 이메일로 두 번째 추가 시도.
When: addUser 메서드 호출 시도.
Then: 1. UserError.userAlreadyExists 오류가 발생해야 함.
2. 첫 번째 사용자의 데이터가 변경되지 않아야 함.
- XCTest 사용
func testAddDuplicateUser() throws {
// Given
let username = "testuser"
let email = "test@example.com"// 첫 번째 사용자의 이메일
let duplicateEmail = "another@example.com"// 중복 시도에서 사용할 이메일
// 첫 번째 사용자 추가 시도
let newUser = try userManager.addUser(username: username, email: email)
// When
var thrownError: Error?
XCTAssertThrowsError(try userManager.addUser(username: username, email: duplicateEmail)) {
thrownError = $0
}
// Then
XCTAssertEqual(thrownError as? UserError, UserError.userAlreadyExists)
// 추가된 사용자가 여전히 userManager에 있는지 확인
let retrievedUser = userManager.getUser(byUsername: username)
XCTAssertNotNil(retrievedUser)
XCTAssertEqual(retrievedUser?.email, email)
}
- Quick과 Nimble 사용
context("중복된 사용자를 추가할 때") {
it("userAlreadyExists 오류를 던지고 기존 사용자가 유지되어야 한다") {
// Given
let username = "testuser"
let email = "test@example.com"
let duplicateEmail = "another@example.com"
let newUser = try? userManager.addUser(username: username, email: email)
expect(newUser).notTo(beNil())
// When
let action = { try userManager.addUser(username: username, email: duplicateEmail) }
// Then
expect(action).to(throwError(UserError.userAlreadyExists))
let retrievedUser = userManager.getUser(byUsername: username)
expect(retrievedUser).notTo(beNil())
expect(retrievedUser?.email).to(equal(email))
}
}
같은 시나리오를 XCTest와 Quick/Nimble을 사용해서 구현해 보니까 둘의 차이점을 이해할 수 있게 되었다.
앞으로 복잡한 시나리오를 가진 테스트 코드가 필요할 수도 있는데 이럴 때 Quick과 Nimble을 사용해서 구현하면 구조 파악이 좀 더 쉬울 것 같다. 프로젝트 규모에 따라서 어떤 걸 선택해서 테스트 코드를 구현할지 생각해봐야 할 것 같다.
'스위프트' 카테고리의 다른 글
[Swift] - fastlane + GitHub Actions CI 구축(3) (0) | 2025.01.20 |
---|---|
[Swift] - fastlane + GitHub Actions CI 구축(2) (0) | 2024.11.25 |
[Swift] - fastlane + GitHub Actions CI 구축(1) (0) | 2024.11.12 |
[Swift] - Rxswift 예제로 이해해보기 (0) | 2024.11.02 |
[Swift] - RxSwift가 무엇인가? (0) | 2024.10.16 |