๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/Architecture

[iOS/Framework] Quick/Nimble์ด ๋ญ˜๊นŒ? (feat. ์‚ฌ์šฉ๋ฒ•)

by Fomagran ๐Ÿ’ป 2021. 8. 31.
728x90
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š” Foma ๐Ÿ’ป  ์ž…๋‹ˆ๋‹ค!

 

ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ์„ ์ฐพ์•„๋ณด๋‹ค๊ฐ€ ์šฐ์—ฐํžˆ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค์ด Quick๊ณผ Nimble์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„

 

์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ Quick๊ณผ Nimble์ด ์–ด๋–ค ๊ฒƒ์ด๊ณ  ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

๋ฐ”๋กœ ์‹œ์ž‘ํ• ๊ฒŒ์š”~


Quick

 

๋จผ์ € Quick์€ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž์‹ ๋“ค์„ ์†Œ๊ฐœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

RSpec, Specta,Ginkgo์—์„œ ์˜๊ฐ์„ ๋ฐ›์€  Swift ๋ฐ Objective-C๋ฅผ ์œ„ํ•œ ํ–‰๋™ ์ค‘์‹ฌ ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. 

 

์ฆ‰ iOS ์ „์šฉ BDD ํ”„๋ ˆ์ž„์›Œํฌ๋ผ๋Š” ๊ฒƒ์ด์ฃ .

 

์ •๋ง ๋†€๋ž๊ฒŒ๋„ ํ•œ๊ตญ์–ด ๋ฒ„์ „์œผ๋กœ ๋ฌธ์„œ๊ฐ€ ์ œ๊ณต๋˜์–ด ์žˆ์–ด ๊ตฌ์ฒด์ ์œผ๋กœ ๊ถ๊ธˆํ•˜์‹  ๋ถ„๋“ค์€ ์—ฌ๊ธฐ์—์„œ ์ฝ์–ด๋ณด์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

 

 

GitHub - Quick/Quick: The Swift (and Objective-C) testing framework.

The Swift (and Objective-C) testing framework. Contribute to Quick/Quick development by creating an account on GitHub.

github.com


Nimble

 

Nimble์€ ์•„๋ž˜์™€ ๊ฐ™์ด ์ž์‹ ๋“ค์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

 

Cedar์—์„œ ์˜๊ฐ์„ ๋ฐ›์€ Swift ๋ฐ Objective-C ํ‘œํ˜„์‹์˜ ์˜ˆ์ƒ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” Matcher ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

 

์ฆ‰, XCTest์—์„œ Assertion์„ ๋”์šฑ ํŽธ๋ฆฌํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

 

Nimble์€ Apple์—์„œ ์ œ๊ณตํ•˜๋Š” XCTest Assertion์—์„œ ๋‹จ์ ์„ ๊ทน๋ณตํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๋Š”๋ฐ์š”.

 

1. ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์€ ๋งคํฌ๋กœ 

 

XCTest Assertion์—๋Š” ๋ฌธ์ž์—ด์— ํŠน์ • ํ•˜์œ„ ๋ฌธ์ž์—ด์ด ํฌํ•จ๋˜๊ฑฐ๋‚˜ ์ˆซ์ž๊ฐ€ ๋‹ค๋ฅธ ๋ฌธ์ž์—ด๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™๋‹ค๊ณ  ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์—†์Šต๋‹ˆ๋‹ค.

 

2. ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์˜ ์–ด๋ ค์›€

 

XCTest๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋งŽ์€ ์ƒ์šฉ๊ตฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ด ๋‘๊ฐ€์ง€ ์™ธ์—๋„ ์ •๋ง ๋งŽ์€ ์ด์ ์ด ์žˆ๋Š”๋ฐ ์—ฌ๊ธฐ์„œ ๋‹ค ์†Œ๊ฐœ๋Š” ๋ชป๋“œ๋ ค์„œ ์•„๋ž˜์— ๋งํฌ๋ฅผ ๋‚จ๊ฒจ๋†“๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

GitHub - Quick/Nimble: A Matcher Framework for Swift and Objective-C

A Matcher Framework for Swift and Objective-C. Contribute to Quick/Nimble development by creating an account on GitHub.

github.com


Example

 

Quick๊ณผ Nimble์„ ์‚ฌ์šฉํ•˜๋Š” ์˜ˆ์ œ๋กœ ์˜ํ™” ๋ชฉ๋ก์„ ํ…Œ์ด๋ธ”๋ทฐ์— ๋„์šฐ๋Š” ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


Storyboard

 

๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์— ํ…Œ์ด๋ธ”๋ทฐ๋ฅผ ์„ธํŒ…ํ•ด์ค๋‹ˆ๋‹ค.

 

 

ํ…Œ์ด๋ธ”๋ทฐ ์Šคํƒ€์ผ์€ Right Detail๋กœ ํ•ด์ฃผ์‹œ๊ณ  Identifier๋Š” MovieTableViewCell๋กœ ํ•ด์ฃผ์„ธ์š”.

 


Movie

 

์˜ํ™” ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

struct Movie {
    var title: String
    var genre: Genre
    
    func genreString() -> String {
        switch genre {
        case .Action:
            return "Action"
        case .Animation:
            return "Animation"
        default:
            return "None"
        }
    }
}

Genre

 

์˜ํ™” ์žฅ๋ฅด๋ฅผ enum์œผ๋กœ ๋‚˜์—ดํ•ด์ค๋‹ˆ๋‹ค.

 

enum Genre: Int {
    case Animation
    case Action
    case None
}

MoviesDataHelper

 

์˜ํ™” ์ œ๋ชฉ๊ณผ ์žฅ๋ฅด๊ฐ€ ๋‹ด๊ฒจ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋†“์Šต๋‹ˆ๋‹ค.

 

class MoviesDataHelper {
    static func getMovies() -> [Movie] {
        return [
            Movie(title: "The Emoji Movie", genre: .Animation),
            Movie(title: "Logan", genre: .Action),
            Movie(title: "Wonder Woman", genre: .Action),
            Movie(title: "Zootopia", genre: .Animation),
            Movie(title: "The Baby Boss", genre: .Animation),
            Movie(title: "Despicable Me 3", genre: .Animation),
            Movie(title: "Spiderman: Homecoming", genre: .Action),
            Movie(title: "Dunkirk", genre: .Animation)
        ]
    }
}

MovieViewController

 

MovieViewController์— ํ…Œ์ด๋ธ”๋ทฐ๋ฅผ ์—ฐ๊ฒฐํ•ด์ค๋‹ˆ๋‹ค.

 

์˜ํ™”๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ณ  ํ…Œ์ด๋ธ”๋ทฐ์— ํƒ€์ดํ‹€๊ณผ ์žฅ๋ฅด๋ฅผ ๋ฟŒ๋ ค์ค๋‹ˆ๋‹ค.

 

import UIKit

class MovieViewController: UIViewController {

    //MARK:- IBOutlets
    
    @IBOutlet weak var table: UITableView!
    
    //MARK:- Properties
    
    var movies:[Movie] {
        return MoviesDataHelper.getMovies()
    }
    
    //MARK:- Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
       configure()
    }
    
    //MARK:- Helper Functions
    
    private func configure() {
        table.dataSource = self
    }
}

//MARK:- UITableViewDataSource

extension MovieViewController:UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movies.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = table.dequeueReusableCell(withIdentifier: "MovieTableViewCell")
        cell?.textLabel?.text = movies[indexPath.row].title
        cell?.detailTextLabel?.text = movies[indexPath.row].genreString()
        return cell!
    }
}

Pod

 

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์— pod init์„ ํ•ด์ฃผ์‹  ๋’ค ๋‘ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ถ”๊ฐ€ํ•œ ๋’ค pod install ํ•ด์ฃผ์„ธ์š”.

  pod 'Quick'
  pod 'Nimble'

MovieViewControllerSpec

 

ํ”„๋กœ์ ํŠธ๋ช…Tests์— Unit Test Case Class๋ฅผ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

 

์ด๋ฆ„์€ MovieViewControllerSpec์œผ๋กœ ํ•ด์ค๋‹ˆ๋‹ค.

 

 

Quick๊ณผ Nimble ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋ฅผ  import ํ•ด์ฃผ์„ธ์š”.

 

import Quick
import Nimble
@testable import Quick_Nimble_Example

 

ํด๋ž˜์Šค๋ฅผ QuickSpec์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ์„ธ์š”.

 

class MovieViewControllerSpec: QuickSpec {

 

์ƒ์„ ๋œ ๋ชจ๋“  ๋ฉ”์†Œ๋“œ๋ฅผ ์ง€์šฐ๊ณ   spec() ์„ ์ž‘์„ฑํ•ด์ค๋‹ˆ๋‹ค.

 

์ด์ œ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” spec() ๋ธ”๋ก ์•ˆ์— ์ž‘์„ฑํ•ด์ฃผ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

override func spec() { ... }

 

๋จผ์ € describe๋Š” BDD์—์„œ Given์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

์ฆ‰, ์–ด๋–ค ํ™˜๊ฒฝ์— ์žˆ๋Š”์ง€๋ฅผ ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

 

beforeEach ๋ธ”๋ก์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์— ๋ฏธ๋ฆฌ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ธ๋ฐ์š”.

 

MovieViewController๋ฅผ instantiate ํ•ด์ฃผ๊ณ  ํ•ด๋‹น ๋ทฐ๋ฅผ ์„ค์ •ํ•ด์ค๋‹ˆ๋‹ค. (viewDidLoad์™€ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.)

 

context๋Š” BDD์—์„œ When์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

์ฆ‰, ์–ด๋–ค ํ–‰์œ„๊ฐ€ ์ผ์–ด๋‚  ๋•Œ๋ฅผ ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

 

it์€ BDD์—์„œ Then์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

 

์ฆ‰, ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณค Nimble์˜ assertion์„ ์ด์šฉํ•ด์„œ ํ…Œ์ด๋ธ”๋ทฐ์˜ Row์˜ ๊ฐฏ์ˆ˜๊ฐ€ ์ •ํ™•ํ•œ์ง€ ํ…Œ์ŠคํŠธํ•ด์ค๋‹ˆ๋‹ค.

 

 

 var subject: MovieViewController!
           
           describe("๋ฌด๋น„๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์—์„œ") {
               beforeEach {
                subject = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MovieViewController") as? MovieViewController
                   
                   _ = subject.view
               }
               
               context("๋ทฐ๊ฐ€ ๋กœ๋“œ ๋  ๋•Œ") {
                   it("8๊ฐœ ์˜ํ™”๊ฐ€ ๋กœ๋“œ๋˜์–ด์•ผ ํ•ด") {
                       expect(subject.table.numberOfRows(inSection: 0)).to(equal(8))
                   }
               }

 

์˜ํ™” ๋ฐ์ดํ„ฐ๊ฐ€ 8๊ฐœ ์ด๋ฏ€๋กœ ํ…Œ์ด๋ธ”๋ทฐ๋Š” Row๊ฐ€ 8๊ฐœ ์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๊ณ ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

๋˜ํ•œ ์™ผ์ชฝ ํ…Œ์ŠคํŠธ ํƒญ์—์„œ ์ •์˜ํ•ด์ค€ ํ…Œ์ŠคํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ†ต๊ณผํ–ˆ๋Š”์ง€ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์—ฌ๊ธฐ์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•œ๊ฐœ ๋” ์ง„ํ–‰ํ• ๊ฑด๋ฐ์š”.

 

ํ…Œ์ด๋ธ”๋ทฐ์…€์— ์˜ฌ๋ฐ”๋ฅธ ์ œ๋ชฉ๊ณผ ์žฅ๋ฅด๊ฐ€ ๋„์›Œ์ง€๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.


Given์€ ๋˜‘๊ฐ™์ด ๋ฌด๋น„ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด๊ณ  When๊ณผ Then๋งŒ ๋ฐ”๊ฟ” ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

When์€ ํ…Œ์ด๋ธ”๋ทฐ์…€์ด ๋กœ๋“œ๋  ๋•Œ๋กœ context์— ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

 

beforeEach ๋ธ”๋ก์— ํ…Œ์ด๋ธ”๋ทฐ ์…€์„ ๊ตฌํ˜„ํ•ด์ค๋‹ˆ๋‹ค.

 

it์— ์˜ˆ์ƒ๋˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ •์˜ํ•ด์ฃผ๊ณ  Nimble์˜ assertion์„ ์ด์šฉํ•˜์—ฌ ์˜ˆ์ƒ๋˜๋Š” ์ œ๋ชฉ๊ณผ ์žฅ๋ฅด๋ฅผ ์ ์–ด์ค๋‹ˆ๋‹ค.

 

context("ํ…Œ์ด๋ธ”๋ทฐ์…€์ด ๋กœ๋“œ๋  ๋•Œ") {
                   var cell: UITableViewCell!
                   
                   beforeEach {
                       cell = subject.tableView(subject.table, cellForRowAt: IndexPath(row: 0, section: 0))
                   }
                   
                   it("ํƒ€์ดํ‹€๊ณผ ์žฅ๋ฅด๊ฐ€ ๋ณด์—ฌ์•ผ ํ•ด") {
                       expect(cell.textLabel?.text).to(equal("The Emoji Movie"))
                       expect(cell.detailTextLabel?.text).to(equal("Animation"))
                   }
               }

 

 

์ด๋ ‡๊ฒŒ ์ง„ํ–‰ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ๋‘๊ฐœ ๋‹ค ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 


์˜ค๋Š˜์€ ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์ธ Quick๊ณผ Nimble์„ ์•Œ์•„๋ณด์•˜๋Š”๋ฐ์š”.

 

์ด Quick ๋•์— BDD๋ฅผ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธ์— ์ ์šฉํ•˜๋Š”์ง€ ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๊ณ  Nimble์„ ํ†ตํ•ด์„œ

 

๋”์šฑ ํŽธ๋ฆฌํ•œ assertion์„ ์“ธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

ํ˜น์‹œ๋ผ๋„ ๊ถ๊ธˆํ•˜์‹  ์ ์ด๋‚˜ ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์„ธ์š”!


Reference

 

 

Test Driven Development (TDD) in Swift with Quick and Nimble

Test-Driven Development (TDD) is like a new art of writing code. We will show you what it is, how you apply it in Swift using Quick and Nimble.

www.appcoda.com

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€