Rails 5 Test Prescriptions 第8章 Integration Testing with Capybara and Cucumber

Capybara:  A complete reference is available at rubydoc.info.

 

 

集成测试就是把局部的程序组合起来测试。

端到端测试是一个特殊的集成测试,覆盖了系统的全部行为, end-to-end.

接受测试acceptance test是用来指定正确行为,从客户或商业想法出发。Acceptance tests 通常在代码开始之前就计划或者写了。

 

集成测试也使用Javascript driver来评测基于Javascript的模仿用户行动。

本章是无Js版本,下章是有Js版本。

 

  • Setting up Capybara✅
  • Using Feature Tests to bulid a Feature✅
  • What to Test in an RSpec System test✅
  • Outside-in Testing
  • Making the Capybara Test Pass
  • Retrospective回顾
  • Setting up Cucumber ⚠️新的知识点,概览!!
  • Writing Cucumber Features
  • Writing Cucumber Steps
  • Advanced Cucumber 
  • is Cucumber Worth it?

 


A Field Guide to Integration and System Tests 

 

Rails core testing library defines two similar types of end-to-end test:

integration test:传统的Rails测试,使用自己的API,功能没Capybara强大。

system test:5.1新增的。需要使用Capybara 。也叫feature tests. 也可以叫integration test。

另外Request 测试,是指URL for routing的test input。也是integration的一种。(11章会学习 request specs.)

RSpec-rails团队,推荐使用Capybara在系统测试中,不论是否使用JavaScript。

 


 

Setting Up Capybara   (API  doucument)

 

使用自己的API来和元素交互,使用驱动来管理实际的交互。默认不提供Js 交互,但可以配置使用一个无头的浏览器,如headless Chrome让JS交互被模仿。

 

gem "capybara-screenshot"用来闪存测试失败 (800星)放在support/system.rb

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end
end
require "capybara-screenshot/rspec" 

 

Using Feature Tests to Build a Feature 

 

Writing a test

Given: 一个project ,2个task. 

When/then: 用户填写form,你确认新的task建立

When/then:用户移动一个task up向上(增加向上和向下的按钮,task的排列?),你确认顺序改变。

 

system/add_tasks_spec.rb

⚠️ RSpec有rspec:feature生成器。

RSpec.describe "adding a new task" do

# given 3 data 

  let!(:project) { create(:project, name: "Project Bluebook") }
  let!(:task_1) { create(
    :task, project: project, title: "Search Sky", size: 1) }
  let!(:task_2) { create(
    :task, project: project, title: "Use Telescope", size: 1) }
  it "can add and reorder a task" do

# when/then1: 

    visit project_path(project)
    fill_in("Task", with:"Find UFOS")
    select("2", from: "Size")
    click_on("Add Task")
    expect(current_path).to eq(project_path(project))
    within("#task_3") do
      expect(page).to have_selector(".name", text: "Find UFOS")
      expect(page).to have_selector(".size", text: "2")
      expect(page).not_to have_selector("a", text: "Down")

# when/then2: 

      click_on("Up")
    end
    expect(current_path).to eq(project_path(project))
    within('#task_2') do
      expect(page).to have_selector(".name", text: "Find UFOS")
    end
  end
end

 

The Capybara API: Navigating 

vist 对应 HTTP GET方法,

click_on(link/button)对应  HTTP POST方法

expect(page).to have_current_path(project_path(project))

⚠️最好👆这么写。current_path方法返回当前页面路径,不建议直接用。

 

草!用expect(current_path).to eq projects_path测试的时候,测试通过,但手动根本没有通过。have_current_path,和 current_path别用了,有bug!

 

The Capybara API: Interacting   (看之前博客)

fill_in(locator,  with: "TEXT")  #locator可以使用DOM ID, name, label text. 

 例子:

<form>
  <label for= "user_email" >Email</label>

  <input name= "user[email]" id= "user_email" />

</form> 

 

fill_in( "user_email" , with: "noel@noelrappin.com" )  #🆔

fill_in("user[email]", with: "noel@noelrappin.com"

fill_in("Email", with: "noel@noelrappin.com"

 

select(value, from:locator) 

attach_file(locator, path) 。#5.2Rails有上传Active storage模块了

 

The Capybara API: Querying 

 可以使用# 或者 .也可以用[]来暗示HTML的属性。如input[nam="email"]

have_selector, take options that limit whether a selector with the given HTML tag, DOM class, and/or ID matches.  

在mini test, assert_no_selector. 

 常用参数:

 count.如count:3,其他如between(1..3), minimun, maximum

 text: "string"    #或者正则

 visible。默认是匹配可见的元素。visble::hidden , visible::all

 

还有have_text, have_link?等灵活的方法。

 

save_and_open_page

#超有用的debug方法。闪存出错的模拟网页。使用gem launchy来自动弹出网页。

gem 'capybara-screenshot  '可以自动储存错误网页到/tmp目录


 

What to Test in an RSpec System Test 

 

在让测试通过前,回顾一下你正在试图做什么。TD process,你将写一个系统测试 来开始,然后开始写单元测试来驱动底层的逻辑。

比喻:测试pyramid.

 

顶层是少量的Integration tests,中间不确定,底层是大量小的单元测试。测试金字塔可以起到指引测试的作用。

系统测试的素材:

 

  • 在控制器和模块之间或者其他对象的互动所提供的数据。 
  • 在由多个控制器行为/方法组成的工作流的互动。
  • 某个安全问题涉及到一个用户状态和一个特别的控制器行为
单元测试:

 

  • 特别的商业逻辑,如如果数据nil或有一个错误的值会发生什么
  • 错误事件。
  • 商业逻辑的内部实现细节。 

 

 

⚠️ 集成测试的缺点:

 

  1. 集成测试更慢
  2. 集成测试不能精确判断内部的逻辑执行的错误。 

 

 


 

Outside-in Testing (feature tests)

从外面写测试并用那个测试来驱动你的单元测试。

相同的思路,TDD使用失败的单元测试来驱动写代码,feature tests使用失败的接受测试(在an acceptance test中的a failing line)来驱动单元测试的创建

 https://ww1.sinaimg.cn/large/006tKfTcly1frsbapf6kfj31h60skadg.jpg

 主线是写集成测试,运行, 看是否通过。

 在运行阶段:出现错误,小错直接修改代码,大的逻辑错误则进行单元测试。 

 当单元测试完成后,再进行写集成测试,最终会通过。

 然后可以重构这个测试。

 如果集成测试增加了新的功能,继续进行这个♻️写测试再运行 。


 

Making the Capybara Test Pass 

system/add_tasks_spec.rb写完了集成测试,见上!⬆️

运行,第一个失败是没有show。所以要写ProjectsController中的show方法和视图。

再测试发现失败,是因为没有创建TasksController中的create方法。

⚠️字段的验证,可以写一行。

  def create
    @task = Task.new(params[:task].permit(:project_id, :title, :size))
    redirect_to @task.project
  end

再测试失败,此时是到了 

 

     Failure/Error:

 

       within("#task_3") do

根据之前的give/when/then的工作流程。这时,应该除了加DOM ID,还应该增加一个新的功能,给tasks列表调整显示的顺序。

经过思考,需要再Task中增加order属性。

rails g migration add_order_to_tasks project_order:integer 

rake db:migrate

在project.rb中,客制化查询条件scopes for has_many

 

  has_many :tasks, -> {order "project_order ASC"}, dependent: :destroy

在集成测试文件内,let(:task_1)也需要更新属性。

 

给一个新的task在order的调整上,增加一点逻辑,有三个方法:

 

  • 使用回调,after_save callback on Task
  • Project中增加一个方法
  • 创建一个Task workflow 对象,类似CreatesProject对象(早期建立的)

现在到了unit-test model,测试的是对tasks.project_order的排序功能。 每添加一个新的task,这个task获得project_order的值是最后一次增加的task的project_order的值+1。

在project.rb中加一个next_task_order方法。 

spec/model/project_spec.rb 

  describe "task order", :xx do
    let(:project){create(:project, name: "Project")}
    it "masks 1 the order of the first task in an entry project" do
      expect(project.next_task_order).to eq 1
    end
    it "gives the order of the next tasks as one more than the highest" do
      project.tasks.create( project_order:1)
      project.tasks.create( project_order:2)
      project.tasks.create( project_order:3)
      puts "~~~#{project.tasks.last.project_order}"
      expect(project.next_task_order).to eq 4
    end
  end

 

 

 下面合并它。

第一,task控制器的新方法, up, down.并修改create方法,增加project_order。

create: 

@project = Project.find(params[:task][:project_id])
@project.tasks.create(task_params.merge(

project_order:@project.next_task_order))

 

 

然后,在view界面增加ID.   

<tr id="task_<%= task.project_order %>">

 

这是测试中对应的 #task_3 selector. 

 

测试,在click_on "up" 处报错,思考现在需要什么逻辑,怎么为它写一个测试?

given: 一个project, 3个task.

when/then:

 

  1. 能判断这个task是不是第一个或最后一个。
  2. 每个task能知道它相邻的2个task
  3. 每个task都可以上/下移
  4. 第一个task上移,order不变。
  5. 最后一个下移,project_order不发生变化。 
需要写几个方法:

first_in_project?, last_in_project?,

previous_task, next_task,

move_up, move_down 

 

⚠️ first_in_project?, last_in_project?直接写在mode层,这是boolean值,带❓的方法。

 

move_up, move_down 需要决定是否使用RESTFul,这里用up/down方法包含前两个方法。

up/down写独立的路径:

  resources :tasks do

 

    member do
      patch :up
      patch :down
    end
  end

然后在view中加up/down的链接。 

        <td>
          <% if !task.first_in_project? %>
            <%= link_to "Up", up_task_path(task.id), method: :patch%>
          <% end %>
          <% if !task.last_in_project?%>
            <%= link_to "Down", down_task_path(task.id), method: :patch%>
          <% end %>
        </td>

然后在task.rb中写previous_task, next_task, move_up, move_down 

 

 

自己写的代码不能通过测试!!!???

      third.previous_task.project_order = 100

     # 如果上面的代码结果是second.project_order = 100,
     # 下面third.previous_task定位会发生❌
     # third.previous_task.project_order

一旦不把代码抽出来重构,就出现nilclass的错误。 

  def move_down
    if !last_in_project?
      # swap_order_with(next_task)
      self.next_task.project_order, self.project_order = self.project_order, self.next_task.project_order
      self.save
      self.next_task.save
    end
  end
     Failure/Error: self.next_task.save
     NoMethodError:
       undefined method `save' for nil:NilClass

在 rails c里面建立数据后,用自己定义的move_down方法,也会出现❌。 

 

2个小时多,问题卡住,没有解决: 休息,🚿。

然后想到从起点开始写代码(从头开始设计✈️)5分钟搞定了哎:

    if !first_in_project?
      up_task = self.project.tasks.find_by(project_order: project_order - 1)
      up_task.project_order += 1
      up_task.save
      self.project_order -= 1
      self.save 
    end

测试都通过,✌️! 

再分析:
      # 选择交互的方式,也通过了测试。

 

      up_task = self.previous_task
      change_order = up_task.project_order
      up_task.project_order = self.project_order
      up_task.save
      self.project_order = change_order
      self.save

     # 之前写的代码的❌之处,是self.next_task.project_order 

不能用这种链式方法,应该分开:

down_task = self.next_task 

down_task.project_order = self.project_order 

链式的写法可以用于读取value,但不能修改。 

 

 


 

Review

通过集成测试开发TDD的优势::

 

  • 帮助开发者建设用户角色(故事):通过模拟用户对产品的使用,形成基础的网页结构和工作流程,并一步一步的落实。
  • 集成测试提供了这个:相关的各类的单元测试能够在一起工作,不会只见🌲不见森林。end-to-end测试强迫开发者描述森林。
  • 尽管集成测试比单元测试慢,但比手动测试浏览器页面快,可反复用,还能作为证据。(最后也得过一遍手动测试)

 


Cucumber(细分项目的工具)忽略未看。

一个写接受测试的工具。上一章提到开始的测试有点长和笨拙。需要重构一下。或者使用Cucumber。是一个使用自然语言(英文)的开发框架,可以让管理者和非开发客户预先设计,猜测:这导致中国人用的少。


 

posted @ 2018-05-29 10:33  Mr-chen  阅读(210)  评论(0编辑  收藏  举报