티스토리 뷰

 

 

이전에 포스팅했던 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. 시나리오 1: 유효한 사용자를 추가할 때
  2. 시나리오 2: 잘못된 이메일을 가진 사용자를 추가할 때
  3. 시나리오 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을 사용해서 구현하면 구조 파악이 좀 더 쉬울 것 같다. 프로젝트 규모에 따라서 어떤 걸 선택해서 테스트 코드를 구현할지 생각해봐야 할 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
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
글 보관함