
λΌμ΄λΈ μ€νΈλ¦¬λ° ꡬ쑰 κ°νΈ, TDD 첫 λμ κΈ°

- #TDD
- #TCA
- #CleanSwift
- #iOS
λ€μ΄κ°λ©°
μλ νμΈμ. μ λ fromm μ±μμ iOS κ°λ°μ λ΄λΉνκ³ μλ μ΄νμ°μ λλ€.
λ°°κ²½: 볡μ‘ν΄μ§ ꡬ쑰, κ·Έλ¦¬κ³ λ³νμ νμμ±
μλ , fromm μ±μ SwiftUIμ TCA μν€ν μ²λ₯Ό μ μ©ν΄ λΌμ΄λΈ μ€νΈλ¦¬λ° κΈ°λ₯μ μ²μ λμ νμ΅λλ€.
TCAλ λ€μν κΈ°λ₯μ μ 곡νκ³ , λ§μ νλ€μ΄ ν¨κ³Όμ μΌλ‘ νμ©νκ³ μλ μν€ν μ²μ§λ§, μ ν¬ νμκ²λ μκ°μ΄ μ§λ μλ‘ λ‘μ§ λΆμ°, λλ²κΉ μ μ΄λ €μ, 볡μ‘ν μν κ΄λ¦¬ λ± λͺ κ°μ§ μΈ‘λ©΄μμ μ μ°¨ μ΄λ €μμ΄ λμ λκΈ° μμνμ΅λλ€.
μ΄λ‘ μΈν΄, ν λ΄λΆ λ Όμ λμ λ€μκ³Ό κ°μ ν° κ²°μ μ λ΄λ Έμ΅λλ€.
1. λΌμ΄λΈ λͺ¨λμ μ λ©΄μ μΈ κ΅¬μ‘° κ°νΈ
- κΈ°μ‘΄ TCA μν€ν
μ² μ κ±°
- νμ μꡬμ λ§μΆ° λ³νν Clean Swift ν¨ν΄ λμ
2. TDD λ°©μ λμ
Clean Swift ν¨ν΄μ μ±ννλ, Clean Architectureλ₯Ό κΈ°λ°μΌλ‘ κ°μ νμ¬ μ μ©νμ΅λλ€. μ΄λ₯Ό κΈ°λ°μΌλ‘ ꡬ쑰λ₯Ό μ ννλ©΄μ Interactor μ€μ¬μΌλ‘ λ‘μ§μ λͺ¨μΌκ³ , View β Interactor β Presenter κ° μ± μμ λͺ νν λΆλ¦¬ν μ μμμ΅λλ€. κ·Έ κ²°κ³Ό, ν μ€νΈ μμ±κ³Ό μ μ§λ³΄μλ νκ²° μμν΄μ‘μ΅λλ€.
νΉν ꡬ쑰 κ°νΈκ³Ό ν¨κ», TDD λ°©μλ λμ ν΄λ΄€λλ°μ. κΈ°μ‘΄μ²λΌ ꡬν μμ£Όλ‘ μμ νλ λ°©μμμ λ²μ΄λ, ν μ€νΈλ₯Ό λ¨Όμ μμ±νκ³ κ·Έμ λ§μΆ° κΈ°λ₯μ ꡬννλ νλ¦μ μ€μ νλ‘μ νΈμ μ μ©ν΄λ³Έ 건 μ΄λ²μ΄ μ²μμ΄μμ΅λλ€.
μ΄λ² κΈμμλ λΌμ΄λΈ μ€νΈλ¦¬λ° ꡬ쑰 κ°νΈ κ³Όμ μμ TDDλ₯Ό μ€μ λ‘ μ΄λ»κ² μ μ©νλμ§, κ·Έλ¦¬κ³ μ ν¬ νμ΄ μ΄λ€ κΈ°μ€μΌλ‘ ν μ€νΈλ₯Ό μμ±νλμ§, κ·Έ κ²½νμ ν΅ν΄ 무μμ λ°°μ°κ³ λκΌλμ§ μ΄μΌκΈ°ν΄λ³΄λ € ν©λλ€.
π μ°Έκ³ - ꡬ쑰 κ°νΈ κ³Όμ μ μ΄μ λΈλ‘κ·Έ κΈμμ μμΈν λ€λ£¨μμ΅λλ€. ( λ§ν¬ )
TDD, μ°λ¦¬ νμμ μ΄λ κ² μμνμ΅λλ€
μλ‘μ΄ μν€ν μ² λμ λ§μΌλ‘λ νμκ²λ ν° λ³νμλλ°, μ¬κΈ°μ TDDκΉμ§ λν΄μ§λ μ μμ΄ μ½μ§λ§μ μμμ΅λλ€.
νμ₯λ μΈμλ ν
μ€νΈ μ½λ κ²½νμ΄ μμκ³ , μΈ λͺ
μ νμμ΄ κ°μ λ€λ₯Έ μ€νμΌλ‘ μ½λλ₯Ό μμ±νλ€ λ³΄λ μΌκ΄μ±μ μ μ§νλ λ°λ μ½μ§ μμμ΅λλ€. μλ₯Ό λ€μ΄, μ΄λ€ νμμ it
λΈλ‘μ μκ² λλλ λ°©μμ μ νΈνλ λ°λ©΄, λ€λ₯Έ νμμ νλμ it
λΈλ‘ μμ λͺ¨λ κ²μ¦μ λ΄λ λ°©μμ μ¬μ©νκΈ°λ νμ΅λλ€.
μ΄λ° μν© μμμ νμ₯λκ»μ ν μ€νΈ μμ± κ°μ΄λλ₯Ό λͺ νν μ 리ν΄μ£Όμ ¨κ³ , μ ν¬λ μ΄λ₯Ό λ°νμΌλ‘ λ€μκ³Ό κ°μ 5λ¨κ³ TDD 루ν΄μ μ€μ²νμ΅λλ€.
5λ¨κ³ TDD 루ν΄
1. ν
μ€νΈ μΌμ΄μ€ μμ±νκΈ°
2. μ€ν¨νλ ν
μ€νΈ λ§λ€κΈ°
3. μ±κ³΅νλ ν
μ€νΈ λ§λ€κΈ°
4. νλ ₯ κ°μ²΄(router, presenter λ±) ꡬν
5. 리ν©ν λ§ λ° ν
μ€νΈ μ μ§
Interactor ν μ€νΈ, μ΄λ κ² μμ±νμ΅λλ€
μ΄λ² ꡬ쑰 κ°νΈμμλ Clean Swiftμμ ν΅μ¬ μν μ λ΄λΉνλ Interactor μ€μ¬μΌλ‘ λ¨μ ν μ€νΈλ₯Ό μμ±νμ΅λλ€.
λ, Quick + Nimble + Cuckoo μ‘°ν©μ νμ©ν΄ ν μ€νΈ μ½λλ₯Ό μμ± νκ³ , μν κ²μ¦κ³Ό νμκ²μ¦μ μννμ΅λλ€.
ππ»ββοΈ μ κΉ, Interactorκ° λκ°μ?
Clean Swiftμμ Interactorλ μ¬μ©μ μ΄λ²€νΈλ₯Ό μ²λ¦¬νκ³ , λΉμ¦λμ€ λ‘μ§μ μννλ©°, μνλ₯Ό κ΄λ¦¬νλ ν΅μ¬ μν μ λ§‘κ³ μμ΅λλ€.
μλ₯Ό λ€μ΄, μ¬μ©μκ° λ²νΌμ λλ₯΄κ±°λ λ©μμ§λ₯Ό μ λ ₯νλ©΄, Viewλ ν΄λΉ μ΄λ²€νΈλ₯Ό Interactorμ μ λ¬νκ³ , Interactorλ νμν λ‘μ§μ μνν λ€ Presenterλ Routerλ₯Ό ν΅ν΄ νλ©΄ κ°±μ μ μμ²ν©λλ€.
μ ν¬ νμ ꡬ쑰 κ°νΈ κ³Όμ μμ Interactorκ° νλμ μ΄λ²€νΈ νλ¦μ μ± μμ§κ³ μ²λ¦¬ν μ μλλ‘ κ΅¬μ‘°λ₯Ό μ λΉνμ΅λλ€. κ·Έ κ²°κ³Ό, μ΄λ²€νΈ λ¨μλ‘ ν μ€νΈ νλ¦μ μ μνκ³ , νλ ₯ κ°μ²΄μμ μνΈμμ©μ κ²μ¦νκΈ°μλ μ ν©ν κ΅¬μ‘°μΈ Interactorλ₯Ό μ€μ¬μΌλ‘ ν μ€νΈλ₯Ό μ§ννκ² λμμ΅λλ€.
π μ 리νλ©΄, Interactorλ Clean Swift ꡬ쑰 μμμ λΉμ¦λμ€ λ‘μ§μ μ€μ¬μ μ΄μ, TDD ν μ€νΈ νλ¦μμ κ°μ₯ λ¨Όμ κ²μ¦μ΄ μ΄λ€μ§λ μ§μ
ππ» μ κΉ, Quick, Nimble, Cuckooκ° λκ°μ?
- Quick: iOS ν
μ€νΈ νλ μμν¬λ‘,
describe
,context
,it
λ±μ λ¬Έλ²μ ν΅ν΄Given-When-Then
ν¨ν΄μ μμ°μ€λ½κ² μ μ©ν μ μμ΅λλ€. ν μ€νΈλ₯Ό μ€λ³΅λλ λ΄μ© μμ΄ κ΅¬μ‘°ννκΈ°μ μ μ©ν©λλ€. ( λ§ν¬ ) - Nimble: iOS ν
μ€νΈ νλ μμν¬λ‘, μν κ²μ¦ λΌμ΄λΈλ¬λ¦¬μ
λλ€. λ€μν
matcher
λ₯Ό ν΅ν΄ κΈ°λνλ μνλ₯Ό λ ννλ ₯ μκ² κ²μ¦ν μ μμ΅λλ€. ( λ§ν¬ ) - Cuckoo: Swiftμ© Mock μμ± λꡬμ
λλ€. λΉλ μμ μ
Mock
κ°μ²΄λ₯Ό μλμΌλ‘ μμ±ν΄μ£ΌκΈ° λλ¬Έμ, ν μ€νΈ μ½λ λ΄μμ λ³λλ‘ μμ±ν νμκ° μμ΅λλ€. μμ±λ Mockμ StubμΌλ‘ νμ©μ΄ κ°λ₯νμ¬, νμ κΈ°λ° κ²μ¦λ μμ½κ² μνν μ μμ΅λλ€. ( λ§ν¬ )
λ μμΈν λ΄μ©μ 곡μ λ¬Έμλ₯Ό μ°Έκ³ ν΄μ£ΌμΈμ!
ππ»ββοΈ μ€μ λ‘ μ΄λ κ² μ μ©νμ΄μ!
λ€μμ μ€μ λ‘ μμ±ν Interactor ν μ€νΈ μ½λ μ€ μΌλΆλ₯Ό κ°μν μμμ λλ€. μ΄ μμλ₯Ό ν΅ν΄ 5λ¨κ³ TDD 루ν΄μ μ΄λ»κ² μ μ©νλμ§ κ΅¬μ²΄μ μΌλ‘ μκ°ν΄λ³΄κ² μ΅λλ€
1. ν μ€νΈ μ½λ μμ±
λ¨Όμ describe
, context
, it
ꡬ쑰λ₯Ό μ¬μ©ν΄ ν
μ€νΈ μλ리μ€λ₯Ό μ μν©λλ€. μ΄ κ΅¬μ‘°λ₯Ό νμ©νλ©΄ 쑰건(Given) - μ΄λ²€νΈ(When) - κΈ°λ κ²°κ³Ό(Then) νλ¦μ λ°λΌ ν
μ€νΈ λͺ©μ μ λͺ
ννκ² ννν μ μμ΅λλ€.
κ·Έλ¦¬κ³ , μ ν¬ νμμλ it λΈλ‘μ μ¬λ¬ κ°λ‘ λλ기보λ€λ νλμ it λΈλ‘ μμμ μ¬λ¬ κ²μ¦μ μννκ³ , κ° κ²μ¦μ μ£ΌμμΌλ‘ ꡬλΆνλ λ°©μμ μ¬μ©νκ³ μμ΅λλ€. μ΄ λ°©μμ describe λ° context λΈλ‘μ μμ±λ μ½λκ° μ€λ³΅ μ€νλλ κ²½μ°λ₯Ό μ€μ¬μ£Όμ΄, ν μ€νΈ μ€ν μκ°μ μ€μΌ μ μλ€λ μ₯μ μ΄ μμ΅λλ€.
describe("λ©μμ§λ₯Ό μ
λ ₯ν λ") {
it("λμ κ²μ¦νλ€") {
await sut.process(LiveChatRequest.OnChangeMessage(inputText: inputText))
// μ
λ ₯λ λ©μμ§κ° μ΅λ κΈμ μλ₯Ό μ΄κ³Όνμ§ μλλ‘ μ ννλ€
}
context("μ
λ ₯λ λ©μμ§κ° μ΅λ κΈμ μλ₯Ό μ΄κ³Όνλ€λ©΄") {
it("λμ κ²μ¦νλ€") {
await sut.process(LiveChatRequest.OnChangeMessage(inputText: inputText))
// κΈμμ μ ν ν μ€νΈλ₯Ό λ
ΈμΆνλ€
// chatInputTextλ₯Ό μ νν λ©μμ§λ‘ κ°±μ ν΄μ€λ€
}
}
}
2. μ€ν¨νλ ν μ€νΈ λ§λ€κΈ°
1λ¨κ³μμ μμ±ν μλ리μ€μ λ°λΌ ν μ€νΈ μ½λλ₯Ό μμ±ν©λλ€.
μ΄ μμ μλ μ€μ ꡬνμ΄ μκΈ° λλ¬Έμ ν μ€νΈλ λΉμ°ν μ€ν¨νκ² λλλ°, μ΄ν μ΄λ₯Ό λ°νμΌλ‘ ν μ€νΈλ₯Ό ν΅κ³Όμν€κΈ° μν΄ μ΅μνμ κΈ°λ₯μ νλμ© κ΅¬νν΄λκ°κ² λ©λλ€.
μ΄ κ³Όμ μ΄ TDDμ ν΅μ¬μΌλ‘, ν μ€νΈκ° μꡬμ¬νμ λͺ νν μ μν΄μ£Όκ³ ꡬν λ°©ν₯μ μμ°μ€λ½κ² μ‘μμ€λλ€.
describe("λ©μμ§λ₯Ό μ
λ ₯ν λ") {
beforeEach {
inputText = liveChatStub.newInputText
}
it("λμ κ²μ¦νλ€") {
await sut.process(LiveChatRequest.OnChangeMessage(inputText: inputText))
// μ
λ ₯λ λ©μμ§κ° μ΅λ κΈμ μλ₯Ό μ΄κ³Όνμ§ μλλ‘ μ ννλ€
verify(liveMessageOperation).restrictToMaxLength(text: inputText)
}
context("μ
λ ₯λ λ©μμ§κ° μ΅λ κΈμ μλ₯Ό μ΄κ³Όνλ€λ©΄") {
var limitedText: String!
beforeEach {
limitedText = liveChatStub.limitedText
stub(liveMessageOperation) {
$0.restrictToMaxLength(text: inputText).thenReturn(limitedText)
}
}
it("λμ κ²μ¦νλ€") {
await sut.process(LiveChatRequest.OnChangeMessage(inputText: inputText))
// κΈμμ μ ν ν μ€νΈλ₯Ό λ
ΈμΆνλ€
verify(presenter).present(LiveChatResponse.toast(.limitedTextError))
// chatInputTextλ₯Ό μ νν λ©μμ§λ‘ κ°±μ ν΄μ€λ€
verify(presenter).present(LiveChatResponse.chatInputText(limitedText))
}
}
}
3. μ±κ³΅νλ ν μ€νΈ λ§λ€κΈ°
μ΄μ 2λ¨κ³μμ μμ±ν ν μ€νΈκ° ν΅κ³Όλλλ‘ μ€μ ꡬν μ½λλ₯Ό μμ±ν©λλ€.
μ΄ κ³Όμ μμλ ν μ€νΈμ λ§μΆ° ν΅μ¬ λ‘μ§μ μ€μ¬μΌλ‘ μμ°μ€λ½κ² μ½λλ₯Ό μ 리νκ² λμκ³ , μ€μ μꡬμ¬νμ λ§μΆ μ΅μνμ ꡬνμ λΉ λ₯΄κ² ν보ν μ μμμ΅λλ€. λ λΆνμν λΆκΈ°λ κ³Όλν λ°©μ΄ μ½λλ₯Ό λ£μ§ μμλ λμ΄, μ 체 ꡬ쑰λ λ λ¨μνκ³ λͺ νν΄μ‘μ΅λλ€.
protocol LiveMessageOperator: Sendable {
func verifyMessage(content: String) async throws -> Bool
}
enum LiveChatRequest {
...
struct OnChangeMessage {
let inputText: String
init(inputText: String) {
self.inputText = inputText
}
}
}
enum LiveChatResponse: Equatable {
...
case chatInputText(String)
case toast(ToastKind)
enum ToastKind: Equatable {
case limitedTextError
}
}
func process(_ request: LiveChatRequest.OnChangeMessage) async {
if let limitedText = await dependency.liveMessageOperation.restrictToMaxLength(text: request.inputText) {
presenter?.present(.toast(.limitedTextError))
presenter?.present(.chatInputText(limitedText))
}
}
4. νλ ₯ κ°μ²΄(router, presenter λ±) ꡬν
μ΄μ μ€μ μμ‘΄ κ°μ²΄λ€μ ꡬνν μ°¨λ‘λ‘, λ€μκ³Ό κ°μ νλ ₯ κ°μ²΄ LiveMessageOperation
, Presenter
λ±μ κ΅¬μ± μμλ₯Ό μ΄ μμ μμ μ±μλκ°λλ€.
final actor LiveMessageOperation: LiveMessageOperator {
...
func restrictToMaxLength(text: String) async -> String? {
// ꡬν μμ± - μλ΅
}
}
extension LiveChatPresenter: LiveChatPresentationLogic {
func present(_ response: LiveChatResponse) {
switch response {
case let .toast(kind):
switch kind {
case .messageNotSent:
// ꡬν μμ± - μλ΅
}
case let .chatInputText(text):
// ꡬν μμ± - μλ΅
}
}
}
5. 리ν©ν λ§ λ° ν μ€νΈ μ μ§
λͺ¨λ ν μ€νΈκ° ν΅κ³Όν νμλ ꡬν μ½λμ ν μ€νΈ μ½λλ₯Ό μ 리νκ³ κ°μ ν©λλ€.
μ΄ λ¨κ³μμλ μ€λ³΅λ μ½λλ λΆνμν μμ ꡬν λ±μ μ κ±°νκ³ , λ λͺ ννκ³ μΌκ΄λ κ΅¬μ‘°λ‘ λ¦¬ν©ν λ§νλ μμ μ΄ μ΄λ€μ§λλ€.
μλ₯Ό λ€μ΄, ν μ€νΈ μ½λμμλ μ€λ³΅λλ mock μΈν μ΄λ assertion μ½λλ₯Ό
beforeEach
λ 컀μ€ν ν¬νΌ ν¨μλ‘ μ 리ν΄, ν μ€νΈμ λͺ©μ μ΄ λ λλ ·νκ² λλ¬λλλ‘ κ°μ ν©λλ€.
μ΄λ¬ν 리ν©ν λ§ κ³Όμ μ ν΅ν΄ ν μ€νΈμ ꡬν μ½λ λͺ¨λ κ°λ μ±κ³Ό μ μ§λ³΄μμ±μ κ°μΆ κ΅¬μ‘°λ‘ λ€λ¬μ μ μμ΅λλ€.
μ΄λ κ² 5λ¨κ³ ν μ€νΈ κ³Όμ μ λ°μΌλ©΄μ, iOSνμ λΌμ΄λΈ μ€νΈλ¦¬λ° ꡬ쑰 κ°νΈ μμ μ μμ μ μΌλ‘ λ§λ¬΄λ¦¬ν μ μμμ΅λλ€.
λλ μ
TDDλ₯Ό μ²μ λμ νλ©΄μ μΈμ κΉμλ μ μ 곡μ λ리μλ©΄,
- ν μ€νΈ μ½λκ° λͺ νν κ°λ° κ°μ΄λ μν μ ν΄μ£Όμ΄, ꡬν λ°©ν₯μ μμ§ μκ³ κ°λ°ν μ μμμ΅λλ€.
- ν μ€νΈ μ½λκ° λ§λ ¨λμ΄ μμΌλ―λ‘, 리ν©ν λ§ μ΄νμλ κΈ°μ‘΄ κΈ°λ₯μ΄ μ μ λμνλμ§ λΉ λ₯΄κ² κ²μ¦ν μ μμ΅λλ€.
- ν μ€νΈλ₯Ό μ§ννλ©΄μ κΈ°λ₯ ꡬνλΏλ§ μλλΌ μ½λμ νμ§κ³Ό ꡬ쑰κΉμ§ ν¨κ» μ κ²ν μ μμμ΅λλ€.
- μν€ν μ² κ°νΈ κ³Όμ μμ λ―Έμ² λμΉ μ μλ λΆλΆ(μ€ν, μ¬μ΄λ μ΄ννΈ λ±)μ ν μ€νΈ μ½λλ₯Ό μμ±νμ¬ λ³΄λ€ μμ νκ² μ§νν μ μμμ΅λλ€.
λ¬Όλ‘ μ²μμλ λ€μ λλ¦¬κ³ λ²κ±°λ‘κ² λκ»΄μ§ μ λ μμμ΅λλ€. νμ§λ§ λ§μ§λ§μ ν μ€νΈ 컀λ²λ¦¬μ§λ₯Ό νμΈνκ³ μλ ν μ€νΈκΉμ§ μ§νν κ²°κ³Ό, ν° λ¬Έμ μμ΄ μμ μ μΌλ‘ λμνλ€λ κ²μ νμΈνκ³ λμ ν μ€νΈ μ½λκ° μμ μ±κ³Ό μ μ§λ³΄μμ± ν보μ μΌλ§λ μ€μν μν μ νλμ§ μ€κ°ν μ μμμ΅λλ€.
λ§λ¬΄λ¦¬νλ©°
TDD λμ λΏ μλλΌ, μν€ν μ² κ΅¬μ‘° κ°νΈ, μλ‘μ΄ νμλ€κ³Όμ νμ μ΄λΌλ 3κ°μ§ ν° λ³νλ₯Ό λμμ λ§μ£Όν iOSνμκ²λ μ μκ²λ μλ―Έ μλ μκ°μ΄μλ€κ³ μκ°ν©λλ€. μ΄ κΈμ΄ TDDλ₯Ό μ²μ μλν΄λ³΄κ±°λ, ν μ€νΈ λ¬Ένλ₯Ό λμ νλ €λ κ°λ°μ λΆλ€κ» μκ²λλ§ λμμ΄ λμκΈ°λ₯Ό λ°λλλ€.
λκΉμ§ μ½μ΄μ£Όμ μ κ°μ¬ν©λλ€!