BigBing 技术博客

Rails4新项目的测试:minitest + capybara测试基于devise的用户注册

没有测试就会让写程序的开发者没有自信

这次的新项目没有想当然的选择之前一直在用Rspec测试框架,主要是我感觉Rspec里面有太多的magic,在helper文件中很多tricks, 很快相关的配置文件就非常大。这篇Blog说的跟我的体会很像 7 REASONS I’M STICKING WITH MINITEST AND FIXTURES IN RAILS

minitest是Rails4后默认支持的测试框架,另外rails也提供了很多方便和强大的assert方法。可以参考官方文档

下面我就总结和分享一下,Rails4新项目中minitest和capybara的集成和配置:

Gemfile

group :test do
  gem "minitest-reporters"

  gem 'capybara'
  gem "connection_pool"
  gem "launchy"
  gem "selenium-webdriver"

  gem "mocha"
  gem "poltergeist"
  gem 'phantomjs', :require => 'phantomjs/poltergeist'
end
  • minitest-reporters 是为了美化minitest的测试结果输出地,有颜色,有百分比。
  • mocha 是一个方便进行mock和stub的工具
  • 其他几个都是跟capybara相关的工具,具体会在配置中看到。

test_helper中添加的配置内容

在这里创建一个继承自新ActionDispatch::IntegrationTest的FeatureTest类,然后引入Capybara::DSL,作为Feature测试的父类。

require "mocha/mini_test"

require "capybara/rails"
require "capybara/poltergeist"

# 注释掉默认的driver,打开后方便切换driver测试,也可以在代码中指定current_driver
# Capybara.default_driver = :selenium
# Capybara.default_driver = :poltergeist

Capybara.javascript_driver = :poltergeist

class FeatureTest < ActionDispatch::IntegrationTest
  # 可以在功能测试中使用capybara的DSL
  include Capybara::DSL

  # 默认的失败测试提示不是很有用,扩展后方便调试
  def assert_content(content)
    assert page.has_content?(content), %Q{Expected to found "#{content}" in: "#{page.text}"}
  end

  def refute_content(content)
    refute page.has_content?(content), %Q{Expected not to found "#{content}" in: "#{page.text}"}
  end
end

# See: https://gist.github.com/mperham/3049152
class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
  end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection

测试确认邮件

如果要测试邮件的内容,内容中一般都会有链接,所以需要在config/environments/test.rb中加上:

config.action_mailer.default_url_options = { host: '127.0.0.1', port: 3000 }

如果使用了delayed_job可以在其配置中加上:

Delayed::Worker.delay_jobs = !Rails.env.test?

表明测试环境下,邮件任务都是直接发送出去的

用户注册和确认邮件测试的样例代码

在test目录下面,新建features目录,专门存放网站功能测试代码 test/features/user_authentication_test.rb

require 'test_helper'

class UserAuthenticationTest < FeatureTest
  setup do
    ActionMailer::Base.deliveries.clear
  end

  test 'user can sign up with email and password' do
    visit new_user_registration_path

    assert_content "注册帐号"

    within '#new_user' do
      fill_in 'user[email]', with: 'test_user@gmail.com'
      fill_in 'user[password]', with: 'abcd1234'

      click_button '创建新帐号'
    end

    assert_equal sign_up_success_path, page.current_path

    assert_content "test_user@gmail.com"

    test_confirm_email
  end

  private

  def test_confirm_email
    assert_not ActionMailer::Base.deliveries.empty?

    cf_email = ActionMailer::Base.deliveries.last

    assert_equal ["support@guangchuan.com"], cf_email.from
    assert_equal ['test_user@gmail.com'], cf_email.to
    assert_equal '确认信息', cf_email.subject
    assert_match /Confirm my account/, cf_email.body.to_s
  end
end

所以可以看到用minitest写的代码都非常直接,都是纯ruby代码。等代码增多后,可以按照ruby的方式来组织,重用和重构。

Rails4新项目的选择2:前端框架,Simple_form, 后台任务,搜索和用户认证

Rails的世界各种开源的优秀解决方案非常多,项目开始的时候比较痛苦地就是要根据自己的原则和项目的特点来做出正确的选择。

前端开发框架的选择

原本打算在新的项目中开始使用ES6,然后前端的开发框架选择容易上手的,轻量级的vue.js。由于Vue.js只支持IE9以及以上的浏览器,考虑到新项目是面向国内用户的产品,用户中还是有很大一部分的IE8,以及以下的浏览器用户。所以,最后还是决定使用Jquery+knockout.js这样的组合。

通过在论坛里面帖子前端框架选择的一个现实的问题评论了解到。jquery也有支持双向绑定的lib: jquerymy.js 还有就是在这种情况下,如果要做SPA的话,可以使用backbone.js,兼容性也是很好的。

放弃Simple-form

不再使用simple-form,主要是感觉它提供的一些功能对现在的项目帮助不大,还有就是simple form的wrapper定制比较麻烦,也会把页面代码弄得很臃肿。在这种前提下还是使用rails自带的form_for就可以了。原则就是如果要使用的gem帮助不是很大,还是最优先用Ruby和Rails自带的一些方案。

表单验证

尝试使用client side validations这个gem,这样前后端尽可能共享验证的规则,减少自己写相关js代码。少些代码,少制造bug。

Rails后台任务框架的选择:

网上已经有很多资料比较Sidekiq,Resque和DelayedJob。我自己也都有简单用过。 所以决定在项目开始时(流量不大)使用DelayedJob,简单易用,不要单独安装和配置Redis,同时使用ActiveJob提供的API,这样以后迁移到Sidekiq或者Resque也就不会很麻烦。

搜索:Elasticsearch

本周也开始看一下搜索功能的实现,以前我做过几年的基于Lucene的搜索开发,搜索服务就准备用elasticsearch,发现相关的Rails的整合方案也很多。有官方的elasticsearch-rails,还有一个比较流行的是searchkick,看了一下感觉功能很强大,所以下周准备拿searchkick试试。

用户认证:Devise

这个选择主要是因为Devise功能强大,相关文档也很多,之前在项目中也用过。

Rails4新项目之CSS框架的选择:Bourbon

我在之前的项目中使用过BootstrapFoundation,确实非常方便使用,把相关的gem放到项目中之后,然后再到网站上找到一些HTML和CSS的例子,基本能够满足需求,但是要说到对这两个框架有多了解,还真是谈不上,这两个都是大而全的东西,在Rails中基本上都是一股脑全部引入,然后就只管用,确实省心。

但是时间长了就发现,用这两个框架写出来的页面代码还是非常啰嗦,易读性和可维护性随着项目的变大越来越差。

之前还有一个项目用到了CompassCompass中也是包含了很多内容,我个人一直比较喜欢简单,轻量的东西,所以看到这种庞大的库就有点害怕。

然后我就想有没有其他选择呢?后来,在一个开源项目Hours中就发现了Bourbon,然后又是Neat,还有BittersRefills,一开始一下看到四个东西,感觉摸不清思路,都不知道是干什么的。后来,读了几篇文章后,就清楚了Bourbon整个技术栈的理念。就是把像Bootstrap这种大而全的框架,清楚地拆分成四个小的工具库,然后可以组合在一起使用,也可以根据需要来选择使用。

下面简单介绍一下四个工具:

  • Bourbon: 简单的轻量级的Sass Maxin
  • Neat: 一个轻量级的语义化的页面网格框架
  • Bitters: 脚手架(scaffold)样式,变量的定义,方便快速开始Bourbon的项目
  • Refills: 基于以上三个的一些实用的组件库

另外看到Bourbonthoughtbot出品的,我相信这些工具一定在实际项目中得到了验证。我一直以来都很喜欢thoughtbot开发一些gems,非常实用,而且轻量,比如clearance等。

其中下面这两篇文章说服了我:

5 Reasons We Chose Bourbon/Neat Over Foundation or Bootstrap

Why I prefer Bourbon over Bootstrap

最吸引我得就是使用BourbonNeat,可以写出非常易读,易懂的语义化的HTML

特别是当我们使用Haml或者Slim这样的模板语言后,在模板中如果都是一些语义化的CSS class,代码看起来就非常直观。

比如: 用Bootstrap会这样写(如果再加上一些响应式的样式就更加冗长):

.container
  .row
    .col-md-6 /* some content */
    .col-md-6 /* some content */

使用Borbon可能会是这样,非常清晰,直观:

.news
  .hot-news
  .latest-news

接下来简单说一下我是如何在新的Rails项目中使用的。

1. 基本安装:

这里没有什么特别需要说明的,大家只要按照官方github上的文档说明,很容易搞定。

2. 使用normalize.css

在Bootstrap中默认是集成了normalize.css,所以我们不用单独引入,在Bourbon栈中默认是没有的,需要我们单独引用.这里使用normalize-rails这个gem就可以了。

gem “normalize-rails”

3. 使用autoprefixer

Bourbon中有一大块功能是关于vendor prefixes的。需要开发者自己主动地去使用封装好的一些Sass的mixin,这个对于我一个后端出身的号称全栈的开发者要求有些高了。 同时autoprefixer的处理方式就是非常的傻瓜式,开发者自己不要管vendor prefixes,尽管写标准的CSS,剩下的autoprefixer自动搞定。 Bourbon的开发者也同意这一点,所以在Bourbon5.0中,关于vendor prefixes的部分也将会删除。 所以,我们只需要使用gem “autoprefixer-rails”就可以了。

gem “autoprefixer-rails”

如果想了解和深入学习Bourbon相关的技术,可以到官方网站上查看详细的使用文档。

这里还有一个youtube视频系列,可以帮大家系统学习: Awesome CSS with Bourbon, Neat, Bitters & Refills! 国内的朋友记得翻墙哦。

BTW:随着项目的进行,我会逐步写出对于一些架构和工具选择总结的文章。

快速选择Ruby Server:Puma和Unicorn

一句话:如果是Rails4之后的新项目,我会选择Puma,如果是之前的老项目继续使用unicorn。

使用Puma最主要有两个优点:

  • Puma支持多线程,可以提升并发。
  • Puma 省内存。 Unicorn 一个进程 120M - 140M,Puma 一个进程 80M-120M。

Heroku已经推荐使用Puma,主要由于Unicorn容易受到慢客户端的攻击

如果决定使用Puma,下面的两片文章会比较有帮助。

How Do I Know Whether My Rails App Is Thread-safe or Not?

How To Set Up Zero Downtime Rails Deploys Using Puma and Foreman

更多有用资料:

Scaling Ruby Apps to 1000 Requests per Minute - A Beginner’s Guide

Ruby Web服务器:这十五年

Github: awesome-webservers

Javascript学习笔记(持续更新中)

##Conceptual Aside

Syntax Parsers

A program that reads your code and determines what it does and if it’s grammar is valid (interpreter/compiler) do extra stuff

Execution Contexts

A wrapper to help manage the code that is running

Lexical Environments

where something sits physically in the code you write (where you write something is important)

Name/Value Pairs

A name which maps to a unique value

Object

A collection of name value pairs

The Global Environment And the Global Object

Global Object - like (window in browers) ‘this’

Outer Environment Global -> ‘Not inside a Function’

The execution context: creation and ‘hoisting’

Hoisting: Setup Memory Space for Variables and Functions

All variable in javascript are initially set to undefined And functions are sitting in memory in their entirety.

Javascript and ‘Undefined’

undefined: the variable hasn’t been set, is javascript special value.

The execution context: Code Execution

Single Threaded:

one command at a time

Synchronous Execution:

one at a time, in order

Function Invocation And The Execution Stack

invocation: running a function, using parenthesis () execution context stack

Variable Environments:

where the variables live: every execution context has its own variable environment how they relate to each other in memory

The Scope Chain:

Scope: where a variable is available in your code.

ES6

let -> block scope

Asynchronous callbacks:

Types:

Primitive Types:

primitive type: a single value 就是一个独立的值 undefined 也是一个primitive type js的number是浮点数,只有这一种数字类型 symbol是ES6中的新的类型

assignment is right to left:赋值操作的方向是从右往左

coercion: converting a value from one type to another.

  • operator: coerce: number to string

Number(false) = 0 Number(undefined) = NaN Number(null) = 0

Equality ==:

“3” == 3 #true false == 0 #true

Strict Equality: === doesn’t do coerce

绝大部分情况都是使用===进行比较,除非明确知道为什么要用==

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness

Boolean

Boolean(undefined) #false Boolean(null) #false Boolean(“”) #false Boolean(0) #false

Default value:

name = name || “Default name”

Objects and Functions

person[‘firstname’] person.firstname

var person = new Object();

Object literal

var person = {};

Fanking Namespaces: Using Object

keep variables and functions with the same name separate

JSON:

JSON.stringfy(obj) JSON.parse(“”)

Functions are objects

JS的functions也是对象,有自己的name属性等。

First Class Functions:

Everything you can do with other types you can do with functions Assign them to variables, pass them around, create them on the fly

CODE 也是function对象的一个属性 (Invocable)

Expression:

A Unit of code that results in a value

By value VS by reference:

primitive value: copy the value (by value) all objects interact by reference

Objects, Functions, ‘this’

var self = this

arguments and Spread:

arguments: the parameters you pass to a function arguments.length arguments[0]

function getPerson(){

return { #如果return后面没有{,JS的语法解释器会自动加上分号,导致函数就直接返回。 firstname: ‘tony’ }; }

Immediatiely Invoked function expressions

(function(name){ retrun ‘hello’; })(); () 里面就可以放入statement,所以可以放入function的statement

Closure:

小心循环的陷阱

function buildFunctions(){
  var arr = [];
  for(var i=0;i<3;i++){
    arr.push(
      (function(){
        var j = i;
        return function(){
          console.log(j);
        }
      }())
    )
  }
  return arr;
}

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();

function buildFunctions(){
  var arr = [];
  for(var i=0;i<3;i++){
    arr.push(
      (function(j){
        return function(){
          console.log(j);
        }
      }(i))
    )
  }
  return arr;
}

var fs = buildFunctions();
fs[0]();
fs[1]();
fs[2]();

Function Factories

Closures And Callbacks

callback function: A function you give to another function to be run when the other function is finished.

Call(), Apply(), Bind()

bind(obj): 返回一个copy的function,然后把function中的this赋值为obj

call(obj, ‘‘,…):会设置this为obj然后执行这个function

apply(obj, []):跟call类似,就是参数需要是array

function borrowing

function currying 使用bind可以预设参数值

Creating a copy of a function but with some preset parameters very useful in mathematical situations

Functional Programming

underscore.js -> source code # TODO lodash.com

Object-Orientied Javascript and prototypal inheritance

每个obj都有一个proto属性,指向其prototype

prototype chain:

// don’t do this EVER! john.proto

Everything is an object (or a primitive)

base object : Object

function的prototype -> function Empty(){} #apply(), bind(), call()

Reflection and Extend

An object can look at itself, listing and changing its properties and methods.

  for(var prop in john){
    if(john.hasOwnProperty(prop)){
      console.log(prop + ": " + john[prop]);
    }

  }
  underscore.js
    _.extend(john, jane, jim)
    to combine and compose other objects

function Person(){} # function constructor

A normal function that is used to construct objects.

The ‘this’ variable points a new empty object, and that object is returned form the function automatically.

Function的prototype属性只有是使用new的情况下次才会用到。

Built-in function constructors

var a = new Number(3);
a.toFixed(2);

var b = 3

a==b #true
a===b #false

moment.js -> Date lib

for array iterating overall properties is not save
Array.prototype.mycustomFeature = 'cool';
var arr = ['a', 'b', 'c'];
for(var prop in arr){
  console.log(prop + ':' + arr[prop]);
}

Object.create and Pure Prototypal Inheritance

var person = {
  firstname: 'Default',
  lastname: 'Default',
  greet: function(){
    return 'Hi ' + this.firstname;
  }
}

var john = Object.create(person);
john.firstname = 'John';
john.lastname = 'Doe';
console.log(john);

Polyfill

Code that adds a feature which the engine may lack

if (!Object.create) {
  Object.create = function (o) {
    if(argument.length > 1) {
      throw new Error("Object.create implementation only accepts the frist parameter.");
    }
    function F() {}
    F.prototype = o;
    return new F();
  };
}

ES6 and CLASSES

extends

classes in javascript a phrase that it’s just syntactic sugar

ODDS and ENDS

typeof, instanceof

typeof 3 #number typeof ‘hello’ #string typeof {} #object typeof [] #object Object.prototype.toString.call(d) #[object Array]

instanceof #检查prototype chain

typeof (function{}) #function

Strict mode

“use strict”;

function myfunc(){ “use strict”;

…. } https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode

Open Source Education

a dollar sign isNumberic

sizzle

it’s ok to return something from the function constructor (default is this) 可以在返回之前对this做一些操作,然后用return明确得返回

Transpile:

Convert the syntax of one programming language, to another.

TypeScript

Traceur: ES6 -> ES5

To read a list of features existing or coming in ES6, head here: https://github.com/lukehoban/es6features