[EasyCloset] ๐Ÿง‘โ€๐Ÿ”ฌ ๋ทฐ๋ชจ๋ธ ์œ ๋‹› ํ…Œ์ŠคํŠธ

Mason Kimยท2023๋…„ 6์›” 23์ผ
0

EasyCloset ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๋ชฉ๋ก ๋ณด๊ธฐ
5/5

ViewModel ์œ ๋‹› ํ…Œ์ŠคํŠธ, CacheManager ์œ ๋‹› ํ…Œ์ŠคํŠธ

๋ฐฐ๊ฒฝ

  • ์‚ฌ์šฉ์ž์˜ ์•ก์…˜์˜ ๋กœ์ง์„ ๋‹ด๊ณ  ์žˆ๋Š” ViewModel ์˜ unit-test ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ–ˆ์Œ

์œ ๋‹›ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ Mock Repository ๊ตฌํ˜„

image
  • ViewModel์€ Repository์— ์˜์กดํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ์‹ค์ œ Repository๊ฐ€ ์•„๋‹Œ ๊ฐ€์ƒ์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ๋™์ž‘ํ•˜๋Š” Mock Repository๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ViewModel์„ ํ…Œ์ŠคํŠธ

    ๋“œ๋””์–ด protocol ์ถ”์ƒํ™”, ์˜์กด์„ฑ ์ฃผ์ž…์ด ๋น›์„ ๋ฐœํ•  ๋•Œ๊ฐ€ ๋˜์—ˆ๋‹ค..!๐Ÿ˜ฒ๐Ÿฅ‚

  • ๊ธฐ์กด์— ๋งŒ๋“ค์–ด๋†“์€ Mock ๊ฐ์ฒด๋“ค์„ return ํ•ด์ฃผ๋„๋ก ๊ตฌํ˜„ํ•จ

    ์ผ๋‹จ์€ Repository๊ฐ€ ์„ฑ๊ณต์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋งŒ ๋ฐœ์ƒ์‹œํ‚ค๋„๋ก ๊ตฌํ˜„. ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ํ•„์š”ํ•  ์‹œ, ๊ฐ ์‘๋‹ต์„ property ๋กœ ๋งŒ๋“ค์–ด ํ…Œ์ŠคํŠธ ์ง์ „ ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ฃผ์ž…์‹œ์ผœ์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•˜์Œ

final class MockClothesRepository: ClothesRepositoryProtocol {
  func fetchClothesList() -> AnyPublisher<ClothesList, RepositoryError> {
    return Just(ClothesList.mocks)
      .setFailureType(to: RepositoryError.self)
      .eraseToAnyPublisher()
  }
...

ClothesViewModel ํ…Œ์ŠคํŠธ

  • ์ด์ฒ˜๋Ÿผ ClothesRepositoryProtocol ๋ฅผ ๊ตฌํ˜„ํ•œ MockRepository๋ฅผ ViewModel ์— ์ฃผ์ž…ํ•ด์„œ sut๋ฅผ ์ดˆ๊ธฐํ™”
var sut: ClothesViewModel!

override func setUpWithError() throws {
  let mockRepository = MockClothesRepository()
  sut = ClothesViewModel(repository: mockRepository)
...

๊ณ„์ ˆ filter๊ฐ€ ์ ์šฉ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ

  • ์‚ฌ์šฉ์ž๊ฐ€ ํ•„ํ„ฐ ๊ฒ€์ƒ‰์„ ์ž…๋ ฅํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค์ธ filter๋ฅด ๊ฑธ์—ˆ์„ ๋•Œ,
  • ๊ธฐ๋Œ€ํ•˜๋Š” ์ถœ๋ ฅ๊ฐ’์ด ๋‚˜์˜ค๋Š”์ง€ ํ…Œ์ŠคํŠธ
func test_๊ณ„์ ˆ_filter๊ฐ€_์ ์šฉ๋˜๋Š”์ง€_ํ…Œ์ŠคํŠธ() {
  // given
  let categories = ClothesCategory.allCases
  let weatherFilterType: WeatherType = .fall
  sut.searchFilters.send([.weather(weatherFilterType)]) // ๊ณ„์ ˆ filter ๊ฒ€์ƒ‰
  
  // ๊ฐ๊ฐ์˜ ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ expectation
  var expectations: [XCTestExpectation] = []
  
  categories.forEach { category in
    let expectation = XCTestExpectation(description: category.korean)
    expectations.append(expectation)
    
    // when
    sut.clothes(of: category)
      .sink { clothes in     
        // then
        XCTAssertTrue(clothes.allSatisfy {
	 // ๋ชจ๋“  ๊ฒฐ๊ณผ๊ฐ’์ด ํ•ด๋‹น filter์™€ ๋™์ผํ•œ์ง€ ์ฒดํฌ
          $0.weatherType == weatherFilterType
        })
        expectation.fulfill()
      }
      .store(in: &cancellables)
...

์ด๋ฏธ์ง€ ์บ์‹œ๋งค๋‹ˆ์ € ์œ ๋‹› ํ…Œ์ŠคํŠธ

  • ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์ธ NSCache ๋ฅผ ํ™œ์šฉํ•œ ImageCacheManager ๋ฅผ ํ…Œ์ŠคํŠธ
  • ์‹ฑ๊ธ€ํ„ด ํ˜•ํƒœ์ด๊ธด ํ–ˆ์ง€๋งŒ, ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ๊ฐ์˜ ์ผ€์ด์Šค ํ›„ removeAll ๋งŒ ํ˜ธ์ถœ ํ•ด ์ฃผ๋ฉด ๋ฌธ์ œ์—†์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ ํŒ๋‹จ
  • ํŠนํžˆ, ๊ตฌํ˜„ํ–ˆ๋˜ ๊ฐฏ์ˆ˜์ œํ•œ๊ณผ ์šฉ๋Ÿ‰์ œํ•œ์ด ์ ์ ˆํžˆ ์ด๋ค„์ง€๋Š”์ง€๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์—ˆ์Œ

    ๋‹ค๋งŒ, ํ•ด๋‹น ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฐ˜๋“œ์‹œ ์„ฑ๊ณตํ•˜๋ฆฌ๋ผ๋Š” ๋ณด์žฅ์€ ํ•  ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ,
    NSCache์˜ ํŠน์„ฑ์ƒ countLimit๊ณผ totalCostLimit์ด ์ œ๊ณตํ•˜๋Š” limit์ด imprecise ํ•˜๋‹ค๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ.
    ๋‹คํ–‰ํžˆ, ์–ด๋Š ์ •๋„๊นŒ์ง€๋Š” ์˜ˆ์ƒํ•œ ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Œ

  • ์ €์žฅ์‹œ ๊ฐฏ์ˆ˜์ œํ•œ์ด ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธ
func test_์ €์žฅ์‹œ_๊ฐฏ์ˆ˜์ œํ•œ์ด_์ ์šฉ๋˜๋Š”์ง€_ํ™•์ธ() {
  // given
  let ids = (0...10).map { _ in UUID() }
  let countLimit = 3
  
  // when
  sut.countLimit = countLimit // ์บ์‹ฑ ์ €์žฅ ์ด๋ฏธ์ง€ ์ˆ˜๋ฅผ 3๊ฐœ๋กœ ์ œํ•œ
  ids.forEach { id in
    sut.store(UIImage(), for: id)
  }
  
  // then
  let storedImages = ids
    .compactMap { sut.get(for: $0) }

  // ๊ฐฏ์ˆ˜ ์ œํ•œ์„ ์ค€ ๊ฐฏ์ˆ˜๋ณด๋‹ค ๊ฐ™๊ฑฐ๋‚˜ ์ ๊ฒŒ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  XCTAssertGreaterThanOrEqual(countLimit, storedImages.count)
  • ์ €์žฅ์‹œ ์šฉ๋Ÿ‰์ œํ•œ์ด ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธ
    • SF Symbol์˜ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ 5์žฅ์„ ์ €์žฅํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๋ฉฐ,
    • 5์žฅ์˜ ์ด๋ฏธ์ง€ ์ค‘ ๊ฐ€์žฅ ์ž‘์€ ์šฉ๋Ÿ‰ * 3 ์œผ๋กœ ์ œ์•ฝ์„ ์คŒ
func test_์ €์žฅ์‹œ_์šฉ๋Ÿ‰์ œํ•œ์ด_์ ์šฉ๋˜๋Š”์ง€_ํ™•์ธ() {
  // given
  let images = [
    UIImage(systemName: "pencil")!,
    UIImage(systemName: "pencil.slash")!,
    UIImage(systemName: "pencil.circle")!,
    UIImage(systemName: "pencil.circle.fill")!,
    UIImage(systemName: "pencil.line")!
  ]
  // ๊ฐ€์žฅ ์ž‘์€ ์ด๋ฏธ์ง€ ์šฉ๋Ÿ‰
  let imageDataSize = images.compactMap { $0.pngData()?.count }.min() ?? 0
  let ids = (0..<5).map { _ in UUID() }
  
  // when
  sut.byteLimit = imageDataSize * 3 // ๋Œ€๋žต ์ด๋ฏธ์ง€ 3๊ฐœ ์ •๋„์˜ ์‚ฌ์ด์ฆˆ๋งŒํผ ์šฉ๋Ÿ‰ ์ œํ•œ์„ ์คŒ
  zip(images, ids).forEach { image, id in
    sut.store(image, for: id)
  }
  
  // then
  let storedImages = ids
    .compactMap { sut.get(for: $0) }

  // ์šฉ๋Ÿ‰ ์ œํ•œ์„ ์ค€ 3๊ฐœ๋กœ ์ฃผ์—ˆ๊ธฐ์—, ๊ทธ๋ณด๋‹ค ๊ฐ™๊ฑฐ๋‚˜ ์ ๊ฒŒ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  XCTAssertGreaterThanOrEqual(3, storedImages.count)
profile
iOS developer

0๊ฐœ์˜ ๋Œ“๊ธ€