iOS/Swift

[ReactorKit] ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ•ด๋ณด๊ธฐ (feat.expectation)

๊ฒฝํ‘ธ 2024. 7. 15. 23:00
๋ฐ˜์‘ํ˜•

ReactorKit์„ ๊ณต๋ถ€ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ž ์‹œ ์ง„ํ–‰ํ–ˆ๋˜ ํ”„๋กœ์ ํŠธ๋‹ค.

 

GitHub - 9oHigh/usket.RandomUser: Random User Generator๋ฅผ ํ™œ์šฉํ•œ ReactorKit & RxDataSource ํ•™์Šต

Random User Generator๋ฅผ ํ™œ์šฉํ•œ ReactorKit & RxDataSource ํ•™์Šต - 9oHigh/usket.RandomUser

github.com

 

๊ฐ„๋‹จํ•œ ์•ฑ์ด๋‹ค.

๋žœ๋ค ์œ ์ € API๋ฅผ ํ†ตํ•ด์„œ 100๋ช…์˜ ์‚ฌ๋žŒ๋“ค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ 

ํ•ด๋‹น ์ธ๋ฌผ๋“ค์„ ํŠน์ •ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜ํ•ด ๋ณด์—ฌ์ฃผ๋Š” ์•ฑ์ด๋‹ค.

๊ทธ๋ ‡๊ธฐ์— ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ„๋‹จํ•˜๋‹ค.

*UI ํ…Œ์ŠคํŠธ๋Š” ์ง„ํ–‰ํ•˜์ง€ ์•Š์•˜๋‹ค.

 

ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ ์ „

๊ฐ€์žฅ ๋จผ์ € ์–ด๋–ค ๊ฑธ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ• ์ง€ ์ •ํ•ด๋ณด์ž.

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—๋Š” ๋ฆฌ์•กํ„ฐ๊ฐ€ ๋‘๊ฐ€์ง€๋‹ค.

ํ•˜๋‚˜๋Š” ๋ฉ”์ธํ™”๋ฉด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฆฌ์•กํ„ฐ

ํ•˜๋‚˜๋Š” ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ์— ์ง„์ž…ํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋ฆฌ์•กํ„ฐ

๋‘ ๋ฆฌ์•กํ„ฐ์—์„œ ํŠน์ •ํ•œ ์•ก์…˜์„ ์ทจํ–ˆ์„ ๊ฒฝ์šฐ, ์›ํ•˜๋Š” ๋Œ€๋กœ State๊ฐ€ ๋ณ€๊ฒฝ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋ฉด ๋œ๋‹ค.

 

๋ฉ”์ธ ๋ฆฌ์•กํ„ฐ

๋ฉ”์ธ ๋ฆฌ์•กํ„ฐ๋Š” 3๊ฐ€์ง€์˜ ์•ก์…˜์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

1. ์œ ์ € ๋ฐ์ดํ„ฐ ๋กœ๋“œ

2. ๋””ํ…Œ์ผ ํ™”๋ฉด ํ‘ธ์‹œ

3. ํ‘ธ์‹œ๋œ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ nil ์ฒ˜๋ฆฌํ•˜๊ธฐ

์ง„ํ–‰ํ•  ๋ชจ๋“  ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•ด์•ผ ํ•˜๋Š” ๊ณตํ†ต๋œ ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค.

๋กœ๋“œํ•˜๊ธฐ๊ฐ€ ๋ฒˆ๊ฑฐ๋กญ๋‹ค๋ฉด mock ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค์–ด๋„ ์ข‹๋‹ค.

์•„๋ฌดํŠผ,

์ด๋ฅผ ์œ„ํ•ด์„œ setUp ๋ฉ”์„œ๋“œ์— ๋กœ๋“œ์ž‘์—…์„ ๋ฏธ๋ฆฌ ํ•ด์•ผ ํ•œ๋‹ค.

setUp์€ ๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ์ง„ํ–‰๋˜๊ธฐ ์ด์ „์— ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.

( ↔๏ธ tearDown์€ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ๋งˆ๋ฌด๋ฆฌ๋œ ํ›„ ํ˜ธ์ถœ๋œ๋‹ค )

์—ฌ๊ธฐ์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ๋กœ๋“œ๋  ์‹œ๊ฐ„์„ ๋ฒŒ๊ธฐ ์œ„ํ•ด์„œ expectaion์„ ์ด์šฉํ–ˆ๋‹ค.

override func setUp() {
    mainReactor = MainReactor()
    let expectation = self.expectation(description: "Initial data load")

    // ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋œ ํ›„์— ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ๋… ์„ค์ •
    mainReactor?.state
        .map { $0.sectionData }
        .distinctUntilChanged()
        .skip(1) // ์ฒซ ๋ฒˆ์งธ๋Š” ์ดˆ๊ธฐ ๊ฐ’์ด๋ฏ€๋กœ skip
        .subscribe(onNext: { [weak self] sectionData in
            guard let self = self, sectionData.count > 0 else {
                return
            }

            expectation.fulfill()
        })
        .disposed(by: disposeBag)

    mainReactor?.action.onNext(.load(100))
    wait(for: [expectation], timeout: 3.0)
}

 

โœ… wait ๋ฉ”์„œ๋“œ๋Š” expectation ๋ฐฐ์—ด์— ์žˆ๋Š” ๋ชจ๋“  expectation์ด fulfill ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.

โœ… timeout์„ 3์ดˆ๋กœ ์ œํ•œํ•ด ๋‘๋ฉด 3์ดˆ๊ฐ€ ๋˜๋Š” ์‹œ์ ์— fulfill ๋˜์ง€ ์•Š์€ ๋ชจ๋“  expectation์ด fulfill ๋˜๊ฒŒ ๋œ๋‹ค.

(1) load

ํŠน์ •ํ•œ ์นด์šดํŠธ๋งŒํผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ–ˆ์„ ๊ฒฝ์šฐ, ์ •์ƒ์ ์œผ๋กœ ํ•ด๋‹น ๊ฐœ์ˆ˜๋งŒํผ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๊ณ  SectionData๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. ( SectionData๋Š” ๊ฐ ์„น์…˜๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„์— ๋”ฐ๋ฅธ ์‚ฌ๋žŒ๋“ค์„ ์ •๋ฆฌํ•œ ๋”•์…”๋„ˆ๋ฆฌ๋‹ค ) 

์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค. ๋”•์…”๋„ˆ๋ฆฌ์˜ ๋ฐ์ดํ„ฐ์—๋Š” ์ค‘๋ณต๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋ฏ€๋กœ set์„ ํ†ตํ•ด ์ค‘๋ณต์„ ์ œ๊ฑฐํ•ด์•ผ ํ–ˆ๋‹ค.

func testMainReactorLoad() throws {
    let expectation = self.expectation(description: "Section Data is changed")
    var counts = 0
    mainReactor?.state
        .map { $0.sectionData }
        .distinctUntilChanged()
        .subscribe(onNext: { [weak self] sectionData in
            guard let self = self, sectionData.count > 0 else {
                return
            }

            var values: Set<PersonInfoDetail> = []
            for (_, value) in sectionData {
                values.formUnion(value)
            }

            let totalCount = values.count
            counts = totalCount
            
            expectation.fulfill()
        })
        .disposed(by: disposeBag)

    wait(for: [expectation], timeout: 3.0)
    XCTAssertEqual(counts, 100)
}


 (2) toSectionDetail

load๋ฅผ ํ†ตํ•ด ๋“ค์–ด์˜จ ๋ฐ์ดํ„ฐ ์ค‘ ์ผ๋ถ€ ์‚ฌ๋žŒ๋“ค ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ–ˆ์„ ๊ฒฝ์šฐ, ์ƒํƒœ์˜ pushingViewController๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ์ฆ‰, nil์ด ์•„๋‹ˆ๊ฒŒ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค. XCAssertNotNil์„ ํ†ตํ•ด ๊ฐ„ํŽธํ•˜๊ฒŒ nil์ด ์•„๋‹Œ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

func testMainReactorToSectionDetail() throws {
    let expectation = self.expectation(description: "PushingViewController is changed")

    mainReactor?.action.onNext(.toSectionDetail(mainReactor?.currentState.sectionData["20๋Œ€"] ?? []))

    mainReactor?.state
        .map { $0.pushingViewController }
        .subscribe(onNext: { value in
            expectation.fulfill()
        })
        .disposed(by: disposeBag)

    wait(for: [expectation], timeout: 3.0)
    XCTAssertNotNil(mainReactor?.currentState.pushingViewController)
}


 (3) removePushed

pushingViewController์˜ ๊ฐ’์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , removePushed ์•ก์…˜์„ ์š”์ฒญํ–ˆ์„ ๋•Œ, ์ •์ƒ์ ์œผ๋กœ pushingViewController๊ฐ€ nil์ด ๋˜์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

func testMainReactorRemovePushed() throws {
    let expectation = self.expectation(description: "PushingViewController is changed")
    mainReactor?.action.onNext(.removePushed)

    mainReactor?.state
        .map { $0.pushingViewController }
        .subscribe(onNext: { _ in
            expectation.fulfill()
        })
        .disposed(by: disposeBag)

    wait(for: [expectation], timeout: 3.0)
    XCTAssertNil(mainReactor?.currentState.pushingViewController)
}

 


๋””ํ…Œ์ผ ๋ฆฌ์•กํ„ฐ

๋””ํ…Œ์ผ ๋ฆฌ์•กํ„ฐ์—๋Š” ํ•˜๋‚˜์˜ ์•ก์…˜์ด ์žˆ๋‹ค.

ํŠน์ •ํ•œ ์‚ฌ๋žŒ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ, ๊ทธ ๊ทผ์ฒ˜์— ์žˆ๋Š” ์‚ฌ๋žŒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์•ก์…˜์ด ์žˆ๋‹ค.

๋”ฐ๋ผ์„œ, State์—๋Š” expandedIndexPath๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

toggle(IndexPath)

 ํŠน์ •ํ•œ IndexPath๋ฅผ ์ „๋‹ฌํ–ˆ์„ ๊ฒฝ์šฐ, ์ƒํƒœ์˜ expandedIndexPath๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด๋ด์•ผ ํ•œ๋‹ค.

func testDetailReactor() throws {
    let expectation = self.expectation(description: "IndexPath is changed")
    let detailReactor: DetailReactor = DetailReactor(people: mainReactor!.currentState.sectionData["20๋Œ€"]!)
    let indexPath: IndexPath = IndexPath(row: 0, section: 0)
    detailReactor.action.onNext(.toggle(indexPath))

    detailReactor.state
        .map { $0.expandedIndexPath }
        .distinctUntilChanged()
        .subscribe(onNext: { _ in
            expectation.fulfill()
        })
        .disposed(by: disposeBag)

    wait(for: [expectation], timeout: 3.0)
    XCTAssertNotNil(detailReactor.currentState.expandedIndexPath)
}

 

๋ฐ˜์‘ํ˜•