Tag: tdd

Objective-C中的单元测试

飞行猕猴桃 每种语言都有自己的规则。 查看语法,语言功能或语言类型都没关系。 一切都改变了,同时保持不变。 在我们的主题应用程序中,我们将使用swift,但是由于Objective-C是iOS开发的起源,因此我们绕了弯路,以了解此处的具体操作。 我不是XCTest的忠实拥护者。 不要误会我的意思,它确实可以正常工作..但是就这样..就可以了。 了解我们要测试的应用程序和操作的整体状态,取决于我们命名测试的能力。 总的来说,我们很讨厌这样做。 一种替代方法是基于RSpec的框架。 他们在我们的测试中添加了语法糖,可能只是为了了解测试2小时或解决问题而有所不同。 猕猴桃 对于Objective-C,这是猕猴桃。 它提供了我们可以描述,正在测试什么以及处于何种状态的不同阶段: describe :描述被测系统(sut) 上下文 :描述缝合状态 关于这些定义,我们可以编写一个带有演示类的简单测试文件结构: 私下入侵 我已经提到每种语言都有自己的怪癖。 在Objective-C中,私有不存在。 相反,它仅对编译器可见。 我们仍然可以通过运行时将消息发送到对象的特定实例,从而忽略可见性。 好的..我们不会开始使用objc_msgSend。 不用担心 相反,我们可以使用类别来完成这项工作。 在我们的测试类中,我们将添加一个类别,其中包含我们要测试的所有私有属性和方法,这些属性和方法无法从外部访问。 这样做将为编译器提供可见性,即使它们被认为是私有的,我们也可以将任何消息发送给对象。 存根 时不时地,我们不得不依靠我们无法控制的API。 这可以是简单的SDK API或网络服务。 想象一下有一个登录视图控制器。 您如何测试呢? 尤其是当您的测试应该在没有外部依赖性的黑暗洞穴中运行时,该怎么办? 欢迎进行嘲笑和存根。 在本文中,我们将讨论如何在单元测试和集成测试中对方法调用进行存根。 存根的想法是通过指定返回值或简单地替换它来定义方法调用的行为。要存根方法,我们需要对象对此方法做出响应。 有了这个,我们可以简单地通过编写以下代码来定义返回值: [testClass存根:@selector(方法)和Return:Value]; 该值可以是任何值:布尔值,NSString,NSInteger或您返回的任何值。 猕猴桃不在乎。 这样,我们可以简单地替换本地网络服务器的登录方法。 是不是很简单? 但是,当我们无法获得所用类的实例时,我们该怎么办? 铁杆存根 为此,有方法令人毛骨悚然。 这是Objective-C内的黑魔法的一部分,不应粗心使用。 但是,如果您知道自己在做什么,并且将其用于测试,则可能会从中获得一些价值。 那么什么是方法混乱? 基本上在运行时,您将另一个方法添加到一个类中,并用另一个方法切换该方法的签名。 关于我们先前描述的登录方法,让我们添加另一个名为swizzleLogin的方法。 通过使用swizzleLogin修改登录方法,现在每次登录都将执行swizzleLogin,反之亦然。 这很容易做错,所以让我们介绍一下JRSwizzle。 […]

使用FBSnapshotTestCase测试用户界面

有时,您需要为应用程序的UI添加自动测试。 有几种不同的方法可以实现此目的。 在测试中,您可以获取屏幕的元素并断言所有帧是否都符合您的期望。 根据要测试的UI,这可能需要很多工作。 或者,您可以使用Xcode提供的UI测试。 但是这些操作非常慢,以我的经验,有时它们只是停止工作。 有更好的选择。 Facebook有一个名为FBSnapshotTestCase的开源组件。 此类允许您创建快照测试。 快照测试将视图的UI与快照的外观进行比较。 让我们看看它是如何工作的。 我要测试的UI如下所示: 有两个带有标签,按钮和两个文本字段的堆栈视图。 使用迦太基安装FBSnapshotTestCase 我是迦太基人。 因此,我将向您展示如何使用迦太基在测试目标中安装FBSnapshotTestCase并使用它为简单的登录屏幕添加快照测试。 创建一个如下所示的Cartfile: github“ facebook / ios-snapshot-test-case” 然后要求Carthage使用以下命令创建动态框架 迦太基更新-平台iOS 迦太基将从github上获取源代码并构建框架。 迦太基完成后,将框架从迦太基/ Bild / iOS文件夹中拖动到测试目标的链接二进制文件与库构建阶段: 接下来,向测试目标添加一个新的运行脚本构建阶段。 输入命令 / usr / local / bin / carthage复制框架 并添加输入文件$(SRCROOT)/Carthage/Build/iOS/FBSnapshotTestCase.framework 。 在Xcode中,它应如下所示: 配置FBSnapshotTestCase 接下来,您需要配置放置快照的目录。 每当快照测试失败时,您还可以告诉FBSnapshotTestCase创建差异映像。 这意味着,当测试失败时,将创建一个图像,该图像显示预期的UI和使测试失败的UI之间的差异。 这样,您可以找出UI中发生了什么变化。 打开您使用测试的方案,并添加以下环境变量: FB_REFERENCE_IMAGE_DIR:$(SOURCE_ROOT)/ $(PROJECT_NAME)测试/失败差异 FB_REFERENCE_IMAGE_DIR:$(SOURCE_ROOT)/ $(PROJECT_NAME)测试 在Xcode中,它看起来像这样: 创建快照 要创建快照测试,请将FBSnapshotTestCase的子类添加到测试目标并添加以下导入语句: 导入FBSnapshotTestCase […]

测试驱动开发:开发人员魔术棒

让我们讲一个故事。 “不久以前,我已经为即将上线的测试仪提供了我的应用程序的构建。 我已经对其进行了一些修复,根据我的开发人员健全性测试,这些构建已使生产准备就绪。 但是经过测试,情况完全不同。 在某些情况下,由于某些较早的修复程序,我遇到了错误。 搞什么鬼??? 除了使我的构建稳定之外,我使它更加不稳定。” 我不是唯一遇到这种情况的人,但是许多开发人员也遇到了这种情况。 那么我们该怎么做呢? 答案是测试驱动开发(TDD)。 我们要学什么? TDD上有很多博客。 我将列出您可以在其中找到的一些最佳参考。 我们不会讨论理论上的TDD概念,而是将重点放在我们如何从开发人员的角度计划实现TDD。 什么是TDD? 为什么选择TDD? 如何规划TDD? TDD的警告。 您需要TDD吗? 看起来太多了吗? 不用担心,我们会做的很快而简短。 😉 什么是TDD? 测试驱动开发是美国软件工程师肯特·贝克(Kent Beck)提出的开发程序,在编写代码的同时,我们还记录了测试用例。 从而允许我们在继续开发的同时测试我们的代码。 下图描述了传统开发与TDD之间的区别。 在传统开发中,我们首先开发代码,完成功能并进行手动测试。 但是在TDD中,我们首先写下测试用例,然后相应地开发代码。 这有助于我们最大程度地减少代码失败或错误的机会。 如果不更新旧的测试用例,则开发的任何新功能也应尊重现有的测试用例。 TDD基于RGR的概念,即红色,绿色和重构。 红色:我们首先编写失败的测试用例。 绿色:我们编写通过测试所需的最少代码。 重构:如果需要测试代码,我们也会重构代码。 为什么选择TDD? 最大限度地减少代码失败的机会。 最小化团队中任何新开发人员在代码库上工作的机会。 由于引入了新功能或错误修复,破坏现有功能的机会较小。 提高产品知识。 改进编码标准。 如何规划TDD? 百万美元问题来了? 我们都知道什么是TDD。 但是我们如何计划TDD? 我们将如何决定需要编写哪些测试用例? 我要编写并开发该功能,然后为它们编写测试还是将来如何? 困惑? 了解正确实施的功能。 让我们以以下要求为例: 要求: 构建一个程序,该程序将从用户那里获取城市的人口输入,并返回该城市所属的类别。 以下是城市类型及其人口范围: 小(5,000至10,000) 中(10,000至50,000) […]

在Swift中编写更好的单元测试:第一部分

问题 瞧,我们是这里的朋友,所以我觉得我很坦白:我在职业生涯中写了一些非常糟糕的单元测试。 20行怪物,具有多个模拟和断言以及异步期望。 您在书中看到的这类标题为“如何修复以前工作过的白痴留下的混乱”的书。我还必须在写完单元测试和不太那么说的代码后维护该代码。更好。 可以这么说,我现在将编写“好的”单元测试作为优先事项。 在开始之前,我将定义我认为是“良好”的单元测试。 如果我们可以同意一个单元测试(或者实际上是任何一个测试)是由一些设置 ,我们正在测试的动作以及关于该动作效果的断言组成的,那么我可以这么简单地说,一种“好的”单元测试可以使这三个组件中的每一个都清晰可见。 (此外:这可能与您对“好的”单元测试的定义有所不同,而这只是我们俩都必须忍受的。) 在一些博客文章中,我将向您展示我们在Clue所做的一些工作,以确保我们始终试图编写“良好”的单元测试。 在本文中,我们将看一个简单的技巧,我们可以使用它来最小化单元测试的设置部分,同时保持清晰度。 假设我们要在一个简单的Swift结构上对相等方法进行单元测试。 struct用户:Equatable { 命名:字符串 让电子邮件:字符串 让需求验证:布尔 静态函数==(lhs:用户,rhs:用户)-> Bool { 返回lhs.name == rhs.name && lhs.email == rhs.email && lhs.needsVerification == rhs.needsVerification } } 我们可以(无限)有许多不同的参数组合可以传递给此方法进行测试。 显然,我们无法对所有这些进行测试,因此我们必须选择一些代表总体趋势的案例。 对该方法进行单元测试的有效方法(实际上,我通常会使用测试驱动的开发方法编写这种方法)是从两个User的所有属性均相等的情况开始,然后测试每个属性不同时会发生什么。 这给了我们这样的测试套件: func test_equals_allPropertiesMatch_isTrue(){ let sut = User(名称:“”,电子邮件:“”,needsVerification:false) 让其他=用户(名称:“”,电子邮件:“”,需要验证:false) XCTAssertEqual(ut,其他) XCTAssertEqual(other,sut) } func test_equals_nameDiffers_isFalse(){ let sut = User(名称:“ Jo”,电子邮件:“”,需要验证:false) 让其他=用户(名称:“”,电子邮件:“”,需要验证:false) […]

带有TDD的手工iOS应用程序中的身份验证规则

让我们从身份验证过程开始,但并非全部开始,目前,我们仅对注册部分感兴趣。 这部分具有一些验证和重要行为。 此实现将使我们能够与一些BASS内容进行交互,这代表了应用程序难题的重要组成部分。 在这篇文章中,我们将介绍: 测试驱动开发 输入数据验证 准备与外部服务的一些集成 您可以在此处检查相关的github问题。 用户注册用例 该项目将使用自下而上的流程进行开发。 因此,我们将从编写测试和规则开始,而不是进行注册或登录屏幕。 第一个测试即将尝试使用空电子邮件值注册用户。 经过绿色测试之后,该重构用例类了。 第一个奇怪的是寄存器功能处的mutating关键字。 需要使用此关键字是因为该函数试图更改默认情况下不可变的结构值。 此应用程序将使用称为Clean Architecture的体系结构,该体系结构将软件分层。 这些层对于隔离按行为分开的组件和职责至关重要。 清洁体系结构和SOLID原则值得一提,我将在稍后单独讨论。 通过将软件划分为多个层,并遵循“依赖关系规则”,您将创建一个具有内在可测试性的系统,并具有其所隐含的所有优势。 马丁,罗伯特。 “清洁建筑” 2012 同样,此用例正在保存应该在表示层中的状态。 不在“应用程序业务规则”层中。 为了解决这个问题,我们可以创建一个演示者。 …我们也不希望该层受到数据库,UI或任何常用框架等外部性变化的影响。 该层与此类问题无关。 马丁,罗伯特。 “清洁建筑” 2012 上面的协议将成为演示者。 而且它的实现与用例无关紧要。 用例只需要具有故障方法的东西。 在我们的测试环境中,我们将使用演示者的测试双精度表示形式,该表示形式为存根双精度类型。 出于测试目的,我们可以用等效的“特技替身”: Test Double来代替真实的DOC(而不是SUT!)。 迈扎罗斯(Meszaros),杰拉德(Gerard)。 2009年“ Test Double” 存根提供对测试过程中进行的呼叫的固定答复,通常通常根本不响应为测试编程的内容。 存根还可以记录有关呼叫的信息,例如,电子邮件网关存根可以记住“已发送”的消息,或者仅记住“已发送”的消息数量。 福勒,马丁。 “不打Mo”,2007年 用例已更改为通过依赖关系使用演示者。 演示者将用作用例的依赖项,这样做将符合SOLID原则,更具体地讲是Dependency Inversion一个。 原则指出: 答:高级模块不应依赖于低级模块。 两者都应依赖抽象。 B.抽象不应依赖细节。 细节应取决于抽象。 马丁,罗伯特。 […]

iOS中的真实场景单元测试

在敏捷开发环境中,编写单元测试和测试驱动的开发是团队和开发人员最后考虑的事情。 编写单元测试浪费时间,维护麻烦并且会污染生产代码是一个神话。 相反,它使您可以遵循最佳实践,例如依赖项注入,松散耦合的代码,编写自Swift发行以来受到广泛认可的协议。 单元测试对于避免大型团队的项目中的错误非常有帮助,因为新的开发人员加入团队可以进行一些更改,这些更改可能会使某些测试用例和方案失败,因此,如果编写了测试用例,则可以很早地发现它。 它还有助于在编写逻辑或函数时考虑所有可能出现的情况。 单元测试的网络示例: 如果您的测试目标(SUT,被测系统)以某种方式与现实世界相关,例如网络和CoreData,则编写测试代码会更加复杂。 基本上,我们不希望我们的测试代码依赖于现实世界中的事物。 SUT不应依赖于其他复杂系统,因此我们能够更快,时间不变和环境不变地对其进行测试。 此外,重要的是我们的测试代码不要“污染”生产环境。 “污染”是什么意思? 这意味着我们的测试代码将一些测试内容写入数据库,将一些测试数据提交至生产服务器,等等。这就是存在依赖项注入的原因。 给定一个应该在生产环境中通过Internet执行的类。 Internet部分称为该类的依赖项。 如上所述,当我们运行测试时,该类的Internet部分必须能够用模拟或伪造环境代替。 换句话说,该类的依赖关系必须是“可注入的”。 依赖注入使我们的系统更加灵活。 我们可以在生产代码中“注入”真实的网络环境。 同时,我们还可以“注入”模拟网络环境来运行测试代码,而无需访问互联网。 为URLSession创建协议,以便可以将模拟对象注入到HttpClient中。 协议1:URLSessionProtocol提供dataTask api。 协议2:URLSessionDataTaskProtocol提供恢复API。 因此,如您在上面的屏幕截图中所见,测试用例已失败。 XCTAssertNotNil的执行无需等待get API的结果。 幸运的是,我们有XCTest Expectations可以测试异步代码。 该机制允许您指定一个或多个“期望”,这些期望将由于测试中的操作而异步发生 。 一旦设置了所有期望值,就会调用“等待” API,该API将阻止后续测试代码的执行,直到满足所有期望条件或发生超时为止。 运作方式: 设置一个“期望”,告诉Xcode它应该开始等待。 当我们的异步代码返回时,我们通知测试运行器一切正常,不再需要等待。 让Xcode知道失败之前应该等待多长时间。 因此,通过定义期望并在接收到数据时调用满载api(),并通过调用API waitForExpectations并设置计时器,直到Xcode必须等待期望完成,才使Xcode在XCTAssert调用之前等待。

iOS单元测试简介

您最有可能阅读本文,因为您已经听说过有关单元测试的知识,并且想要了解更多。 如果您从未听说过单元测试,并且想要基本介绍,那么您来对地方了。 本文将为您提供单元测试的一般概述,并将讨论单元测试与iOS之间的关系。 什么 如果您考虑一下, 每个复杂的系统都由较小的部分组成。 您的汽车配有发动机,燃油管,油箱,挡风玻璃等。 将这些组件中的每一个组合在一起,就可以构成您的汽车。 您的汽车行驶到任何地方的唯一原因是这些组件中的每个组件都能发挥出色的作用。 单元测试中的“单元”是这些组件之一。 单元测试不仅适用于软件。 它是一个广义的工程术语,可以应用于由零件组成的任何领域,每个领域都需要完成特定的工作。 您可以将汽车拆开并在每个组件上运行诊断程序,以对汽车进行单元测试。 也许您想测试机油滤清器清除某些脏油的能力,或者您想测试气帽上的密封。 每个测试都是一项测试,您可以对组成汽车的各个单元进行测试。 这很有趣。 我已经看到“单元测试”一词在某些我从未期望使用过的领域中得到了应用。 在星际争霸中。 如您所见,这是一个非常广泛的概念。 编写有效的单元测试需要什么 一个好的单元测试需要具备以下条件。 您正在测试的隔离组件。 如果您一起测试一件事以上,那就是集成测试 。 您正在测试的特定行为。 不用说,此行为需要与您正在测试的组件有关。 成功和失败的条件。 毫无疑问。 在单元测试中,没有什么是部分成功的 。 运行时,每个单元测试必须成功或失败。 这些看起来很简单,但是您通常会发现第一部分具有挑战性。 大多数系统并不是以模块化的方式设计的,因此需要花费一些额外的精力才能有效地隔离不同的组件。 挑战:隔离组件 例如,假设您要在汽车的机油滤清器上进行单元测试。 您不能只是弹出过滤器并对其进行测试。 您必须将其卸下,将机油排入锅中,进行测试,将其放回去,然后再将油放回车中。 如此彻底地测试机油滤清器需要花费大量时间和精力,这就是为什么大多数机油滤清器都是一次性产品。 您只需在一定距离后或在怀疑它们会变坏时更换它们。 他们就像灯泡。 花时间尝试修复它们是浪费。 汽车中几乎每个组件都很难进行单元测试。 您几乎永远都不会在该级别上测试汽车的组件。 实际上,您在汽车上进行的大多数测试都是集成测试。 整合测试 集成测试是将一个或多个组件一起测试的任何测试。 当您测试汽车将清洗液输送到挡风玻璃的能力时,您正在执行集成测试。 您可以同时测试清洗液泵,清洗液管线和喷涂机机构。 除非有严重的故障,否则任何理智的人都不会花费时间或精力单独测试这些组件。 将系统分解并重新组合在一起会花费太多时间。 这个类比并没有完全映射到软件领域,但是有一个重要的考虑因素。 设计这样一个易于分解的系统需要花费额外的精力。 无论是汽车,软件还是星际争霸战,您都需要考虑这一成本。 大多数汽车都不容易进行单元测试,因为单元测试不是其设计的核心。 汽车公司不希望您一直保持汽车所有零件的良好维护。 […]

使用Xcode XCTest的TDD iOS网络API调用

测试驱动开发(TDD)是软件开发人员可以在软件开发中使用的方法之一。 在TDD中,开发人员计划要创建的软件功能,然后在编写功能实现之前,为软件的每个功能编写测试用例。 在开始时,测试用例显然会失败,因为代码尚未实现。 此阶段通常称为红色阶段。 然后,开发人员编写代码以确保测试用例成功通过,并且不会破坏任何组件或当前的测试用例,因此不必完全优化和高效地完成此阶段中的代码。 此阶段称为绿色阶段。 此后,开发人员应通过清理,维护代码库和优化代码效率来重构代码的实现。 然后,应在添加新的测试用例时重复此循环。 每个测试单元应做得尽可能小且隔离,以使其易于阅读和维护。 在本文中,我们将使用带有Xcode XCTest Framework的TDD构建一个简单的网络API单元测试。 Network API将对服务器的网络调用封装为以JSON格式获取电影列表,然后将其编码为Movie Swift类的数组。 网络测试需要在不发出实际网络请求的情况下快速执行,为此,我们将创建模拟对象和存根来模拟服务器调用和响应。 APIRepository类:此类封装了我们对服务器的网络请求调用,以获取电影列表 APIRepositoryTests类:XCTest子类,我们将使用该子类为APIRepository类编写测试用例 MockURLSession类:URLSession子类,充当模拟对象以测试传递的URL以及使用存根数据,URLResponse和Error对象创建的MockURLSessionDataTask MockURLSessionDataTask类:URLSessionDataTask子类,充当模拟对象,用于存储网络调用中的存根数据,URLResponse,Error,completionHandler对象,它覆盖恢复调用并调用传递存根对象的completionHandler存根。 测试用例1 —从API获取电影按预期设置URL主机和路径 我们将创建的第一个测试用例是测试get films方法是否在正确的期望范围内设置了URL Host和Path。 首先在“测试”模块内创建APIRepositoryTests单元测试类。 不要忘记添加“ @testable import project-name”以将项目模块包含在测试模块中。 要在Xcode中运行测试,可以使用快捷键Command + U。 在函数中,我们设置实例化APIRepository类对象,此方法中创建了一个名为testGetMoviesWithExpectedURLHostAndPath()的方法,因为我们尚未输入APIRepository类,所以我们键入了有关未解析标识符APIRepository的编译器投诉。 导入 XCTest @testable 导入 APITest 类 APIRepositoryTests:XCTestCase { func testGetMoviesWithExpectedURLHostAndPath(){ 让 apiRespository = APIRepository() } } 要编译测试,请创建一个包含APIRepository类的名为APIRepository.swift的新文件。 导入基础类 APIRepository {} 接下来,在testGetMoviesWithExpectedURLHostAndPath内部,我们将调用APIRepository方法,以从通过完成处理程序的网络中获取电影。 […]

在Xcode 10 Playgrounds中使用3rd party框架

注意:这是上一篇文章的Xcode 10更新,但有一些补充。 Xcode Playgrounds是编写代码原型甚至练习测试驱动开发的好地方。 有时,第三方库可能会方便使用。 在本文中,我将向您展示两种在框架中使用Xcode Playgrounds的方法。 第三方框架不能简单地包含在游乐场中,甚至不能作为“来源”的一部分: 假设我们有一个要在操场上测试的框架-在下面的示例中,我选择了一个著名的库:由多产的开源和作者Marin Todorov编写的SwiftSpinner。 下载的档案包含可以直接打开的Xcode项目。 确保项目构建成功(Cmd + B) 操场 现在,让我们创建要使用的游乐场-从菜单中选择“文件”>“新建”>“游乐场”,确保它是一个iOS游乐场,然后选择“单一视图”模板。 您可以使用默认名称(MyPlayground.playground)并将其存储在框架本身中: 创建游乐场后,切换到“助手编辑器”以查看游乐场输出: 现在,使用该框架非常容易: 将其导入顶部: 让我们看看如何在操场上使用多个框架。 我们将使用相同的SwiftSpinner和Alamofile。 首先,下载两个库并将它们并排存储在文件夹中: 现在,我们将像以前一样打开SwiftSpinner Xcode项目,并围绕它创建一个工作区-从菜单中选择:File> Save as Workspace。 我将其命名为:Playground.workspace,并将其保存在两个文件夹旁边: 添加第二个框架 现在,让我们添加第二个框架—从Alamofire主项目中,将Alamofire.xcodeproj(确保您添加了项目而不是Alamofire.xcworkspace)拖放到SwiftSpinner项目旁边的工作区中。 现在,工作空间应包含来自两个框架的方案。 操场 与上一节一样,我们将创建一个游乐场-从菜单中选择“文件”>“新建”>“游乐场”,确保它是一个iOS游乐场,然后选择“单视图”模板。 确保将其添加到“游乐场”组。 一种统治所有人的方案 现在,在使用框架之前,我们必须一个一个地构建它们。 虽然对于我们而言,这并不难,但是如果使用更多的它们,或者如果您在操场上使用它们时积极开发它们,可能会很麻烦。 为此,我们将添加一个将构建所有框架的新方案。 从方案选择器中,选择“新方案…”: 确保目标是“无” —我已将其命名为“我的游乐场”: 在下一个屏幕的Build类别下,使用小+按钮添加从属目标-这是应如何配置的: 点击“关闭”。 现在,每次您建立Playground目标时,也会建立所有的依存关系。 注意:请确保您要为模拟器而不是真实设备而构建。 使用框架 现在可以像以前一样使用这些框架: 导入它们 快乐的游乐场! 🤓

与Swift编译器配对存根

通常,我们需要对带有副作用的代码进行单元测试,这些副作用通过抽象与Swift中的协议与系统的其他部分进行交互。 有很多方法可以验证它是否按预期工作:打桩,监视,伪造等。总的来说,工程师将这些形式称为模拟 ,即使“ test double”也是合法名称。 仅供参考,“测试双倍”的名称来自电影行业中的“特技双倍”。 在Swift中编写测试双打(例如存根或模拟)是一个无聊的过程,开发人员经常尝试使用Sourcery / SwiftyMocky之类的工具自动生成它。 但是,如果您希望进行更精细的控制,或者不想在构建过程中引入新的步骤,则可以使用一个技巧来快速(至少更快)实现双重测试。 如果您知道存根和模拟的工作原理,则可以轻松地跳过以下两章,跳至下一章或直接跳至解决方案。 首先,让我们区分存根和模拟之间的区别是什么,因为它们非常相似。 根本区别在于验证过程: 对于存根,测试用例必须手动调用一些方法以验证是否发生了预期的副作用 模拟使用预定义的配置自动进行验证 让我们用伪swift代码进行比较: 与CounterStub相反, CounterMock是在初始化期间进行预配置的,在CounterStub中,在验证步骤中,我们需要将一些addCalledTimes属性与3进行显式比较。实现这两个测试对addCalledTimes的方式是实现细节。 一般的经验法则是, 验证可以区分我们是使用存根还是模拟(或混合混合)。 让我们先不进行理论讨论,然后再回到地面。 存根实际上如何在Swift中实现? 我们想要创建一个符合协议的对象,并有机会1)验证是否已使用期望的参数调用了给定的函数,以及2)控制函数返回了值。 有许多方法可以做到这一点,最简单的方法就是引入想要跟踪到存根中的行为一样多的属性。 例如,如果要在给定的时间调用该函数,请添加一个计数器,在函数中对其进行递增,然后在测试后验证其值-如上面的代码段所示。 但是,一种替代方法是添加一个附加变量,该变量提供一个函数占位符 (具有与该函数几乎相同的签名类型),并在遵守协议时调用它。 我写这本书几乎是因为如果测试用例完全希望忽略给定的功能,我们将其设为可选只是为了方便。 在此博客文章中,我们将讨论协议功能,因为变量相对于存根而言相对简单-仅通过引入实例即可。 为了这篇文章的缘故,让我们考虑一下最简单的协议,该协议将用户名同步保存到数据库中,并返回一个布尔值来通知过程是否成功。 它的模拟看起来像这样: 对于函数addUser ,我们引入了一个变量addUserAction -每次调用addUser时addUser对其求值的函数。 然后,在测试场景中,您可以1)验证被测系统(测试代码)确实调用了该函数,并且2)模拟数据库的响应方式(无论返回true还是false ): 一般而言,通过引入与函数参数和返回类型匹配的addUserAction变量,我们让测试用例定义协议相关函数的主体。 顺便说一句,如果您不喜欢addUserAction属性的可选性,则可以继续执行等效的实现: 在上面显示的存根中,我们必须显式提供一种addUserAction变量。 对于短函数来说,这似乎是一件容易的事,但对于涉及多个参数,闭包, (re)throws , @autoclosure ,最终可能会导致您和编译器之间的较量。 为了克服这种麻烦,我们可以利用Swift的类型推断系统。 解决方案涉及一个全局函数,该函数返回与参数类型相同的nil值: 起初,它看起来像一个胡说八道的函数,但我们只会用它来拉出T类型,而不是它的值。 函数是Swift中的一等公民,因此我们可以将函数作为参数传递给asNil以获取Wrapped类型与函数类型匹配的nil实例。 在下面的示例中,我们将变量addUserAction初始化为与协议功能相同类型的nil值。 那正是我们以前必须手动写下的内容: 需要将此变量标记为 lazy 因为引用 addUser 隐式依赖于 […]