本文主要是介绍Rust能力养成之(13)测试组建和测试基元,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
在本章中,我们将继续学习Cargo,并学习如何编写测试,如何写开发文档,以及如何用基准测试来度量代码的性能。然后,将综合利用这些技能来构建一个模拟逻辑门的crate,以亲身体验一下如何编写单元测试,集成测试,以及文档测试。
本章内容将涉及:
-
测试动机(Motivation on testing)
-
组织测试和测试原语(Organizing tests and testing primitives)
-
单元测试和集成测试(Unit tests and integration tests)
-
文档测试(Documentation tests)
-
基准测试(Benchmark tests)
-
与Travis CI的持续集成(Continuous integration with Travis CI)
而本篇内容将涉及上面的前两项。
为什么要测试?
虽然基本的原因大家也都了解,这里还是针对性的说一下。一般而言,软件系统就像带有各种零件的机器。如果单个零件发生故障,整个机器就很有可能在不可靠因素的影响下运行,自然出问题的几率也就增加了。在软件中,单个零件可以是函数、模块或所使用的任何库。自然,对软件系统的各个零件组件进行功能测试是维护高质量代码切实有效的方法。固然这并不能保证bug可以被根除,但确实为代码生成后的实际部署提供了可靠依据,而且长期看来,也可以维护并维持住代码的效果和稳健性。
此外,如果没有单元测试(unit test),大规模软件的重构也是很难做到的。在软件开发中,灵活和适当的使用单元测试具有良好效果:在代码实现阶段,编写良好的单元测试已经成为软件组件中的不成文规矩。在维护阶段,现有的单元测试作为对代码基中的回归控制,可以促进即时修复。在像Rust这样的编译语言中,由于编译器提供了有效细致的错误诊断,单元测试回归以及可能所涉及的重构于是有着非常详实的指导信息,不得不说,这一点当真比起其他同类型语言颇具优势。
单元测试的另一个不错的追加影响是,其鼓励程序员编写主要依赖于输入参数的模块化代码,即无状态函数(stateless function),这就使程序员尽量不去写依赖于全局可变状态的代码,因为编写依赖于全局可变状态的测试是很难的,再者,仅仅考虑为一段代码编写测试的行为可以很快帮助程序员找出其代码实现中的错误。最后,对于想要理解代码库的不同部分如何交互的新手来说,也可以作为非常贴近实际的文档。
组织测试(Organizing tests)
通常,在开发软件时会编写两种测试:单元测试(unit test)和集成测试(integration test),各自服务于不同的目的,并以不同的方式与被测试的代码库进行交互。单元测试总是轻量级的,测试单个组件,以便开发人员可以经常运行受测内容,从而提供较短和快捷的反馈循环路径。相比而言,集成测试是重型的,旨在模拟真实场景,会根据其环境和规范做出断言性质的判定。Rust的内置测试框架为开发者编写和组织这些测试提供了常规的默认设置,如下所示:
-
单元测试:通常写在首测代码的同一个模块中。当这些测试数量增加时,会被组织到一个实体中作为嵌套模块。一般在当前模块中创建一个子模块,按照惯例,将其命名为tests,并在上面加上#[cfg(test)]属性的注释,再将所有与测试相关的函数放在其中。此属性只是告诉编译器包含在测试模块中的代码,但条件仅仅是在运行cargo测试时。稍后会再详细介绍相关属性。
-
集成测试:是在cargo root的“tests/目录”中单独编写,写起来就如同是在真实的使用相关代码。在test /目录中的任何.rs文件都可以添加use声明,以引入需要测试的任何公共API。
测试基元(Testing primitives)
Rust的内置测试框架基于一组主要由属性(attributes)和宏(macro)组成的基本元素,在我们编写任何实际的测试之前,需要熟悉一下相关的用法。
属性(Attributes)
Rust语言中的属性,其实就是对某一个条目(item)的注释信息。所谓的这些条目,在结构上被称为顶层语言结构(top-level language construct),比如函数(functions),模块(modules),结构体(structs),枚举(enums),常数声明(constant declarations),以及其他任何需要在crate的根目录进行定义的对象。属性用来指示编译器,来为出现在它们下面的条目,添加额外的代码或含义,如果适用于某个模块,则为模块添加额外的代码或含义。有关这方面的内容,在后续篇章中还会讨论。这里,我们先介绍两种属性。
-
#[<name>]:该属性适用于该名称所涉及的各个部分,通常出现在其定义之上。例如,Rust中的测试函数使用#[test]属性进行注释,表示该函数将被视为测试工具的一部分。
-
#![<name>]:加了一个叹号,代表该属性适用于整个软件包(whole crate),通常放在crate 根目录的最顶层位置。
在模块中编写测试时,还可以使用其他形式的属性,如#[cfg(test)]。此属性添加在测试模块顶层,以提示编译器根据相应条件去编译模块,但者仅仅是当代码在测试模式下进行编译之时。显然,属性并不仅仅在测试代码中使用;而是在Rust中广泛存在,在接下来的章节中还会看到很多。
断言宏(Assertion macros)
在测试中,当给定一个测试用例时,我们会试图断言(assert)或推断一下软件组件在给定输入范围内的可能行为。编程语言通常会提供断言函数(assertion function)来执行上述所提及的这些断言,而Rust为我们提供了以宏来实现的断言函数,来帮助我们实现同样的目的。我们来看看一些常用的语句:
assert!(true);
assert!(a == b, "{} was not equal to {}", a, b);
-
assert!: 这是最简单的断言宏,其接受一个布尔值来进行断言。如果该值为false,test panic 方面会显示哪里出现了问题。当然,断言宏还可以接收格式字符串,后跟相应数量的变量,以提供可定制的错误消息,如下所示:
let a = 23;
let b = 87;
assert_eq!(a, b, "{} and {} are not equal", a, b);
-
assert_eq!: 该宏接受两个值,如果不相等,则会显示失败信息,同时也可以接受定制性错误消息的格式字符串
-
assert_ne!:该宏与assert_eq!类似,不同处在于用来断言两个值是否不相等
-
debug_assert!:该宏与assert!类似,不止用于测试,大多用于断言在代码运行阶段的不变或规则性内容。而这些断言仅在调试构建时有效,在调试模式下运行时,可以帮助捕获违反断言的情况。当代码以优化模式编译时,这些宏调用完全被忽略,并被优化为无操作。类似的,还有debug_assert_eq!和debug_assert_ne !,其工作方式与assert!宏同属一类。
为了比较这些断言宏内的值,Rust会使用特性/特征(trait)。例如,assert!(a == b)实际上是一个方法调用,a.eq(&b),而后返回一个bool值,而eq方法来自 PartialEq特性 。绝大多数Rust的内建类型都会执行PartialEq 和Eq特性 来进行比较,而这两个特性的区别,我们还要等到下一章来讲。
但是,对于用户定义的类型,我们需要实现这些特性。幸运的是,Rust为我们提供了一个名为derive的宏,非常方便,可以接收一个或多个特性来进行实现,该宏可以通过在任何用户定义的类型上添加#[derive(Eq, PartialEq)]这样的注释就能使用。注意括号内的特性质名称,很熟悉吧。derive是一个过程宏,只是为有其出现的类型的impl块生成代码,并实现trait的方法或相关的函数。而关于各种宏,会在后续专门涉及宏的章节中为大家介绍。
结语
至此,终于可以要写一些测试了,那么我们从下一章开始。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
1.Rust编程之道,2019, 张汉东
2.The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
3.Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
4.Beginning Rust ,2018,Carlo Milanesi
5.Rust Cookbook,2017,Vigneshwer Dhinakaran
这篇关于Rust能力养成之(13)测试组建和测试基元的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!