不起眼的TableView

我最近一直在考虑表视图。 表视图和集合视图已成为为许多类型的应用程序(尤其是内容驱动的应用程序)构建UI的默认方法。 但是,随着我们的表格视图变得越来越复杂,很难测试它们是否显示了正确的内容。

我认为创建可测试表的许多困难源于一种模式,在这种模式下,我们具有描述应用程序域的模型对象,然后尝试将其显示在表中。 尽管这些模型完美地描述了应用程序域的特定问题,但它们可能无法完全描述我们要在特定表单元格中显示的内容。

让我们看一个例子,然后看看如何改进。 我将使用表视图,因为它稍微简单一些,但是您也可以将这些技术应用于集合视图。

您的应用程序中可能有一些模型对象,它们描述了您的应用程序所关注的领域。 例如,如果您制作一个社交应用程序,则可能具有UserFriendMessage对象。 您可能是通过从api获得的Json表示形式创建对象来获得这些对象的,并在整个应用程序中使用它们来对应用程序的域进行建模。

在我们的示例中,我们可以想象一个音乐流应用程序,其中有一个特定艺术家的屏幕,其中显示了可以为该艺术家播放的所有曲目。 我们可能已经有一个域对象Track ,因此我们可以获取艺术家的所有Track ,并在表中显示它们:

  struct Track { 
let id:字符串
让标题:字符串
持续时间:双倍
让streamURL:URL
} TrackCell类:UITableViewCell {
func configure(track:Track){
//用轨道配置单元格
}
}

好吧,还不错! 当用户打开艺术家页面时,我们将从MusicAPI获取Track ,并将其显示在表格中。 然后,该单元格可以显示曲目标题和持续时间,并且当用户按下播放时,我们可以开始流式传输。

通过此设置,我们直接从域模型( Track )转到UITableViewCell

但是,当我们必须在单元格中显示Track对象未描述的信息时,该策略就会Track 。 假设我们有一个新的要求–我们的应用程序现在可以下载轨道,并且需要在列表中显示每个轨道的下载状态。

我们的用户界面将如下所示。 在此示例中,已下载了第三首曲目,而其他未下载。

不幸的是,我们的Track模型现在无法完全描述TrackTableViewCell需要了解的所有内容。 我们是从MusicAPI获取Track的,但是我们需要参考DownloadsManager来确定是否在本地下载了轨道。

也许我们可以在创建单元格并将其分别传递给下载状态时调出给DownloadsManager ,甚至可以从单元格内部调用它,如下所示:

  TrackCell类:UITableViewCell { 
func configure(track:Track){
//用轨道配置单元格
downloadIcon.showDownloaded = DownloadsManager.hasDownloaded(track)
}
}

尽管很难测试我们的单元是否显示了正确的信息:

  • 我们可以测试MusicAPI返回正确的曲目。
  • 我们可以测试DownloadsManager报告正确的轨道下载状态。
  • 但是我们无法测试显示下载的轨道的单元格实际上是否将该轨道显示为已下载。

这是因为在MusicAPIDownloadsManager之间协调的代码现在位于应用程序的UI部分,这确实很难测试。 我们的单元测试必须创建一个拥有表的视图控制器实例,滚动表以确保已创建我们要检查的单元格,并检查其视图状态(实际上应该是私有的)无论如何)

肯定有更好的办法! 让我们看看如何使该表更具可测试性。

而不是使用域模型来配置我们的单元,我们将创建一个TrackCellModel ,它将完全描述需要在单元中显示的信息,包括下载状态。

  struct TrackCellModel { 
让标题:字符串
让持续时间:字符串
let isDownloaded:布尔
} TrackCell类{
func configure(model:TrackCellModel){
//配置单元
}
}

TrackCellModel为单元格提供标题,持续时间和下载状态。 注意,我们已经将持续时间格式化为String -我们将在文本字段中显示它,因此这很有意义。 我们还将受益于将这种处理推进到数据源,这样我们就可以轻松地对其进行测试(稍后会进行更多介绍)。

通过此设置,我们从域模型( Track )到单元模型( TrackCellModel )到UITableViewCell

这些单元模型在哪里创建? 让我们看一个示例流程:

ArtistTableDataSource调用MusicAPI以获取Track 。 然后,它向DownloadsManager询问每个Track的下载状态,并使用此信息构建TrackCellModel

现在,我们的ArtistTableDataSource已与UI完全分离,只需提供要显示的TrackCellModel数组TrackCellModel 。 剩下的就是创建一个UITableViewDataSource ,它可以创建相关的单元格以显示每个单元格模型,并使用该模型配置该单元格。

通过此设置,实际的UITableViewDataSource只是从TrackCellModelUITableViewCell的轻量级转换层,因此我经常发现让视图控制器担当此角色是可以的。

现在,我们已经有了由简单模型对象描述的完整表表示形式,我们可以轻松编写测试来验证表的内容。 在创建UITableViewCell之前,我们将仅测试ArtistTableDataSource的输出。

验证下载状态是否正确显示的测试可能如下所示:

  func testArtistCellDisplaysCorrectDownloadState(){ 
让mockMusicAPI = MockMusicAPI()
嘲笑MusicAPI.returnTracksWithIds([“ a”,“ b”,“ c”,“ d”])让mockDownloadsManager = MockDownloadsManager()
mockDownloadsManager.downloadedTrackIds = [“ a”,“ d”]让dataSource = ArtistTableDataSource(
musicAPI:mockMusicAPI,
downloadsManager:mockDownloadsManager

dataSource.reload()//验证该表在下载时是否显示轨道“ a”和“ d”
XCTAssertTrue(dataSource.cellModels [0] .isDownloaded)
XCTAssertFalse(dataSource.cellModels [1] .isDownloaded)
XCTAssertFalse(dataSource.cellModels [2] .isDownloaded)
XCTAssertTrue(dataSource.cellModels [3] .isDownloaded)
}

使用这种技术,我们可以确保从多个信息源构建的表将显示正确的数据。

随着表数据变得越来越复杂,我们可以扩展此解决方案。 例如,我们还可以增加用户收藏曲目的能力,并且我们想显示每个曲目是否受收藏。 ArtistTableDataSource还可以与FavouritesManager核对每个曲目,并在isFavourited上设置isFavourited标志,然后我们可以在测试中进行验证。

谦虚的对象模式通过将我们代码中可以测试的部分中的复杂性保持在我们可以测试的代码库中,并使我们不能简单到不需要测试的部分中,来帮助我们使代码可测试。

我们没有很好的方法来测试实际的UITableView是否显示正确的UITableViewCell以及所有这些单元格都具有正确的内容。 对于我们而言,构建一个TrackCellModel数组(每个数组完整描述单元格的内容)并编写测试套件以验证这些模型是否包含给定输入范围的每个单元格的预期配置,要容易TrackCellModel

为了使我们的表尽可能谦虚,我们需要在实际的UITableViewCell进行尽可能少的计算(流程中不可测的部分)。 我们将通过简单地将TrackCellModel的数据绑定到单元格的视图,并将所有艰苦的工作推送到ArtistTableDataSource来实现这ArtistTableDataSource

例如,当您将域模型传递到单元格时,它可能包含一个Date ,然后使用单元格内的DateFormatter将其转换为String 。 使用单元模型,可以通过在将域模型的Date转换为String之前将其转换为String ,从而增加表的可测试性。 然后,我们可以测试日期格式是否已正确应用于单元格模型,并且在实际单元格本身中,我们仅将该字符串绑定到UILabel

通过创建一个可测试的数据源,我们不仅可以测试单个单元的配置,还可以测试整个表的内容。 例如,我们可以为用户提供按最受欢迎或最新的方式更改曲目的排序顺序的功能。 我们可以编写一个测试来创建一个连接到模拟MusicAPI ,设置排序顺序,并验证所生成的TrackCellModels是否以正确的顺序出现。

例如,这是我们如何测试按字母顺序升序的排序选项:

  func testAlphabeticalOrdering { 
self.mockMusicAPI.returnTracksWithTitles([“ Wonderwall”,
“你好”,
“超音速”])
self.dataSource.sortMode = .alphabeticalAscending
dataSource.reload()XCTAssertEqual(dataSource.cellModels [0] .title,“你好”)
XCTAssertEqual(dataSource.cellModels [1] .title,“超音速”)
XCTAssertEqual(dataSource.cellModels [2] .title,“奇妙的墙”)
}

我们还可以扩展模型以包括不同的单元格类型。 除了显示特定艺术家的曲目外,我们可能还希望显示包含该艺术家和相关艺术家的播放列表。 我们将为每一个单独的单元格。 我们的模型可能如下所示:

 枚举ArtistCellModelType { 
案例跟踪(TrackCellModel)
案例播放列表(PlaylistCellModel)
案例相关艺术家(RelatedArtistCellModel)
}

现在,我们的ArtistTableDataSource将产生一个ArtistCellModelType数组。

现在,我们可以编写一个测试来验证特定单元格是否出现在表格的正确位置。 以下测试验证播放列表单元格将显示在跟踪单元格下面:

  func testPlaylistAppearsOnArtistPage { 
self.mockMusicAPI.returnTracksWithIds([“ a”,“ b”,“ c”])
self.mockPlaylistAPI.returnPlaylistsWithName([“热门歌曲”])
self.dataSource.reload()//在ArtistCellModelType上添加便利扩展
//使我们的测试更容易阅读
XCTAssertTrue(dataSource.cellModels [0] .isTrack(withId:“ a”))
XCTAssertTrue(dataSource.cellModels [1] .isTrack(withId:“ b”))
XCTAssertTrue(dataSource.cellModels [2] .isTrack(withId:“ c”))
XCTAssertTrue(dataSource.cellModels [3] .isPlaylist(withName:“ Top Hits”))
}

由于ArtistCellModelType的扩展,该测试的可读性ArtistCellModelType 。 这种扩展名仅用于测试,因此可以在测试目标中声明。 随着表数据变得越来越复杂,我发现维护一组良好的扩展名有助于使测试简短易懂。

我们还可以做更多的事情。

  • 也许用户可以收藏曲目,并且收藏的曲目出现在列表的顶部。 通过将MockFavouritesManager传递到我们的数据源,我们可以将某些曲目ID标记为收藏,并验证它们是否首先出现在数组中。
  • 也许我们增加了一项功能,对于年轻用户,带有明确歌词的曲目将从列表中过滤掉。 我们可以User具有启用此功能ageUser来初始化ArtistTableDataSource 。 然后,我们可以告诉MockMusicAPI返回containsExplicitLyrics标志设置为true轨道,并验证我们的数据源在表中未包含这些轨道。

最近,我一直在对表视图和集合视图使用“单元模型”方法,并且发现它是增加可测试性的好模式。 将表格内容完整地表示为模型对象,可以轻松编写测试来验证各个单元格是否包含正确的内容,以及表格的总体结构是否符合我们的预期。 我希望您也觉得它有用!

想法/评论/投诉/想聊天吗? 通知我。