Stub VS Mock

Luke·2024년 5월 2일

TestCode

목록 보기
3/5
post-thumbnail

이 글을 작성하게 된 이유는 테스트 코드를 작성하는 도중 Mock과 Stub에 대해 알게 되어, 개념을 정리하기 위해 이 글을 작성하게 되었습니다.


테스트 더블

Stub과 Mock에 대한 개념을 이해하기전 테스트 더블이라는 개념을 이해해야 합니다.

테스트 더블(Test Double)은 실제 구현 대신 테스트 환경에서 사용되는 객체를 가리키는 용어로, 영화 산업에서 스턴트 더블(Stunt Double)이 위험한 장면을 대신 수행하는 것과 유사한 개념입니다.

간단하게 대체할 모의 객체를 생성해서 테스트를 진행하는 기술이라고 보면 됩니다.

  • Dummy 객체는 전달되지만 실제로는 사용되지 않습니다. 보통 매개변수 목록을 채우는 데만 사용됩니다.

  • Fake 객체는 실제로 작동하는 구현을 가지고 있지만 일반적으로 프로덕션에 적합하지 않은 지름길을 사용합니다.

  • Stub은 테스트 중 호출에 대해 미리 준비된 답변을 제공하며, 일반적으로 테스트를 위해 프로그래밍된 것 이외의 것에는 전혀 응답하지 않습니다.

  • Spy는 호출 방식에 따라 일부 정보를 기록하는 스텁입니다. 한 가지 예시로는 전송된 메시지 수를 기록하는 이메일 서비스일 수 있습니다.

  • Mock는 예상되는 호출에 대한 기대값으로 미리 프로그래밍된 객체입니다.

개념 출처

위 그림을 보면 설명한 것과 같이 Dummy, Stub, Spy, Mock, Fake로 구성이 되어 있는데, 이중에서 일반적으로 많이 사용하는 Stub과 Mock에 대해서 알아보겠습니다.


Stub

  • 더미 객체를 생성하여 실제로 동작하는 것처럼 보이게 만드는 가짜 객체 입니다.
  • 호출된 요청에 대해 미리 준비해둔 응답값을 전달합니다.
  • 객체의 최소한의 기능만을 임의로 구현합니다.

Stub's Lifecycle

  • Setup: 테스트 준비
  • Exercise: 기능 테스트
  • Verify state: 객체 상태 테스트
  • Teardown: 리소스 정리

Mock

  • 예상되는 호출에 대한 내용을 미리 작성하고, 해당 내용에 따라 동작하도록 프로그래밍 된 객체입니다.
  • 특정 동작을 수행하는지에 대한 검증을 합니다.
  • 즉, 행위 검증을 진행합니다.

Mock's Lifecycle

  • Setup data: 데이터 준비
  • Setup expectations: 예상되는 결과 준비
  • Exercise: 기능 테스트
  • Verify expectations: 예상 검증
  • Verify state: 객체 상태 테스트
  • Teardown: 리소스 정리

Stub을 사용한 테스트 코드 예시:

// 프로토콜 정의
protocol DataService {
  func fetchData() -> String
}

// Stub 클래스 정의
class DataServiceStub: DataService {
  func fetchData() -> String {
    return "Stubbed Data"
  }
}

// 테스트
class DataServiceTests: XCTestCase {
  func testFetchDataWithStub() {
    let dataService = DataServiceStub()
    XCTAssertEqual(dataService.fetchData(), "Stubbed Data")
  }
}

위의 코드에서는 DataService 프로토콜을 정의하고, 이를 구현하는 DataServiceStub 클래스를 작성합니다. DataServiceStub 클래스는 fetchData() 메소드를 구현하여 항상 "Stubbed Data"를 반환합니다. 그리고 DataServiceTests 클래스 내에서 이를 테스트하는 testFetchDataWithStub() 메소드를 작성합니다.


Mock을 사용한 테스트 코드 예시:

// 프로토콜 정의
protocol NetworkService {
  func fetchData(completion: @escaping (String) -> Void)
}

// Mock 클래스 정의
class NetworkServiceMock: NetworkService {
  var fetchDataCalled = false
  
  func fetchData(completion: @escaping (String) -> Void) {
    fetchDataCalled = true
    completion("Mocked Data")
  }
}

// 테스트
class NetworkServiceTests: XCTestCase {
  func testFetchDataWithMock() {
    let networkService = NetworkServiceMock()
    let expectation = XCTestExpectation(description: "Wait for fetchData() completion")
    
    networkService.fetchData { data in
      XCTAssertEqual(data, "Mocked Data")
      expectation.fulfill()
    }
    
    XCTAssertTrue(networkService.fetchDataCalled)
    wait(for: [expectation], timeout: 1.0)
  }
}

위의 코드에서는 NetworkService 프로토콜을 정의하고, 이를 구현하는 NetworkServiceMock 클래스를 작성합니다. NetworkServiceMock 클래스는 fetchData(completion:) 메소드를 구현하여 항상 completion 클로저를 호출하고, fetchDataCalled 속성을 통해 메소드가 호출되었는지 여부를 추적합니다. 그리고 NetworkServiceTests 클래스 내에서 이를 테스트하는 testFetchDataWithMock() 메소드를 작성합니다. 이 메소드는 기대된 데이터를 반환하고, 메소드가 호출되었는지 확인합니다.


Stub VS Mock

요약 해보자면 Mock은 행위 검증이고, Stub은 상태 검증이라고 할 수 있습니다.

즉, Stub과 Mock은 실제 서비스 대신 테스트 더블을 사용하고, Stub은 상태 검증을 하고 Mock은 행위 검증을 하고 있다는 점에서 차이가 있다.

위 설명에 추가적인 설명을 덧붙이자면 Mock은 해당 메소드가 잘 호출되었는지, 얼마나 호출되었는지 중요할때 쓰고, Stub은 실제 객체 대신 가짜 객체를 넣어야 하는데, 실제 로직은 필요 없고 반환 값 또는 콜백을 통해 데이터를 반환하는 가짜 객체가 필요할때 쓰인다.

  • 상태 검증
    • 메소드가 수행된 후, 객체의 상태를 확인하여 올바르게 동작해는지 확인하는 검증법입니다.
  • 행위 검증
    • 메소드의 리턴 값으로 판단할 수 없는 경우, 특정 동작을 수행하는지 확인하는 검증법입니다.
// 상태검증
let stateClass = StateClass() stateClass.doSomething() 
XCTAssert(stateClass.getStatus() == true)

// 행위 검증
let behaviorClass = BehaviorClass()	
XCTAssertNoThrow(behaviorClass.doBehavior())

참고

https://ssyoni.tistory.com/31
https://jiseobkim.github.io/swift/2022/02/06/Swift-Test-Double(%EB%B6%80%EC%A0%9C-Mock-&-Stub-&-SPY-%EC%9D%B4%EB%9F%B0%EA%B2%8C-%EB%AD%90%EC%A7%80-).html
https://www.turing.com/kb/stub-vs-mock
https://azderica.github.io/00-test-mock-and-stub/
https://martinfowler.com/articles/mocksArentStubs.html

0개의 댓글