감자주먹밥

MVP 알아보기 (간단한 테스트 작성) 본문

Design Pattern

MVP 알아보기 (간단한 테스트 작성)

JustHm 2023. 1. 27. 18:27
728x90

  • Presenter - 데이터 처리 (API Request,..)
  • Model - 데이터 수정, 추가
  • ViewController - View 설정, 업데이트

Apple MVC는 기존 ViewController에 View, Controller 기능이 집중되어 작성을 하며 커지게 되어 테스트를 하기 어려운 환경이다. 

Controller의 역할을 분리해 구현한것이 MVP다.

View를 최대한 멍청하게 UI를 뿌려주고 Action만 주고받고 받은 Action을 Presenter에 넘겨 그려야 하는 값만 넘겨주고 View에서 다시 그려주는 방식이다.

MVVM도 비슷해보이지만 다른점, MVP는. 1 : 1연관관계, MVVM은 * : 1 이다. 

특징

  • UI를 뿌려주는 View, 비즈니스 로직처리를 하는 Presenter를 따로 작성해 기능을 테스트 하기 용이하다.
  • View, Presenter는 1:1로 연관관계를 가지고 있다 
  • 여러 View에서 중복된 기능이 필요하다 하면 Presenter에 중복된 코드를 작성해야하는 단점이 있다.

과정

  1. View에 사용자 입력이 들어온다 .
  2. View는 Presenter에 입력이 들어온 것을 전달한다.
  3. Presenter에서 View의 입력대로 데이터를 처리하고, 처리과정에서 Model을 사용한다. (Service를 따로 가지고 요청 등)
  4. 데이터가 반환되면 View로 업데이트 한다.

구현

ViewController

import UIKit
import SnapKit

class ViewController: UIViewController {
    private lazy var presenter = Presenter(viewController: self)
    private lazy var label: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.textColor = .label
        label.font = .systemFont(ofSize: 16, weight: .medium)
        return label
    }()
    private lazy var leftButton: UIButton = {
        let button = UIButton()
        button.setTitle("Prev", for: .normal)
        button.addTarget(self, action: #selector(didTapLeftButton), for: .touchUpInside)
        return button
    }()
    private lazy var rightButton: UIButton = {
        let button = UIButton()
        button.setTitle("Next", for: .normal)
        button.addTarget(self, action: #selector(didTapRighttButton), for: .touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }
    
    
}

extension ViewController: DefaultProtocol {
    func updateViews(str: String) {
        DispatchQueue.main.async {
            self.label.text = str
        }
    }
    func setupViews() {
        view.backgroundColor = .brown
        
        [label, leftButton, rightButton].forEach {
            view.addSubview($0)
        }
        label.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
        leftButton.snp.makeConstraints {
            $0.bottom.equalToSuperview().inset(24)
            $0.left.equalToSuperview().inset(24)
        }
        rightButton.snp.makeConstraints {
            $0.bottom.equalToSuperview().inset(24)
            $0.right.equalToSuperview().inset(24)
        }
    }
}

private extension ViewController {
    @objc func didTapLeftButton() {
        presenter.didTapLeftButton()
    }
    
    @objc func didTapRighttButton() {
        presenter.didTapRighttButton()
    }
}

Presenter

import Foundation

protocol DefaultProtocol {
    func setupViews()
    func updateViews(str: String)
}

final class Presenter {
    private let viewController: DefaultProtocol
    private let service = Service()
    private var page: Int = 1
    
    init(viewController: DefaultProtocol) {
        self.viewController = viewController
    }
    
    func viewDidLoad() {
        viewController.setupViews()
        service.request(page: page)  {[weak self] str in
            self?.viewController.updateViews(str: str)
        }
    }
    
    func didTapLeftButton() {
        guard page != 1 else { return }
        page -= 1
        service.request(page: page) {[weak self] str in
            self?.viewController.updateViews(str: str)
        }
    }
    
    func didTapRighttButton() {
        page += 1
        service.request(page: page) {[weak self] str in
            self?.viewController.updateViews(str: str)
        }
    }
}

테스트

간단하게 Unit Test만 작성.

먼저 기능 테스트를 위해 Presenter에 정의했던 Protocol을 따로 구현해놓는다.

final class MockViewController: DefaultProtocol {
    var isCalledSetupViews = false
    var isCalledUpdateViews = false
    func setupViews() {
        isCalledSetupViews = true
    }
    func updateViews(str: String) {
        isCalledUpdateViews = true
    }
}

Presenter를 테스트 대상으로 코드를 작성했다.

간단하게 호출 됐는지와 페이지 인덱스만 확인하는 테스트 코드를 작성했다.

final class MVPTestTests: XCTestCase {
    var sut: Presenter!
    var viewController: MockViewController!
    override func setUpWithError() throws {
        viewController = MockViewController()
        sut = Presenter(viewController: viewController)
    }
    
    override func tearDownWithError() throws {
        viewController = nil
        sut = nil
    }
    
    func test_viewDidLoad() {
        sut.viewDidLoad()
        XCTAssertTrue(viewController.isCalledSetupViews)
        
        var resultOfTask: String?
        
        let expectation = XCTestExpectation(description: "APIPrivoderTaskExpectation")
        
        Service().request(page: sut.page) { result in
            resultOfTask = result
            expectation.fulfill() // 작업 완료 알림.
        }
        
        wait(for: [expectation], timeout: 5.0) // 이 위치에서 작업이 완료될 때 까지 최대 5초 동안 대기.
        
        XCTAssertNotNil(resultOfTask) // 성공
    }
    
    func test_tapLeft() {
        let page = sut.page
        sut.didTapLeftButton()
        if page == 1 {
            XCTAssertEqual(sut.page, page)
        } else {
            XCTAssertEqual(sut.page, page-1)
        }
    }
    func test_tapRight() {
        let page = sut.page
        sut.didTapRightButton()
        XCTAssertEqual(sut.page, page+1)
    }
    
}
728x90

'Design Pattern' 카테고리의 다른 글

MVVM - UIKit + Combine  (0) 2023.04.19
MVVM 개념과 MVVM + Rxswift  (0) 2023.01.11
Comments