在有关RSpec基础知识的最后一篇文章中,我们介绍了可以避免的一些棘手问题,如何组成测试,为什么应尽可能避免使用数据库以及如何加快测试套件的速度。
主题
- 测试速度
- 数据库瓶颈
- 弹簧预紧器
- Iffy RSpec的便利
- 神秘客人
- 内联代码
- 提取方法
现在您已经掌握了基础知识,我们应该花时间讨论RSpec和TDD的一些棘手的部分-一些很容易被过度使用的问题,以及使用RSpec的DSL的某些部分的缺点。 我想避免在刚孵出的TDD大脑中塞满许多高级概念,但我认为在进行第一次测试大礼包之前需要指出几点。 此外,由于容易避免的不良习惯而导致创建缓慢的测试套件是您可以立即改进的新手知识。
当然,您需要做很多事情才能获得更多经验,然后才能感到舒适和有效地进行测试,但是我敢打赌,如果您采取一些可以改善您的最佳实践的最佳做法,那么从一开始您也会感到更好。规格多种多样,而无需过多地扩展您的技能。 这也是进入更高级概念的一个小窗口,您需要逐渐了解这些概念才能进行“主”测试。 我觉得一开始我不应该太打扰您,因为在您将所有事物巧妙地联系在一起之前,它可能会让您感到困惑和困惑。
测试速度
让我们从速度开始。 快速套间绝非偶然。 这是“持续”维护的问题。 经常聆听您的测试非常重要-至少在您使用TDD且已经喝了一段时间Kool-Aid的情况下-快速的测试套件使您更合理地关注测试的指导位置您。
测试速度是您应该注意的事情。 养成定期养成良好习惯并保持有趣的习惯至关重要。 您希望能够快速运行测试,以便在开发过程中获得快速反馈。 执行测试套件所需的时间越长,越有可能越跳过越多的测试,直到您只想在最后发布它才发布新功能。
乍一看听起来并不那么糟糕,但这不是一个小问题。 测试套件的主要优点之一是可以指导您的应用程序设计-对我而言,这可能是TDD的最大赢家。 较长的测试运行几乎不可能使这部分工作,因为您很可能不会为了避免中断流程而运行它们。 快速测试可确保您没有理由不运行测试。
您可以将此过程视为您和测试套件之间的对话。 如果对话太慢,继续下去真的很痛苦。 当您的代码编辑器提供了还可以运行测试的可能性时,您绝对应该使用此功能。 这将大大提高速度并改善您的工作流程。 每次在编辑器和外壳程序之间切换以运行测试变得非常快。 但是由于这些文章是针对新手程序员的,所以我不希望您立即设置这样的工具。 您还有其他方法可以改善此过程,而无需立即修改您的编辑器。 不过,很高兴知道这一点,我建议您将此类工具纳入您的工作流程。
另外,请注意,您已经学会了如何分割测试,并且不需要一直运行完整的测试套件。 您可以轻松地运行单个文件,甚至单it
块,所有有能力的代码编辑器中,而无需离开它的终端。 例如,您可以将测试集中在被测线上。 坦率地说,这就像魔术一样,它永远不会感到无聊。
数据库瓶颈
向数据库中写入过多消息(通常是不必要的)是一种可以快速显着降低测试套件速度的可靠方法。 在许多测试方案中,您可以伪造设置测试所需的数据,而只关注直接在测试中的数据。 您不需要经常在所有时间内访问数据库,尤其是不需要直接受测试且仅以某种方式支持测试的部件:在测试付款额时的登录用户例如,结帐。 用户就像是可以伪造的额外产品。
您应该尝试避免尽可能不击中数据库,因为这会占用缓慢测试套件的很大一部分。 另外,如果根本不需要数据,请尽量不要设置太多。 特别是在集成测试中,这很容易忘记。 从定义上讲,单元测试通常更加集中。 在避免随着时间的推移降低测试套件的速度方面,该策略将非常有效。 仔细选择依赖项,然后查看使测试通过的最少数据量。
我现在不想再谈任何细节了,谈论存根,间谍,假货和东西可能还为时过早。 在这里将您与这样的高级概念混淆似乎会适得其反,并且您很快就会遇到这些。 有许多用于快速测试的策略,其中还涉及RSpec之外的其他工具。 现在,尝试使用RSpec和一般的测试来笼罩整个全局。
您还希望尽可能只对所有内容进行一次测试。 不要一遍又一遍地重新测试同一件事,那只是浪费。 这主要是由于意外和/或错误的设计决策而发生的。 如果您开始进行的测试很慢,那么这是一个容易重构的地方,可以提高速度。
您的大多数测试也应该在单元级别上,以测试您的模型。 这不仅可以使事情Swift进行,还可以为您带来最大的收益。 测试整个工作流程的集成测试(通过将一堆组件组合在一起并进行同步测试来在某种程度上模仿用户的行为)应该是测试金字塔的最小部分。 这些都是相当缓慢且“昂贵”的。 也许有10%的整体测试并非不切实际,但是我想这取决于。
尽可能少地行使数据库可能会很困难,因为您需要学习更多的工具和技术来有效地实现这一目标,但是,必须以相当快的速度增长测试套件,而这种套件必须足够快才能真正地频繁运行测试。
弹簧预紧器
Spring服务器是Rails的功能,可以预加载您的应用程序。 这是另一种直接提高测试速度的简单策略,我应该立即添加。 它的作用只是使您的应用程序在后台运行,而无需在每次测试运行时都将其引导。 Rake任务和迁移也是如此。 这些也将运行得更快。
从Rails 4.1开始,Spring已包含在Rails中(已自动添加到Gemfile中),并且您无需做很多事情即可启动或停止此预加载器。 过去,我们不得不为此选择自己的工具,当然,如果您有其他偏好,仍然可以这样做。 真正好和体贴的是,如果您更改了一些gem,初始化程序或配置文件,它将自动重新启动,这是一种非常方便的操作,因为您很容易忘记自己照顾它。
默认情况下,它配置为仅运行rails
和rake
命令。 因此,我们需要将其设置为也可以与rspec
命令一起运行以运行测试。 您可以这样询问spring的状态:
终奌站
spring status
输出量
Spring is not running.
由于输出告诉我们Spring没有运行,因此您只需使用spring server.
启动它即可spring server.
现在运行spring status
,应该看到类似以下内容:
输出量
Spring is running:
3738 spring server | rspec-dummy | started 21 secs ago
现在,我们应该检查将Spring设置为预加载的方式。
终奌站
spring binstub --all
输出量
* bin/rake: spring already present
* bin/rails: spring already present
这告诉我们Spring正在为rake
和rails
命令预加载Rails,到目前为止没有其他事情了。 我们需要照顾。 我们需要添加gem spring-commands-rspec
,然后可以预加载我们的测试。
宝石文件
gem 'spring-commands-rspec', group: :development
终奌站
bundle install
bundle exec spring binstub rspec
我将bundle install
的输出留给您使用; 我敢肯定,您所看到的不仅仅是您应得的。 另一方面,运行bundle exec spring binstub rspec
生成一个bin/rspec
文件,该文件基本上将其添加到要由Spring预加载的文件中。 让我们看看这是否有效:
终奌站
spring binstub --all
这创建了一个叫做binstub的东西-诸如rails
,rake, bundle
, rspec
类的可执行文件的包装-因此,当您使用rspec
命令时,它将使用Spring。 顺便说一句,此类binstub可确保您在正确的环境中运行这些可执行文件。 它们还使您可以从应用程序中的每个目录运行这些命令,而不仅仅是从根目录运行。 binstubs的另一个优点是您不必在所有内容之前都添加bundle exec
。
输出量
* bin/rake: spring already present
* bin/rspec: spring already present
* bin/rails: spring already present
看起来不错! 在继续之前,让我们停止并重新启动Spring服务器:
终奌站
spring stop
spring server
因此,现在您在一个专用的终端窗口中运行spring服务器,并在另一个窗口中以稍微不同的语法运行测试。 我们只需要在每个测试运行之前添加spring
命令:
终奌站
spring rspec spec
当然,这将运行您的所有规格文件。 但是没有必要停在那里。 您还可以通过Spring运行单个文件或标记的测试-没问题! 他们现在都将很快闪电; 在较小的测试套件中,它们似乎几乎是瞬时的。 最重要的是,您可以对rails
和rake
命令使用相同的语法。 很好,是吗?
终奌站
spring rake
spring rails g model BondGirl name:string
spring rake db:migrate
...
因此,我们可以立即使用Spring来加快Rails的运行速度,但是我们一定不要忘记添加这个小Gem,让Spring知道如何使用RSpec玩球。
Iffy RSpec的便利
只要您能找到其他解决方案,就可以避免本节中提到的事情。 过度使用RSpec的一些便利可能会导致养成不良的测试习惯,至少会造成一些不良习惯。 我们将在这里讨论的内容表面上很方便,但稍后可能会咬住您。
不应将它们视为“反模式”(应避免的事情),而应将其视为“气味”,这是您应注意的事项,可能会带来您通常不希望支付的高昂费用。 这样做的原因涉及更多的想法和概念,您可能是初学者最不熟悉的,但坦率地说,此时您可能已经有点头疼了,但我至少应该把一些现在要考虑并提交内存的危险信号。
-
let
首先,拥有很多let
引用似乎非常方便-尤其是因为它们使事情干了很多。 首先,将它们放在文件顶部似乎是一个不错的提取。 另一方面,如果您在以后花费大量时间访问特定测试,则它们很容易使您难以理解自己的代码。 在let
块中没有设置数据不会过多地帮助您理解测试。 这并不是一开始听起来那么琐碎,特别是在涉及其他需要阅读您的作品的开发人员的情况下。
随着开发人员的参与,这种混乱变得更加昂贵。 如果您不得不一遍又一遍地寻找let
引用,这不仅很耗时,而且也是愚蠢的,因为可以通过很少的努力避免它。 清晰为王,这毫无疑问。 保持此数据内联的另一个理由是您的测试套件将不那么脆弱。 您不想建立一堆纸牌屋,而每一次let
每个测试都隐藏细节的卡片都会变得更加不稳定。 您可能已经了解到,使用全局变量不是一个好主意。 从这个意义上讲, let
在规范文件中是半全局的。
另一个问题是,对于相似的场景,您将需要测试很多不同的变体,不同的状态。 您很快就会用不上合理命名的let
语句来覆盖您可能需要的所有不同版本,或者最终会成堆成堆的类似名称的状态变量。 当您直接在每个测试中设置数据时,就不会出现此问题。 局部变量便宜,可读性强,并且不会与其他范围混淆。 实际上,它们可以更具表现力,因为您无需考虑成千上万的其他测试,这些测试可能会对特定名称造成问题。 您要避免在每个人都需要为使用let
每个测试解密的框架之上创建另一个DSL。 我希望这感觉像是在浪费每个人的时间。
-
before
和after
before
特殊场合保存诸如before
, after
及其变体之类的东西,不要在所有地方一直使用它。 将其视为获取元数据的大手枪之一。 清理数据是一个很好的例子,对于每个单独的测试来说,它都太元了。 您当然要提取它。
神秘客人
通常,您将let
内容放在文件的顶部,并将这些详细信息与使用它们的整个文件隔离开来。 您希望使相关信息和数据尽可能靠近实际进行测试的部分,而不是千里之外,这会使各个测试变得更加晦涩难懂。
最后,感觉就像挂了太多绳子,因为let
引入了广泛共享的装置。 这基本上可以分解为范围不够严格的虚拟测试数据。
这很容易导致一种主要的气味,称为“神秘客人”。 这意味着您的测试数据无处不在或者只是被假定。 您通常需要首先寻找它们才能理解测试-尤其是自编写代码以来已经过去了一段时间,或者如果您不熟悉代码库,则尤其如此。 在特定测试的设置中而不是在更广泛的范围内,准确地在需要的位置内联定义测试数据会更加有效。
代理商规格
...
...
...
describe Agent, '#print_favorite_gadget' do
it 'prints out the agents name, rank and favorite gadget' do
expect(agent.print_favorite_gadget).to eq('Commander Bond has a thing for Aston Martins')
end
end
当您查看此内容时,它读起来很不错,对吧? 它简洁,单线,很干净,不是吗? 让我们不要自欺欺人。 该测试不会告诉我们太多有关agent
的信息,也不能告诉我们整个故事。 实现细节很重要,但是我们没有看到任何细节。 该代理似乎是在其他地方创建的,为了完全了解此处发生的情况,我们必须先对其进行查找。 因此,表面上看起来可能很优雅,但价格昂贵。
是的,在这方面,您的测试可能不会一直都是超级干燥的,但是我认为这是付出更多代价才能表现得更强,更易于理解。 当然有例外,但是在您穷尽了Ruby纯正提供的选项之后,确实应该将它们仅应用于特殊情况。
对于一个神秘的客人,您必须找出数据来自何处,为何重要以及其具体细节。 在特定的测试本身中看不到实现细节,只会使您的生活变得比实际需要更加艰苦。 我的意思是,如果您在自己的项目上工作,您会感觉如何,但是当涉及其他开发人员时,最好考虑使他们的代码使用体验尽可能流畅。
当然,与许多事情一样,关键在于细节,而您不想让自己和他人对这些东西一无所知。 可读性,简洁性和let
的便利性不应以失去实现细节和误导为代价。 您希望每个测试都能讲述整个故事,并提供所有上下文以立即理解它。
内联代码
长话短说,您希望基于逐个测试的基础上具有易于阅读且易于推理的测试。 尝试指定实际测试中需要的所有内容,并且不要超过此数量。 就像其他垃圾一样,这种废物开始“闻”。 这也意味着您应该尽可能迟地添加特定测试所需的详细信息,即在实际场景中而不是远程的地方整体创建测试数据时。 建议使用let
提供了一种与该想法相反的便利。
让我们再来看前面的示例,并在没有神秘来宾问题的情况下实现它。 在下面的解决方案中,我们将找到测试内联的所有相关信息。 如果此规范失败了,我们不需要做任何事情,也不需要在其他地方查找其他信息。
代理商规格
...
...
...
describe Agent, '#print_favorite_gadget' do
it 'prints out the agents name, rank and favorite gadget' do
agent = Agent.new(name: 'James Bond', rank: 'Commander', favorite_gadget: 'Aston Martin')
expect(agent.print_favorite_gadget).to eq('Commander Bond has a thing for Aston Martins')
end
end
如果let
您设置可以在每个特定测试中需要了解的基础上进行增强的准系统测试数据,那将是很好的选择,但是这不是let
滚动的方式。 这就是我们最近通过Factory Girl使用工厂的方式。
我将为您保留细节,尤其是因为我已经写了一些有关它的文章。 如果您已经对此感到好奇,这是我的新手量身定制的文章101和201,其中介绍了Factory Girl必须提供的功能。 它也是为没有大量经验的开发人员编写的。
让我们看另一个简单的示例,该示例充分利用了内联设置的支持测试数据:
代理商规格
describe Agent, '#current_mission' do
it 'prints out the agent’s current mission status and its objective' do
mission_octopussy = Mission.new(name: 'Octopussy', objective: 'Stop bad white dude')
bond = Agent.new(name: 'James Bond', status: 'Undercover operation', section: '00', licence_to_kill: true)
bond.missions << mission_octopussy
expect(bond.current_mission).to eq ('Agent Bond is currently engaged in an undercover operation for mission Octopussy which aims to stop bad white dude')
end
end
如您所见,我们将测试所需的所有信息集中在一个地方,而无需在其他地方查找任何细节。 它讲述了一个故事,但并不晦涩。 如前所述,这不是DRY代码的最佳策略。 不过,回报是好的。 清晰和可读性远胜于这些重复的代码,尤其是在大型代码库中。
例如,假设您编写了一些看似无关的新功能,但突然间该测试由于附带损坏而开始失败,而且您很久没有接触过此规范文件了。
如果您需要先解密设置组件以了解并解决此失败的测试,然后再继续使用完全不同的功能,您是否会感到高兴? 我觉得不是! 您希望尽快摆脱这种“无关”的规范,然后回到完成另一项功能。
当您在所有可以告诉您失败之处的地方找到所有测试数据时,就可以通过Swift解决此问题而增加机会,而无需将应用程序的完全不同的部分“下载”到大脑中。
提取方法
通过编写自己的帮助程序方法,可以显着地清理和干燥代码。 无需使用RSpec DSL就可以像Ruby方法一样便宜。
假设您发现了一些重复的固定装置,开始感到有点脏。 代替与去的let
或一个subject
,在的底部限定的方法描述块的公约和提取共性进去。 如果在文件中使用更广泛,则也可以将其放在文件的底部。
一个好的副作用是您不会以这种方式处理任何半全局变量。 如果需要稍微调整数据,还可以避免在整个地方进行大量更改。 现在,您可以转到定义该方法的中心位置,并立即影响该方法使用的所有位置。 不错!
代理商规格
describe Agent, '#current_status' do
it 'speculates about the agent’s choice of destination if status is vacation' do
bond = Agent.new(name: 'James Bond', status: 'On vacation', section: '00', licence_to_kill: true)
expect(bond.current_status).to eq ('Commander Bond is on vacation, probably in the Bahamas')
end
it 'speculates about the quartermaster’s choice of destination if status is vacation' do
q = Agent.new(name: 'Q', status: 'On vacation', section: '00', licence_to_kill: true)
expect(q.current_status).to eq ('The quartermaster is on vacation, probably at DEF CON')
end
end
如您所见,有一些重复的设置代码,我们希望避免一遍又一遍地编写它。 相反,我们只想了解此测试的要点,并有一种方法可以为我们构建对象的其余部分。
代理商规格
describe Agent, '#current_status' do
it 'speculates about the agent’s choice of destination if status is vacation' do
bond = build_agent_on_vacation('James Bond', 'On vacation')
expect(bond.current_status).to eq ('Commander Bond is on vacation, probably in the Bahamas')
end
it 'speculates about the quartermaster’s choice of destination if status is vacation' do
q = build_agent_on_vacation('Q', 'On Vacation')
expect(q.current_status).to eq ('The quartermaster is on vacation, probably at DEF CON')
end
def build_agent_on_vacation(name, status)
Agent.new(name: name, status: status, section: '00', licence_to_kill: true)
end
end
现在,我们提取的方法将处理section
和licence_to_kill
内容,因此不会分散我们对测试本质的关注。 当然,这是一个虚拟的示例,但是您可以根据需要扩展其复杂性。 策略不会改变。 这是一种非常简单的重构技术,这就是为什么我这么早引入它,但是它是最有效的一种。 而且,它几乎可以避免RSpecs提供的提取工具。
您还应该注意的是,这些辅助方法在不付出任何额外代价的情况下表现力如何。
最后的想法
避免使用RSpec DSL的某些部分,并充分利用良好的Ruby和面向对象编程原则,是编写测试方法的好方法。 当然,您可以自由使用要点, describe
, context
及其it
。
找到充分的理由使用RSpec的其他部分,并尽可能避免使用它们。 仅仅因为看起来似乎方便和花哨并不足以使用它们-最好使事情更简单。
简单就是好; 它可以使您的测试保持健康,快速。
翻译自: https://code.tutsplus.com/articles/rspec-testing-for-beginners-03--cms-26728