设置Clean Swift模式有助于编写单元测试

您是否知道这些时刻可以启动应用程序,说服自己需要通过编写尽可能多的单元测试来证明质量? 在那之前,您发现几乎不可能或花费大量时间编写适当的单元测试。 好了,我有了这些时刻,并开始意识到,您的应用程序中的良好模式有助于编写单元测试。

好的模式是第一位的

为了编写更好的单元测试,您需要一个一致的模式。 如果您想编写更好的单元测试,那么MVC模式绝对不是最佳模式。 大多数情况下,您会得到一个非常大的ViewController ,其中包含各种工作:Massive View Controller。

在良好的模式下,每个组件都充当独立于任何其他组件的黑匣子。 黑匣子里面发生了什么,您不知道也不在乎。 但是这些黑匣子应具有定义明确的输入和输出。 在一致的结构中,应该更容易识别组件,测试主题,输入和输出。

代码独立性是通过在Swift中使用协议来实现的。 通过使用协议,一个组件不会直接拥有另一个组件,而是通过协议间接引用了该组件。 这样,这两个组件不会互相了解,但是都依赖于相同的协议。

在将数据从一个组件发送到另一个组件时,模型结构可以在Swift中提供数据独立性。 通过为数据请求,数据响应和视图模型创建单独的模型,可以避免在更改一个模型时还必须更改将模型用作输入或输出的对象以外的组件。 这也适用于他们的测试,这使这些测试不那么脆弱。

我遇到的一个不错的模式是使用VIP循环的Clean-Swift 。 在这种模式下,一切都会通过一个VIP循环朝一个方向前进。 它从ViewController开始,到Interactor ,再到Presenter ,最后回到ViewController 。 两个组件通过包含输出和输入逻辑的协议相互通信。

ViewController处理显示逻辑, Interactor业务逻辑和Presenter表示逻辑。 对于单元测试,这意味着您可以在每个边界上测试所有协议方法,因此它将完全覆盖所有组件。

哪里去了?

开始时,可​​能不清楚在代码中的什么位置。 为了更好地理解我们所追求的模式的实现,我将举一个例子。 在此示例中,我在UITableViewController中显示电影标题及其发行年份的列表。

电影的模型如下所示:

 结构电影{ 
让标题:字符串
让releaseDate:字符串
}

首先,我创建一个具有Interactable-Presentable-Displayable协议的ListMoviesViewControllerListMoviesInteractorListMoviesPresenter

如果您开始编写应用程序代码,则最简单的方法是先从ViewControllerInteractorPresenter开始 。 开始编写单元测试时,最简单的方法是从InteractorPresenterViewController开始 。 在VIP模式中, ViewController不仅与Interactor (和Presenter )通信,而且还与视图通信。

我从ViewController开始,我要在其中设置VIP周期并获取要显示的电影。 ViewController持有对符合Interactable协议的Interactor的引用。 ViewController将要求Interactor通过工作程序服务获取电影。

我将使用依赖项注入,并使用符合Interactable协议的Interactor实例化ViewController 。 在指定的初始值设定项init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)required init?(coder aDecoder: NSCoder)我调用了一个设置方法来初始化并设置PresenterInteractor并将Presenter连接回ViewController

  // MARK:-属性 
私有var交互器:ListMoviesInteractable?
  // MARK:-初始化程序 
覆盖init(nibName nibNameOrNil:String ?, bundle nibBundleOrNil:
捆绑吗?){
super.init(nibName:nibNameOrNil,捆绑包:nibBundleOrNil)
设定()
}
 需要初始化吗?(编码器aDecoder:NSCoder){ 
super.init(编码器:aDecoder)
设定()
}
  init(interactor:ListMoviesInteractable?= nil){ 
super.init(nibName:无,捆绑:无)
self.interactor =交互器
}
  // MARK:-设置 
私人功能setup(){
让演示者= ListMoviesPresenter(viewController:self)
interactor = ListMoviesInteractor(presenter:presenter)
}

viewDidLoad()我调用fetchMovies()方法,该方法将要求Interactor提取电影:

 私人功能fetchMovies(){ 
interactor?.fetchMovies()
}

因此,与此同时,我在ListMoviesInteractor文件中创建了Interactable协议,并使ListMoviesInteractor符合该协议:

 协议ListMoviesInteractable { 
func fetchMovies()
}
 类ListMoviesInteractor:ListMoviesInteractable {} 

该类具有对符合Presentable协议的Presenter的引用以及对符合将获取我们的电影模型的MoviesDataProvidable协议的Worker / Service的引用。 它们也在初始化器中设置:

 私人var主持人:ListMoviesPresentable 
私人var worker:MoviesDataProvidable
  init(演示者:ListMoviesPresentable,工作者: 
MoviesDataProvidable = MoviesWorker()){
self.presenter =主持人
self.worker =工人
}

Interactor类中,我实现了fetchMovies()方法,该方法向Worker询问电影,并为Worker提供了completionHandler以要求Presenter呈现电影:

  func fetchMovies(){ 
worker.fetchMovies {[弱自我](电影)->
self?.presenter.presentFetchedMovies(电影)
}
}

Presenter符合Presentable协议,并且对ViewController设置了弱引用,以防止出现引用周期。 紧接着它包含将电影模型转换为viewModels的逻辑。 viewModels将以字符串形式显示电影的发行年份,因此如下所示:

  struct ListMoviesViewModel { 
让标题:字符串
年份:字符串
}

Presenter会将来自模型的数据格式化为可呈现的东西,称为viewModel 。 然后,将这些viewModels传递回ViewController,以便ViewController可以使用它们来填充视图中的文本字段:

 协议ListMoviesPresentable { 
func presentFetchedMovies(_电影:[电影])
}
  class ListMoviesPresenter:ListMoviesPresentable { 
 私有弱变量viewController:ListMoviesDisplayable 
  init(viewController:ListMoviesDisplayable 
self.viewController = viewController
}
  func presentFetchedMovies(_电影:[电影]){ 
var displayMovies:[ListMoviesViewModel] = []
var年:字符串
电影中的电影{
如果让日期= movie.releaseDate.convertToDate(){
年= date.getYearString()
}其他{
年=“未知”
}
让displayMovie = ListMoviesViewModel(title:
movie.title,年份:年)
displayMovies.append(displayedMovie)
}
viewController?.displayFetchedMovies(displayedMovies)
}
}

ViewController需要符合Displayable协议:

 协议ListMoviesDisplayable:类{ 
func displayFetchedMovies(_电影:[ListMoviesViewModel])
}

ViewController在此示例中为UITableViewController ,其属性为private var displayedMovies: [ListMoviesViewModel] = [] 此属性设置为一个视图模型数组,该视图模型将通过protocol方法填充tableview单元:

  func displayFetchedMovies(_电影:[ListMoviesViewModel]){ 
显示电影=电影
tableView.reloadData()
}

在来自TableViewDatasource协议的func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell ,单元格使用viewModel配置:

 覆盖func tableView(_ tableView:UITableView,cellForRowAt 
indexPath:IndexPath)-> UITableViewCell {

让displayMovie = displayMovies [indexPath.row]
让单元格= tableView.dequeueReusableCell(withIdentifier:
“ MovieCell”)为! ListMoviesTableViewCell
cell.configure(with:displayedMovie)
返回单元
}

最后, UITableViewCell实现了一种使用viewModel配置其视图的方法

  func configure(with viewModel:ListMoviesViewModel){ 
titleLabel?.text = viewModel.title
yearLabel?.text = viewModel.year
}

我真的很好奇您对这种体系结构有什么经验,以及是否经历过这种模式的陷阱或弊端。

要了解如何在Clean Swift架构中编写单元测试以及什么是存根,假货和间谍,请阅读本文