探索:测试 Swift 中的 ErrorType

原文链接:Testing Swift’s ErrorType: An Exploration

译者:mmoaay

在本篇中,我们对 Swift 新错误类型的本质进行探究,观察并测试错误处理实现的可能性和限制。最后我们以一个说明样例、以及一些有用的资源结尾

如何实现 ErrorType 协议

如果跳转到 Swift 标准库中 ErrorType 定义的位置,我们就会发现它并没有包含明显的要求。

protocol ErrorType { }

然而,当我们试着去实现 ErrorType 时,很快就会发现为了满足这个协议至少有一些东西是必须的。比如,如果以枚举的方式实现它,一切OK。

enum MyErrorEnum : ErrorType {
}

但是如果以结构体的方式实现它,问题来了。

struct MyErrorStruct : ErrorType {
}

我们最初的想法可能是,也许 ErrorType 是一种特殊类型,编译器以特殊的方式来对它进行支持,而且只能用 Swift 原生的枚举来实现。但随后你又会想起 NSError 也满足这个协议,所以它不可能有那么特殊。所以我们下一步的尝试就是:通过一个 NSObject 的派生类实现这个协议

@objc class MyErrorClass: ErrorType {
}

不幸滴是,仍然不行。

更新:从 Xcode 7 beta 5 版本开始,我们可能不需要花费其他精力就可以为结构体和类实现 ErrorType 协议。所以下面的解决方法也不再需要了,但是仍然留作参考。

允许结构体和类实现 ErrorType 协议。(21867608)

怎么会这样?

通过 LLDB 进一步调查发现这个协议有一些隐藏的要求。

(lldb) type lookup ErrorType
protocol ErrorType {
  var _domain: Swift.String { get }
  var _code: Swift.Int { get }
}

这样一来 NSError 满足这个定义的原因就很明白了:它有这些属性,在 ivars 的支持下,不用动态查找就可以被 Swift 访问。还有一点不明白的是为什么 Swift 的一等公民(first class)枚举可以自动满足这个协议。也许其内部仍然存在一些魔法?

如果我们用我们新获得的知识再去实现结构体和类,一切就OK了。

struct MyErrorStruct : ErrorType {
  let _domain: String
  let _code: Int
}

class MyErrorClass : ErrorType {
  let _domain: String
  let _code: Int

  init(domain: String,code: Int) {
    _domain = domain
    _code = code
  }
}

捕获其他被抛出的错误

历史上,Apple 的框架中的 NSErrorPointer 模式在错误处理中起到了重要作用。在 Objective-C 的 API 与 Swift 完美衔接的情况下,这些已经变得更加简单。确定域的错误会以枚举的方式暴露出来,这样就可以简单滴在不使用“魔法数字“的情况下捕获它们。但是如果你需要捕获一个没有暴露出来的错误,该怎么办呢?

假设我们需要反序列化一个 JSON 串,但是不确定它是不是有效的。我们将使用 FoundationNSJSONSerialization 来做这件事情。当我们传给它一个异常的 JSON 串时,它会抛出一个错误码为 3840 的错误。

当然,你可以用通用的错误来捕获它,然后手动检查 _domain_code 域,但是我们有更优雅的替代方案。

let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
    let object : AnyObject = try
     NSJSONSerialization.JSONObjectWithData(data!,options: [])
    print(object)
} catch let error {
    if   error._domain == NSCocoaErrorDomain
      && error._code   == 3840 {
        print("Invalid format")
    } else {
        throw error
    }
}

另外一个替代方案就是我们引入一个通用的错误结构体,这个结构体通过我们之前发现的方法满足 ErrorType 协议。当我们为它实现模式匹配操作符 ~= 时,我们就可以在 do … catch 分支中使用它。

struct Error : ErrorType {
    let domain: String
    let code: Int

    var _domain: String {
        return domain
    }
    var _code: Int {
        return code
    }
}

func ~=(lhs: Error,rhs: ErrorType) -> Bool {
    return lhs._domain == rhs._domain
        && rhs._code   == rhs._code
}

let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
    let object : AnyObject = try
     NSJSONSerialization.JSONObjectWithData(data!,options: [])
    print(object)
} catch Error(domain: NSCocoaErrorDomain,code: 3840) {
    print("Invalid format")
}

但在当前情况下,还可以用 NSCocoaError,这个辅助类包含大量定义了各种错误的静态方法。

这里所产生的叫做 NSCocoaError.PropertyListReadCorruptError 错误,虽然不是那么明显,但是它确实是有我们需要的错误码的。不管你是通过标准库还是第三方框架捕获错误,如果有像这样的东西,你就需要依赖给定的常数而不是自己再去定义一次。

let json : NSString = "{"
let data = json.dataUsingEncoding(NSUTF8StringEncoding)
do {
    let object : AnyObject = try NSJSONSerialization.JSONObjectWithData(data!,options: [])
    print(object)
} catch NSCocoaErrorDomain {
    print("Invalid format")
}

自定义错误处理的编写规范

所以下一步做什么呢?在用 Swift 的错误处理给我们的代码加料之后,不管我们是替换所有那些让人分心的 NSError 指针赋值,还是退一步到功能范式中的 Result 类型, 我们都需要确保我们所预期的错误会被正确抛出。边界值永远是测试时最有趣的场景,我们想要确认所有的保护措施都是到位的,而且在适当的时候会抛出相应的错误。

现在我们对这个错误类型在底层的工作方式有了一些基本的认识,同时对如何在测试时让它遵循我们的意愿也有了一些想法。所以我们来展示一个小的测试用例:我们有一个银行 App,然后我们想在业务逻辑里面为现实活动建模型。我们创建了代表银行帐号的结构体 Account,它包含一个接口,这个接口暴露了一个方法用来在预算范围内进行交易。

public enum Error : ErrorType {
    case TransactionExceedsFunds
    case NonPositiveTransactionNotAllowed(amount: Int)
}

public struct Account {
    var fund: Int

    public mutating func withdraw(amount: Int) throws {
        guard amount < fund else {
            throw Error.TransactionExceedsFunds
        }
        guard amount > 0 else {
            throw Error.NonPositiveTransactionNotAllowed(amount: amount)
        }
        fund -= amount
    }
}

class AccountTests {
    func testPreventNegativeWithdrawals() {
        var account = Account(fund: 100)
        do {
            try account.withdraw(-10)
            XCTFail("Withdrawal of negative amount succeeded,but was expected to fail.")
        } catch Error.NonPositiveTransactionNotAllowed(let amount) {
            XCTAssertEqual(amount,-10)
        } catch {
            XCTFail("Catched error \"\(error)\",but not the expected: \"\(Error.NonPositiveTransactionNotAllowed)\"")
        }
    }

    func testPreventExceedingTransactions() {
        var account = Account(fund: 100)
        do {
            try account.withdraw(101)
            XCTFail("Withdrawal of amount exceeding funds succeeded,but was expected to fail.")
        } catch Error.TransactionExceedsFunds {
            // 预期结果
        } catch {
            XCTFail("Catched error \"\(error)\",but not the expected: \"\(Error.TransactionExceedsFunds)\"")
        }
    }
}

现在假想我们有更多的方法和更多的错误场景。在以测试为导向的开发方式下,我们想对它们都进行测试,从而保证所有的错误都被正确滴抛出来——我们当然不想把钱转到错误的地方去!理想情况下,我们不想在所有的测试代码中都重复这个 do-catch 。实现一个抽象,我们可以把它放到一个高阶函数中。

/// 为 ErrorType 实现模式匹配
public func ~=(lhs: ErrorType,rhs: ErrorType) -> Bool {
    return lhs._domain == rhs._domain
        && lhs._code   == rhs._code
}

func AssertThrow<R>(expectedError: ErrorType,@autoclosure _ closure: () throws -> R) -> () {
    do {
        try closure()
        XCTFail("Expected error \"\(expectedError)\","
            + "but closure succeeded.")
    } catch expectedError {
        // 预期结果.
    } catch {
        XCTFail("Catched error \"\(error)\","
            + "but not from the expected type "
            + "\"\(expectedError)\".")
    }
}

这段代码可以这样使用:

class AccountTests : XCTestCase { func testPreventExceedingTransactions() { var account = Account(fund: 100) AssertThrow(Error.TransactionExceedsFunds,try account.withdraw(101)) } func testPreventNegativeWithdrawals() { var account = Account(fund: 100) AssertThrow(Error.NonPositiveTransactionNotAllowed(amount: -10),try account.withdraw(-20)) } }

但你可能会发现, 预期出现的参数化错误 NonPositiveTransactionNotAllowed 比这里所用到的参数要多个 amount。我们该如何对错误场景和它们相关的值做出强有力的假设呢? 首先,我们可以为错误类型实现 Equatable 协议,然后在相等操作符的实现中添加对相关场景的参数个数的检查。

/// 对我们的错误类型进行扩展然后实现 `Equatable`。
/// 这必须是对每一个具体的类型来做的,
/// 而不是为 `ErrorType` 统一实现。
extension Error : Equatable {}

/// 为协议 `Equatable` 以 required 的方式实现 `==` 操作符。
public func ==(lhs: Error,rhs: Error) -> Bool {
    switch (lhs,rhs) {
    case (.NonPositiveTransactionNotAllowed(let l),.NonPositiveTransactionNotAllowed(let r)):
        return l == r
    default:
        // 我们需要在默认场景,为各种组合场景返回 false。
        // 通过根据 domain 和 code 进行比较的方式,我们可以保证
        // 一旦我们添加了其他的错误场景,如果这个场景有相应的值
        // 我只需要回到并修改 Equatable 的实现即可
        return lhs._domain == rhs._domain
            && lhs._code   == rhs._code
    }
}

下一步就是让 AssertThrow 知道有合理的错误。你可能会想,我们可以扩展已存在的 AssertThrow 实现,只是简单检查一下预期的错误是否合理。但是不幸滴是根本没用:

“Equatable” 协议只能被当作泛型约束,因为它需要满足 Self 或者关联类型的必要条件

相反,我们可以通过多一个泛型参数做首参的方式重载 AssertThrow

func AssertThrow<R,E where E: ErrorType,E: Equatable>(expectedError: E,@autoclosure _ closure: () throws -> R) -> () {
    do {
        try closure()
        XCTFail("Expected error \"\(expectedError)\","
            + "but closure succeeded.")
    } catch let error as E {
        XCTAssertEqual(error,expectedError,"Catched error is from expected type,"
                + "but not the expected case.")
    } catch {
        XCTFail("Catched error \"\(error)\","
            + "but not the expected error "
            + "\"\(expectedError)\".")
    }
}

然后跟预期一样我们的测试最终返回了失败。

注意后者的断言实现就对错误的类型进行了强有力的假设。

不要使用“捕获其他被抛出的错误”下面的方法,因为跟目前的方法相比,它不能匹配类型。很有可能这种错误超出了我们的控制了。

一些有用的资源

在 Realm,我们使用 XCTest 和我们自产的 XCTestCase 子类并结合一些 预测器,这样刚好可以满足我们的特殊需求。值得高兴的是,如果要使用这些代码,你不需要拷贝-粘帖,也不需要重新造轮子。错误预测器在 GitHub 的 CatchingFire 项目中都有,如果你不是 XCTest 预测器风格的大粉丝,那么你可能会更喜欢类似 Nimble 的测试框架,它们也可以提供测试支持。

要开心滴测试哦~

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。