TDD的测试


red -> green -> refactor

1. 先写测试
2. 最简的代码让测试变绿
3. 清理和重构你的代码, 减少重复的代码,如果你不清理你的代码, 未来就会越来越难维护.(这里补充一点是,你的项目会越来越大,如果你项目很小,那也无所谓了.)

当你开始使用TDD的时候, 从哪开始?

从上层功能开始测试交互, 然后 drill down to the nitty gritty?
从底层功能逐步测试到最终的feature?
The answer to this depends, and will vary person-to-person and feature-to-feature.
这个问题的答案取决于个人和特征。
  1. 如果是outside-in , 那第一个测试就是用户能打开的页面的测试. 属于acceptable test, 这需要你对问题理解,设计完成真个功能,最后再来测试.
  2. Inside-Out开发, 你不不知道最后的解决方案.通过一步一步的向前推进,帮助你构建一个又一个的代码组件.每一个步都是一块拼图. 构建好坚固的底层组件后, 你可以改变方向去构建高层组件.

OK, 现在是什么,你写了测试了,那这是不是就是测试驱动呢? NO, 这只是测试先行...


同时你要按照red => green => refactor的这个循环. 只针对test failure的错误信息来写代码.这将会保证你不会过度设计的解决方案或者implement 没有测试的 功能features (实现哪些没有测试的功能)

同样重要的是refactor, 这一步是最重要的一部分,保证你的代码可维护,未来容易修改.

测试的好处:自信/节约时间/Flow/提升设计/

  1. 1. This confidence gives you power to quickly and easily change your code without fear of it breaking.
  2. 2. 刚开始的写测试的时间投资,会节约你在项目增长后期的调试时间.测试的错误信息会给你指引修改的方向,省了你思考,而由测试结果告诉你.
  3. 3.达到一种flow的开发体验,测试的错误指导你去做后续的工作,让编程更加自动化.
  4. 4.TDD能否提升设计?reduce this coupling, making your code easier to test.减少耦合,让测试更容易.TDD helps you recognize coupling up front.解耦才能够复用代码.

务实一点,这个TDD不适应所有场景,TDD也不是必须的,没有TDD,你也能做好,那就行,以结果为导向.

  1. 1.功能你控制不了,或者要做些尝试.如果是做一些尝试,那么可以在之后写写预期会错误的test.
  2. 2.项目小而且不太可能会变.那就可以手动测 test by hand efficiently, you may elect to forgo testing先放弃测试.
  3. 3. 项目临时用.很快被丢弃,不太可能regression testing回归测试. 不测试...
  4. 4.程序异常不要紧,程序不重要,不值得测

什么是Effective测试suit, what are the characteristics?

Fast/Complete/Reliable/Isolated/Maintainable/Expressive/
1. 越快你越运行的越多,变了就运行,越快你的产品就越出来.如果测试运行的慢,等的时间去泡茶刷手机去了,那你就不想运行测试了.
2. 所有公众可以访问的代码,都需要测试覆盖,否则你就没有信心去做修改.
3.测试本身要可靠,测试本身的代码出现间歇性问题,那就很难去diagnose.
4.独立,可以只运行某个testing.避免浪费时间,测试也不能留下测试数据,必须清理data or global state,每次测试都是全新的.
5.写新测试和改测试都必须要容易,否则你就不会愿意去写,也会变低效.你可以用同样的面向对象的设计原则去保持你的代码可维护.
6.Tests本身就是文档,要与时俱进,容易读,当做文档来维护. 在refactor阶段,也要确保移除重复代码,抽象出有用的结构来保持test code简洁.

RSpec起航

选一个框架,只是选择一种代码的写法.
The gem is called Rsepc, because the tests read like specification. 描述了软件做什么,界面会怎么样.
基于这些理念和实践,你可以用别的框架.

Rails安装
rails new 的时候 加上flag:  -T 避免安装minitest,如果安装了, 删除项目下的/test也行.
Gemfile里添加rspec-rails,放在 在development和test组里面.
bundle install
生成RSpec files:
rails generate rspec:install
会创建: .rspec ,  spec/spec_helper.rb, spec/rails_helper.rb. 非常简单.
.rspec  是入口文件
spec/spec_helper.rb 每次都会运行这个文件,不是必须的不要加进去,
spec/rails_helper.rb 这个是加载rails和一些依赖. 需要rails的文件,都需要 require 这个file explicitly.明确的require.

测试的几种类型和分类-Testing Pyramid
1.单元测试 随着a spectrum 落下来的有多种测试类型. 有一个是单元测试. 单元测试是独立的小的,也运行很快的. 每个组件都需要和别的组件进行交互, 如果你测试里面写的是except a function, but in fact it has completely dirrerent one. 那测试全通过,软件还是个破烂货.
2.integration tests.  全靠单元测试是不行的, 集成测试来了.它把系统看着是一个整体,而不是单个模块. 这和用户在系统里面做某一件任务几乎一样. integration test 不关心调用方法和协调,它就是想一个用户一样点击和输入. integration test虽然有效,但是的缺点是易错\运行的相对久.

所以中庸之道就是, 不纯是单元测试或者集成测试, 往往几个组件合在一起测试,而不是测整个系统.  Combination 组合.
几个高层的测试覆盖 general functionality.
几个中层的测试覆盖 子系统的相关细节.
很多的单元测试覆盖 每个组件的细节.

这种方法可以plays to the strength of each type of test 发挥优势, minimize the downsides they have(such as slow run times)降低劣势.
pyramid test


Feature Specs 功能规格测试
如果你是outside-in的测试模式,那就是从功能测试开始.
比如我们的第一个功能是运行用户创建一个link post. 要做到这个,他们必须在home page上点一个link, fill in the title and URL of the link.然后click "SUbmit". 我们会测试,一旦他做了这些后,他会进入到一个新页面看到title是他们输入的,link也是他们提供的. 比如:
As a user
when I visit the home page
And I click "Submit a link post"
And I fill in my title and URL
And I click "Submit"
THen I should see the title on the page
And it shoud link to the given URL

有一点你要注意,我们开始描述的  who 就是终端用户, 我们的系统只有一种角色, 用user是安全的. 有很多应用程序需要多个, 未认证的用户,管理员,指定的角色如教练运动员等.

Capybara  是为了和浏览器交互的工具, Capybara通过api和浏览器交互,提供方法访问pages,填forms,click buttons and More...
bundle add 'capybara'
require it from your rails_helper
require "capybara/rails"

安装capybara后,我们继续把这个功能测试写完:
# spec/features/user_submits_a_link_spec.rb
require "rails_helper"
RSpec.feature "User submit a link" do
  scenario "they see the page for the submitted link" do
    link_title ="This Testing Rails book is awesome!"
    link_url = "http://testingrailsbook.com"

    visit root__path
    click_on "Submit a new link"
    fill_in "link_title", with: link_title
    fill_in "link_url", with: link_url
    click_on "Submit!"

    expect(page).to have_link link_title, herf: link_url
  end
end

.feature 是Capybara提供的一个方法

调用
这个方法可以让你进入capybara的和page交互的方法, feature是全局的上下文.
feature takes a string, 用来描述你的feature. 可以和文件名字一样. 像文档的格式.

scenario "" do
end
这是一个单独的specification. 描述一个可能的创建方式.  scenario的描述 要和 feature的有关联性, 当他们一起读的时候,应该像是一句话, 这样运行spec的时候,读起来well.

看里面的代码很直接: visit  方法是Capybara的. root_path 是你的application的. 为什么是root_path, 这是rails的约定 convention.

click_on and fill_in 也来自 capybara,  fill_in 会find a method by it's name, id or label and fills it in  with the given text. 如果是用id, 要在前面加#.
我们知道 rails给所有的fields定义ids,通过model的名字,我们先不定义这些id, 让rails发挥他的优势.We know that Rails gives ids to all fields by joining the model name and the field name. As long as we don’t customize those, we can use them to our advantage.


expect(page).to have_link link_title, href: link_url
这里有些 "magic" RSpec也是ruby code.
expct(page).to(have_link(link_title,  { href: link_url })

#expect 是一个RSpec的方法, 构建一个assertion断言.他有个参数 page , page从哪来的呢? Capybara提供的进入当前page.
运行这个assertion, 调用 #to返回的值 通过matcher来判断,返回 truthy or falsy.
这里的matcher 就是  have_link, have_link 来自Capybara.

魔幻的地方在这里: 其实没有 have_link?方法.
RSpec定义了  has_ 开头  ? 结尾的方法.  而Capybara定义了Page的 has_link?方法.  如果我写成了  page.has_link?  那要怎么搞?
RSpec will automatically look for the method #has_link? when it sees #have_link .

这里有点复杂,如果你写大量的测试,熟悉起来后,这种就会好点.

运行测试
rspec   path/your_spec.rb
 
打印的结果, is called documentation . 运行一个结果文档展现的比较好看,但是如果运行很多这样显示就是cumbersome 大而笨重.

This section outlines all of the failures. outlines 概括了.

Randomized with seed 5573
结果的最后会显示这么一句.我们运行我们的specs in a random order to help diagnose specs that may not clean up after themselves properly.

每次只修改一次提示的错误,然后再次运行, 不多写代码.好处是所有的代码都被测试了. 因为测的非常多,所有需要测试运行的很快才行.

随着你越来越熟悉, 就不需要一点代码就去跑测试. ,你可能可以预测到结果会是什么, 就直接写代码pass了test, 这也能节约不少时间.

无论如何, it is imperative (重要紧急的) failed的test才是你要写的代码.如果你不能预测到failure message, 你应该运行tests.

还要写点错误的用例,来测试对错误的输入的反应是否正确.

#have_content . Like #have_link , this method comes from Capybara, and is actually #has_content?
. #has_content? will look on the page for the given text, ignoring any HTML tags.

从上面的过程中抽象出测试的4个阶段

为什么要分四个阶段? 让测试通过分区变得容易点? 1部分变成4部分不是变得复杂了吗,怎么便容易了? 因为测试变多的情况下,就变成了一坨,看代码就变得困难. 分开后方便更快定位到代码.

test do
  1. setup,   创建变量和对象
  2. exercise,  执行功能
  3. verify, 验证结果
  4. teardown, clean-up,重置数据库到测试前的状态,重置全局变量, 测试框架悄悄的完成了.
end

你用你的判断力discretion将他们分到各个逻辑部分,主要目的是让代码更容易读.

前面我们测试了创建一个link.现在要要在首页展现这个link,是否需要再次运行创建link的逻辑呢? 不是不行,是调用capybara运行比较慢,而且已经测通过了. 我们可以直接将数据写入数据库来测试.
link = Link.create(title: "Testing Rails", url: "http://testingrailsbook.com")

这有用,但是有些 serious downfalls. 严重的向下掉的,也就是效率会下降. 因为如果测试大型应用.几百个测试,每个都收到创建一个Link. 然后后面我们要给所有的test的link加个reqired field..那必须把所有的测试都过一遍.让他们pass.
有两个可行的解决方案.

Fixtures
在yaml文件里定义样例数据

# fixtures/links.yml
testing_rails:
  title: Testing Rails
  url: http://testingrailsbook.com

# In your test
link = links(:testing_rails)

问题是fixures定义在测试外, 找问题的时候, 需要hunt down(追捕到) another file to be able to understand the entirety of what is happening.
as applications grow.需要各种variations在每个models对应不同的situations. fixtures的数量可能会比较多.各种用例要覆盖到,那就要造各种数据.

FactoryBot
Rather than defining hardcoded data, factories 定义分类,预定义必要的逻辑,当你instantiating实例化的时候,可以覆盖变量. like this:

rails generate factory_bot:model Link title:string url:string
      insert  spec/factories.rb
 cat spec/factories.rb 
FactoryBot.define do
  factory :link do
    title { "MyString" }
    url { "http://MyString/" }
  end

end

# spec/factories.rb
FactoryGirl.define do
  factory :link do
    title {"Testing Rails"}
    url {"http://testingrailsbook.com"}
  end
end
# In your test
link = create(:link)

# Or override the title
link = create(:link, title: "TDD isn't Dead!")


factories 比 fixture 慢一点,但是灵活一些, 值得使用.
group :development, :test do
gem 'factory_bot_rails'
end

还要数据库清理
Instead of using the database_cleaner gem directly, each ORM has its own gem. Most projects will only need the database_cleaner-active_record gem:

# Gemfile
group :test do
  gem 'database_cleaner-active_record'
end

If you are using multiple ORMs, just load multiple gems:

# Gemfile
group :test do
  gem 'database_cleaner-active_record'
  gem 'database_cleaner-redis'
end

Install the new gems and create a new file spec/support/factory_bot.rb :
# spec/support/factory_bot.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  config.before(:suite) do
    begin
      DatabaseCleaner.start
      FactoryBot.lint
    ensure
      DatabaseCleaner.clean
    end
  end
end
测试套件运行前,这个file will lint your factories . make sure 所有的factories都是valid.
FactoryBot.lint会写数据到数据库,所以需要Database Cleaner来还原数据库状态.
现在还需要在你的rails_helper里面包含这个factory.
把支持文件夹下面的文件都引入吧.

# Uncomment me!
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

现在就可以创建 factories file了.
#spec/factories.rb

FactoryBot.define do
end

#spec/features/user_views_homepage_spec.rb .

require "rails_helper"
RSpec.feature "User views homepage" do
  scenario "they see existing links" do

  end
end

我们用Factory的create
link = create(:link)
但是....有错误,  应该是FactoryBot.create,,, 为了保存代码干净, 我们更好的做法是在Link class里面写这个代码.
# spec/factories.rb
factory :link do
title "Testing Rails"
url "http://testingrailsbook.com"
end
注意:定义之定义最少的field,  多了后就不可管理了.
We only define defaults for fields that we validate presence of.

Not following this advice is a common mistake in Rails codebases and leads to major headaches.

测试的时候,通过 data-role来定位页面上的元素,不要通过css,因为css会变.
We’ll frequently use data-role s to decouple our test logic from our presentation logic. This way, we can change class names and tags without breaking our tests!


单元测试
RSpec.describe Link, "#upvote", type: :model do

end

We prefix instance methods with a # and class methods with a .
实例方法前面加#
类方法前面加.


link = build(:link, upvotes: 1)
.build is another FactoryGirl method. It’s similar to .create , in that it instantiates
an object based on our factory definition, however
 .build does not save the object.
.build 方法不保存到对象里.

Whenever possible, we’re going to favor .build over .create , as persisting to the database is one of the slowest operations in our tests.
可能的情况下,我们更喜欢用build,而不是create, 比较create要执行比较慢的写入数据库的操作.
那你可能会问: 为什么不用Link.new 呢?
通new创建的对象是不会立即写入对象,  但是一旦你调用方法,那就写入对象了......

比如 link = Link.new(upvotes: 2, downvotes: 1)
link.upvote
link.upvote
  Link Update All (0.2ms)  UPDATE "links" SET "upvotes" = COALESCE("upvotes", 0) + ? WHERE "links"."id" IS NULL  [["upvotes", 11]]


后面的比较零碎, 简要的做些记录

测试是个单独的孤立的组件.但这是理论上的,That's nice in theory. but in the real world most objects depend on collaborators  which may in turn depend on their own collaborators.
You set out to ()test a single object and end up with a whole sub-system.
阅读量: 1068
发布于:
修改于: