Clean Swift架构中的单元测试

读者好!


测试是任何开发必不可少的部分,这已经不是什么秘密了。 在之前的文章中,我们介绍了Clean Swift架构的基本架构,现在是时候学习如何使用测试覆盖其Unit了 。 我们将以关于工人的文章中的项目为基础,并分析要点。



理论


由于依赖注入面向协议Clean Swift中的所有场景组件都彼此独立,可以单独进行测试。 例如, Interactor依赖于PresenterWorker ,但是这些依赖关系是可选的并且基于协议。 因此, Interactor可以在没有Presenter'aWorker'a的情况下执行其工作(尽管很差),并且我们还可以将其替换为根据其协议签名的其他对象。


由于我们要分别测试每个组件,因此需要用伪组件替换依赖 。 这将帮助我们进行间谍(Spy)。 间谍是测试对象,它们实现了我们想要注入并跟踪其中的方法调用的协议。 换句话说,我们为PresenterWorker创建Spy ,然后将它们注入到Interactor中以跟踪方法调用。



公平地说,我还要添加一些测试对象( 测试双打DummyFakeStubMock 。 但是在本文的框架内,我们不会影响它们。 在此处阅读更多内容-TestDoubles


练习


最后,让我们开始做生意。 为了节省您的时间,我们将考虑抽象代码,而不涉及每种方法的实现细节。 可以在此处找到应用程序代码的详细信息CleanSwiftTests


对于场景的每个组件,我们创建一个包含测试的文件,并测试组件依赖关系的测试倍数Test Doubles )。


这种结构的一个例子:



每个测试文件(用于组件)的结构看起来都相同,并且遵循以下编写顺序:


  • 我们用SUT (我们要测试的对象)声明一个变量,并声明其主要依赖关系的变量
  • 我们在setUp()中初始化SUT及其依赖项,然后在tearDown()中清除它们
  • 测试方法

正如我们在理论上所讨论的,可以分别测试场景的每个组成部分。 我们可以将测试副本( Spy )注入其依赖项,从而监视SUT方法的操作。 让我们仔细研究一下使用Home场景的Interactor示例编写测试的过程。


HomeInteractor依赖于两个对象-PresenterWorker 。 该类中的两个变量都具有协议类型。 这意味着我们可以创建根据HomePresentationLogicHomeWorkingLogic协议签名的测试副本,然后将其注入HomeInteractor中


final class HomeInteractor: HomeBusinessLogic, HomeDataStore { // MARK: - Public Properties var presenter: HomePresentationLogic? lazy var worker: HomeWorkingLogic = HomeWorker() var users: [User] = [] var selectedUser: User? // MARK: - HomeBusinessLogic func fetchUsers(_ request: HomeModels.FetchUsers.Request) { // ... } func selectUser(_ request: HomeModels.SelectUser.Request) { // ... } 

我们将测试两种方法:


  • fetchUsers(:) 。 负责通过API获取用户列表。 使用Worker发送API请求。
  • selectUser(:) 。 负责从已加载的用户( users )列表中选择活动用户( selectedUser )。

要开始编写Interactor的测试,我们需要创建间谍程序,以跟踪HomePresentationLogicHomeWorkingLogic中方法的调用。 为此,请在目录“ CleanSwiftTestsTests / Stores / Home / TestDoubles / Spies”中创建HomePresentationLogicSpy类,签署协议HomePresentationLogic并实现此协议的方法。


 final class HomePresentationLogicSpy: HomePresentationLogic { // MARK: - Public Properties private(set) var isCalledPresentFetchedUsers = false // MARK: - Public Methods func presentFetchedUsers(_ response: HomeModels.FetchUsers.Response) { isCalledPresentFetchedUsers = true } } 

这里的一切都非常透明。 如果调用presentFetchedUsersHomePresentationLogic )方法,则将isCalledPresentFetchedUsers变量的值设置为true 。 因此,我们可以跟踪在Interactor的测试过程中是否调用了此方法。


使用相同的原理,创建HomeWorkingLogicSpy 。 区别之一是我们称为完成 ,因为 Interactor中的部分代码将被包装在此方法的闭包中。 HomeWorkingLogic方法处理网络请求。 在测试期间,我们需要避免实际的网络请求。 为此,我们将其替换为测试用例,该用例跟踪方法调用并返回模板数据,但不向网络发出任何请求。


 final class HomeWorkingLogicSpy: HomeWorkingLogic { // MARK: - Public Properties private(set) var isCalledFetchUsers = false let users: [User] = [ User(id: 1, name: "Ivan", username: "ivan91"), User(id: 2, name: "Igor", username: "igor_test") ] // MARK: - Public Methods func fetchUsers(_ completion: @escaping ([User]?) -> Void) { isCalledFetchUsers = true completion(users) } } 

接下来,我们创建HomeInteractorTests类,通过它我们将测试HomeInteractor


 final class HomeInteractorTests: XCTestCase { // MARK: - Private Properties private var sut: HomeInteractor! private var worker: HomeWorkingLogicSpy! private var presenter: HomePresentationLogicSpy! // MARK: - Lifecycle override func setUp() { super.setUp() let homeInteractor = HomeInteractor() let homeWorker = HomeWorkingLogicSpy() let homePresenter = HomePresentationLogicSpy() homeInteractor.worker = homeWorker homeInteractor.presenter = homePresenter sut = homeInteractor worker = homeWorker presenter = homePresenter } override func tearDown() { sut = nil worker = nil presenter = nil super.tearDown() } // MARK: - Public Methods func testFetchUsers() { // ... } func testSelectUser() { // ... } } 

我们指出了三个主要变量-sutworkerpresenter


setUp()中,初始化必要的对象,在Interactor中注入依赖项并将对象分配给类变量。
tearDown()中,我们清除类变量以确保实验的纯度。


在该方法完成工作之前,将在测试方法开始之前调用setUp()方法,例如testFetchUsers()tearDown() 。 因此,我们在每个测试方法运行之前重新创建测试对象( sut )。


接下来是测试方法本身。 该结构分为3个主要逻辑块-必要对象的创建, SUT中测试方法的启动以及结果验证。 在下面的示例中,我们创建一个请求(在我们的示例中,该请求没有参数),运行fetchUsers(:) Interactor'a方法,然后检查是否在HomeWorkingLogicSpyHomePresentationLogicSpy中调用了必要的方法。 我们还检查Interactor是否已将从Worker接收的测试数据保存到其DataStore中


 func testFetchUsers() { let request = HomeModels.FetchUsers.Request() sut.fetchUsers(request) XCTAssertTrue(worker.isCalledFetchUsers, "Not started worker.fetchUsers(:)") XCTAssertTrue(presenter.isCalledPresentFetchedUsers, "Not started presenter.presentFetchedUsers(:)") XCTAssertEqual(sut.users.count, worker.users.count) } 

我们将通过类似的结构测试用户的选择。 我们声明变量ExpectationIdExpectationName ,通过它们我们将比较用户选择的结果。 users变量存储我们分配给Interactor的用户测试列表。 因为 测试方法彼此独立地调用,并且在tearDown()中将数据清零,然后Interactor用户列表为空,我们需要用一些东西填充它。 然后,在调用sut.selectUser(:)之后 ,检查是否在DataStore Interactor'a中分配了该用户,以及该用户是否合适。


 func testSelectUser() { let expectationId = 2 let expectationName = "Vasya" let users = [ User(id: 1, name: "Ivan", username: "ivan"), User(id: 2, name: "Vasya", username: "vasya91"), User(id: 3, name: "Maria", username: "maria_love") ] let request = HomeModels.SelectUser.Request(index: 1) sut.users = users sut.selectUser(request) XCTAssertNotNil(sut.selectedUser, "User not selected") XCTAssertEqual(sut.selectedUser?.id, expectationId) XCTAssertEqual(sut.selectedUser?.name, expectationName) } 

测试Presenter'aViewController'a的原理相同,差异最小。 区别之一是,为了测试ViewController,您将需要创建一个UIWindow并从setUp()中Storyboard中获取控制器,并在表和集合上创建Spy对象。 但是这些细微差别因需求而异。


为了完整起见,建议您通过本文末尾的链接使您熟悉该项目。


结论


我们已经介绍了在Clean Swift架构上测试应用程序的基本原理。 它与其他体系结构上的测试项目没有本质上的重大差异,所有相同的测试都加倍了,注入和协议相同。 最主要的是不要忘记,每个VIP周期都应该有一个(也只有一个!)责任。 这将使代码更简洁,测试也更加明显。


链接到项目: CleanSwiftTests
撰写文章的帮助: Bastien


系列文章


  1. Clean Swift体系结构概述
  2. Clean Swift体系结构中的路由器和数据传递
  3. 干净的Swift体系结构中的工人
  4. Clean Swift架构中的单元测试(您在这里)
  5. 一个简单的在线商店架构Clean Swift的示例

Source: https://habr.com/ru/post/zh-CN483882/


All Articles