[testing] Styles of unit testing

Donghoon Bae·2025년 9월 8일

testing

목록 보기
1/6

세 가지 스타일

  • 출력 기반 테스트: 입력을 주고 반환된 출력만 검증하는 방식으로, 부작용이 없는 코드(수학적 함수)에 적합하다.
import Testing

struct Product {
  let name: String
}

struct PriceEngine {
  func calculateDiscount(_ products: Product...) -> Decimal {
    let discount = Decimal(products.count) * 0.01
    return min(discount, 0.2)
  }
}

@Test
func discountOfTwoProducts() {
  let product1 = Product(name: "Hand wash")
  let product2 = Product(name: "Shampoo")
  let sut = PriceEngine()
  let discount = sut.calculateDiscount(product1, product2)
  #expect(discount == 0.02)
}
  • 상태 기반 테스트: 동작 후 SUT, 협력자, DB/파일 등 시스템의 상태를 검증한다.
import Testing

struct Product: Equatable {
  let name: String
}

struct Order {
  private var products: [Product] = []
  var allProducts: [Product] { products }

  mutating func addProduct(_ product: Product) {
    products.append(product)
  }
}

@Test
func addingAProductToAnOrder() {
  let product = Product(name: "Hand wash")
  var sut = Order()
  sut.addProduct(product)
  #expect(sut.allProducts.count == 1)
  #expect(sut.allProducts[0] == product)
}
  • 커뮤니케이션 기반 테스트: Mock으로 SUT와 협력자 간 상호작용(메서드 호출)을 검증한다.
import Testing

protocol EmailGateway {
  func sendGreetingsEmail(_ email: String)
}

struct Controller {
  private let emailGateway: EmailGateway

  init(_ emailGateway: EmailGateway) {
    self.emailGateway = emailGateway
  }

  func greetUser(_ email: String) {
    emailGateway.sendGreetingsEmail(email)
  }
}

final class EmailGatewayMock: EmailGateway {
  private(set) var receivedEmails: [String] = []

  func sendGreetingsEmail(_ email: String) {
    receivedEmails.append(email)
  }
}

@Test
func sendingAGreetingsEmail() {
  let emailGatewayMock = EmailGatewayMock()
  let sut = Controller(emailGatewayMock)
  sut.greetUser("user@email.com")
  #expect(emailGatewayMock.receivedEmails == ["user@email.com"])
}

4대 요소로 본 비교

  • 회귀 방지, 피드백 속도: 세 스타일 모두 원칙적으로 동일하나, 커뮤니케이션 기반은 남용 시 얕은 테스트가 되기 쉽다.
  • 리팩토링 내성: 출력 기반이 가장 강하고, 상태 기반은 중간, 커뮤니케이션 기반은 상호작용 결합으로 가장 취약하다.
  • 유지보수성: 출력 기반이 가장 짧고 간결하며, 상태 기반은 단언이 비대해지기 쉽고, 커뮤니케이션 기반은 Mock 설정, 검증으로 가장 크다.
구분출력 기반 스타일상태 기반 스타일커뮤니케이션 기반 스타일
회귀 방지동일동일동일
피드백 속도동일동일동일
리팩토링 내성낮음중간중간
유지보수성낮음중간높음

함수형 아키텍처

  • 함수형 프로그래밍은 수학적 함수(mathematical function) 을 사용한 프로그래밍이다.
func calculateDiscount(_ products: [Product]) -> Decimal {
  let discount = Decimal(products.count) * 0.01
  return min(discount, 0.2)
}

  • 함수형 아키텍쳐는 부작용을 비즈니스 로직 동작의 끝으로 몰아서 비즈니스 로직과 부작용을 분리한다.
  • 가변 셸은 모든 입력을 수집한다.
  • 함수형 코어는 결정을 생성한다.
  • 가변 셸은 결정을 부작용으로 변환한다.
profile
iOS & Swift

0개의 댓글