芜湖企业做网站,英文网站首页优化,谷歌推广app,海南论坛论坛网站建设使用CucumberRspec玩转BDD(2)——邮件激活 2009年3月2日 星期一 ### 温故知新 ###前面我们已经完成了新用户注册功能的开发#xff0c;为了方便我们后面的开发工作且不扰乱之前的工作成果#xff0c;我们先将这份源代码归档并做个标记。为了获得更好的阅读体验#xff0c;读… 使用CucumberRspec玩转BDD(2)——邮件激活 2009年3月2日 星期一 ### 温故知新 ### 前面我们已经完成了新用户注册功能的开发为了方便我们后面的开发工作且不扰乱之前的工作成果我们先将这份源代码归档并做个标记。 为了获得更好的阅读体验读者朋友们可以在这里下载源码http://github.com/404/bdd_user_demo/tree/master### 提交工作成果到GIT仓库 ### $ cd ~/code/user_demo $ git init $ git add . $ git commit -m A user can be able to sign up. $ git tag v1 “git init” 会在 ~/code/user_demo 目录中初始化版本库接着 “git add .” 将 user_demo 目录中的所有文件信息编入索引index files然后 “git commit” 命令将根据 index 中的信息将工作内容提交到项目的GIT仓库里边去-m 选项加上了本次提交的一些说明最后 “git tag” 给这次提交所生成的版本号标记了一个别名叫 v1。 其实好习惯是在新建rails-app后就初始化版本库。由于篇幅的关系笔者才将些许GIT的内容放到这篇文章中。这不正好派上用场喽 在主干master上工作是危险的因为控制的不够好会扰乱版本这不是我们愿意看到的。为此GIT允许我们在主干道的基础上建立新的分支branch在分支中进行开发工作这样好控制风险。比如有时候分支中的工作搞得一塌糊涂开发人员想重来的时候直接丢掉删除这个分支再新建一个工作分支重新工作就是了这对项目中的主干完全没有丝毫影响不会扰乱你上次提交到master中的工作成果等你在新分支中开发完毕后再将这个分支中的工作成果归并到主干中就行。GIT的分支告诉我们丢掉一个烂摊子比收拾一个烂摊子要轻松得多潜意识里我们几乎一致认为这对开发人员的大脑是友好的:) 下面我们在主干的基础上为后面邮件激活这个功能的开发新建一个分支。### 新建工作分支 ### $ git checkout -b email_activation 或者 $ git branch email_activation $ git checkout email_activation 查看当前工作所在的分支 $ git branch 会返回项目中的所有分支前面加星*就是当前的工作分支。 做开发要步步为营不是吗git可以很方便地帮我们做到这点。在归档源码后接着我们新建了一个名为 email_activation 的分支并将当前的工作状态从master主干切换到email_activation分支中。这里说明下此时的 email_activation 相当于之前源码v1的一份副本这份副本是我们进行后续开发的基础后面我们将在用户已经能够注册的基础上进行用户激活帐号的开发工作只不过在这个基础上所开发的一举一动都会被记录到email_activation分支中。当用户注册成功并能通过邮件激活帐号后我们就可以将email_activation分支下的工作成果提交且归并到master主干中从而把邮件激活的功能和用户注册的功能完美的衔接在一起同时使得项目的版本干净整洁。 如果我们在email_activation的分支中的开发工作不尽人意怎么办呢如果是一些小小的修改那非常好办直接改成你想要的就是了可如果是大范围地修改后结果却不是你想要的有时会萌发重做的想法。下面就来告诉你一些开倒车的技巧 如果新增了文件需要先用git add添加这会被编入git的index但不会提交到git仓库否则回滚后会遗留下来这句话好像就是说等到你重新开发的时候发现要编码的文件已经存在了。可以用 git status 命令查看都添加或者修改了哪些文件。 如果你当前的工作目录working tree已经混乱不堪但是还没有提交可以使用 $ git reset --hard 这会丢弃所有的改变包括去除已经加到git index里边的内容然后将 working tree 和 index 恢复到上次commit时的状态。 如果想回滚到一个指定的版本就需要指定版本号 $ git reset --hard v1 v1 是我们在之前标记过的别名即上次commit所生产的版本号别名也可以替换成commit后的版本号比如 af2d45c... 版本号是一个唯一的哈希值每次commit都会生成一个省去了你找不到版本号的尴尬基本上使用git log 命令都能看到版本号。指定版本号的时候不需要写上所有字符取前5个就可以反正能说明版本号是唯一的就行了。比如你只有两次提交记录指定版本号的时候取哈希值的前两个字符又何尝不可呢 还有记得 --hard 选项要慎重使用具体的您可以使用 “git reset -h” 命令查阅更多关于撤销修改的详细信息。 如果只是想放弃对某一文件的修改可以使用 checkout 命令。这个命令不单用于分支间的切换还可以回滚一个指定的文件内容到上次所做的修改例如 $ git checkout app/models/user.rb 这会放弃对user.rb所做的修改并将user.rb的内容从上一个已提交的版本中更新回来。当然还可以指定回滚到指定版本例如 $ git checkout v1 app/models/user.rb 这会将user.rb的内容从已提交的v1所对应的版本中更新回来。 好了到此您已经了解了一些实用的GIT知识是时候步入正题进行我们的开发工作了我们来了解下工作内容。### 邮件激活功能 ### 1. 用户成功注册成为网站用户 2. 系统发送一封包含激活链接的邮件到用户注册时填写的邮箱中 3. 用户点击邮箱中的激活链来接激活帐号 4. 用户帐号激活成功并给出帐号激活成功的提示消息。 根据上面的功能需求我们在前面两个故事的基础上再添两笔。### 故事用例之用户通过邮件激活帐号 ### $ gedit features/user_signup.feature 修改后的文件内容如下 功能: 注册成为网站会员 为了能够浏览网站只对在线会员可见的那些内容 作为一名访客 我希望注册成为网站会员 场景: 用户填写无效数据并注册 当 我来到用户注册页面 而且 我在输入框用户名中输入invalid username 而且 我在输入框电子邮箱中输入invalid email 而且 我在输入框密码中输入password 而且 我在输入框确认密码中输入verify password 而且 我按下注册按钮 那么 我应该看到注册失败的提示信息 场景: 用户填写正确的数据并注册 当 我来到用户注册页面 而且 我在输入框用户名中输入404 而且 我在输入框电子邮箱中输入xuliicomgmail.com 而且 我在输入框密码中输入password 而且 我在输入框确认密码中输入password 而且 我按下注册按钮 那么 我应该看到注册成功的提示信息 而且 应该有封激活帐号的邮件发送至xuliicomgmail.com 场景: 用户激活帐号 假如 我已经使用404/xuliicomgmail.com/password注册过 当 我访问xuliicomgmail.com邮件中激活帐号的链接 那么 我应该看到帐号激活成功的提示信息 我们只是在已有的故事上加了个别子句。为了能让故事跑起来我们还需要针对故事场景中的情节编写相应的测试代码。### 编写用于驱动故事运行的测试代码 ### $ gedit features/step_definitions/user_steps.rb 添加如下代码 Then /^应该有封激活帐号的邮件发送至(.)$/ do |email| user User.find_by_email(email) user.activation_token.should_not be_blank sent ActionMailer::Base.deliveries.last sent.to.should eql([user.email]) sent.subject.should ~ /激活/ sent.body.should ~ /#{user.activation_token}/ end Given /^我已经使用(.*)\/(.*)\/(.*)注册过$/ do |username, email, password| valid_attributes { :username username, :email email, :password password, :password_confirmation password } user User.create!(valid_attributes) end When /^我访问(.*)邮件中激活帐号的链接$/ do |email| user User.find_by_email(email) visit activate_url(:token user.activation_token) end 故事用例基本上涵盖了我们开发的用意测试代码准备就绪还等什么赶紧跑起来看看吖。 运行测试 $ ruby script/cucumber -l zh-CN features/user_signup.feature 测试未能通过原本应该有封激活帐号的邮件发送至xuliicomgmail.com然而却没有因为我们还没有编写用于发送激活邮件的代码。习惯了玩测试的话测试结果无疑对指导你的编码工作非常有帮助 接下来我们就来做这些工作。### 添加激活码字段 ### 怎么知道用户有没有激活帐号呢答案是在 users 表中增加用于标识用户帐号是否激活的两个字段一个用来存放激活码另一个用来记录帐号激活时间。假设这两个字段分别是 activation_token 和 activated_at如果 users.activation_token 字段有值那么就说明用户还没有激活如果 users.activation_token 为空且 users.activated_at 有值那么就说明用户已经激活过了。 下面来添加这组字段 $ ruby script/generate migration EmailConfirm $ gedit db/migrate/*_email_confirm.rb class EmailConfirm ActiveRecord::Migration def self.up add_column :users, :activation_token, :string add_column :users, :activated_at, :datetime end def self.down remove_column :users, :activated_at remove_column :users, :activation_token end end $ rake db:migrate $ rake db:test:prepare 表结构准备完毕后再来生成用户注册时的激活码。### 生成激活码——activation_token ### $ gedit app/models/user.rb before_create :initialize_salt, :encrypt_password, :initialize_activation_token # 生成并返回标识码 def generate_token encrypt(Time.now.to_s.split(//).sort_by {rand}.join) end # 生成激活码 def initialize_activation_token if new_record? self.activation_token generate_token end end 数据模型搞定后再从路由下手需要指定控制器该如何分配响应请求。### 配置激活帐号的路由——activate_url ### $ gedit config/routes.rb 修改后routes.rb文件内容如下 ActionController::Routing::Routes.draw do |map| map.with_options :controller users do |page| page.signup /signup, :action new page.activate /activate/:token, :action activate end map.resources :users end 此时如果你不清楚接下来要做什么不妨运行测试测试结果会告诉你答案。由于笔者知道会失败也知晓接下里该做什么所以就略过此步因为Model和Route都准备完毕是时候动手编写业务流程了。 如果你用Rails发过邮件下面的步骤你一定很熟悉。### 生成邮件 ### $ ruby script/generate mailer UserMailer confirm $ gedit app/models/user_mailer.rb class UserMailer ActionMailer::Base def confirm(user, sent_at Time.now) subject 请激活您的帐号 recipients user.email from Admin sent_on sent_at body :username user.username, :url activate_url(:token user.activation_token) end end $ gedit app/views/user_mailer/confirm.erb 亲爱的 %username% 您的帐号已经创建成功请点击下面的链接激活您的帐号 %link_to url, url%### 发送邮件 ### 用户注册成功之后需要发送一封确认邮件到用户注册时填写的电子邮箱中。虽然可以在 User 模型中添加 after_create 的一个回调代码来执行但这样就给 User 模型类增添了本不应该承担的责任我们只需要 User 模型提供数据而不是将发送邮件的任务丢给它。这时候 ActiveRecord 提供的 Observer 就可以派上用场了使用Observer的好处是它可以将自身连接到模型类中并注册为回调却无需修改任务模型类的代码我们将其称之为观察器是否联想到Ruby设计模式中的观察者模式呵呵。下面我们针对 User 模型创建一个观察器 $ ruby script/generate observer User $ gedit app/models/user_observer.rb class UserObserver ActiveRecord::Observer def after_create(user) UserMailer.deliver_confirm(user) end end 然后在 config/environment.rb 注册这个 Observer。 $ gedit config/environment.rb config.active_record.observers :user_observer 再次发动测试引擎看看是否working $ ruby script/cucumber -l zh-CN features/user_signup.feature 由于在生成邮件那一章节里激活链接我们用的是 link_url 这种形式如果你知道 link_url 和 link_path 的区别那么根据上面的测试结果你应该了解出错的原因。如果不了解笔者在这里补充下link_url 会在链接中加上协议名、主机名和端口号这些而 link_path 则不用它会直接用根目录“/”代替之也就是说 link_url 会在链接中加上网址又或者说link_url 采用绝对路径而 link_path 采用相对路径。 考虑到现实中的用户注册系统会发送一封包含网址的邮件到注册用户的邮箱中我们之前的邮件模板里不得不采用 link_url 这种形式。结合测试结果来看也许此时您已经意识到我们是不是忘了配置主机名呢 恭喜您您确实猜对了。### 配置邮件中激活链接的绝对路径 ### $ gedit app/models/user_mailer.rb default_url_options[:host] HOST 在 config/environments/test.rb 和 config/environments/development.rb 这两个配置文件中定义 HOST 常量为了开发和测试需要这里设置成localhost就可以了。 HOST localhost:3000 不过在 config/environments/production.rb 中HOST 常量的值就必须是真实的主机名了。 另一种方法无需修改app/models/user_mailer.rb和定义HOST常量直接在各environment/各文件或environment.rb中配置就行了如下代码 $ gedit config/environment.rb config.action_mailer.default_url_options { :host localhost:3000 } 这样做的好处是只需修改一处。 好了补上这个配置再运行测试看看有什么不同。 $ ruby script/cucumber -l zh-CN features/user_signup.feature ### 激活帐号 ### 看来我们的邮件能够成功发送了不过好像访问邮件中的确认链接时出了点问题根据调试信息“ActionController::UnknownAction”显示应该是没有找到激活帐号的具体行为action。在前面的开发中我们真的就还没有编写响应用户激活帐号的相关代码想必此时我们都清楚该做哪些工作了。 我们需要给 UserController 类添加一个 Action 来响应用户激活帐号的请求。 $ gedit app/controllers/users_controller.rb 之前我们在config/routes.rb文件中定义了activate_path且该activate_path 的 :action 参数指向 activate 方法于是乎activate 就是我们需要在 UserController 类中添加的 action。activate方法的代码如下 def activate if user User.find_by_activation_token(params[:token]) if !user.activated? user.email_confirm! flash.now[:notice] 恭喜您帐号激活成功 end end end 仔细观察 UserController#activate我们还需要在 User 模型中编写 activated? 和 email_confirm! 这两个实例方法前者用来确认用户的帐号是否已经激活过后者则用来激活用户的帐号。 $ gedit app/models/user.rb 在 protected 之前添加如下两个方法 # 检查是否已经激活 def activated? # 当 activation_token 为 nil 时表示用户帐号已经激活 activation_token.nil? end # 激活帐号 def email_confirm! update_attributes(:activation_token nil, :activated_at Time.now) end 运行测试看看 $ ruby script/cucumber -l zh-CN features/user_signup.feature 看来是没有找到模板文件在此补上用户成功激活帐号的页面。 $ gedit app/views/users/activate.html.erb 保存即可。运行测试 $ ruby script/cucumber -l zh-CN features/user_signup.feature OK测试通过如图 ### 亲临现场 ### 最后开发人员自己别忘了手工测试以确保万无一失。 先清除数据库中的记录 $ ruby script/console User.delete_all 假设我们以404为用户名成功注册后我们来看看数据库中404的activation_token字段是否有值。 User.find_by_username(404, :select username, activation_token, activated_at) 可以看到activation_token 的值是一串加密后的字符activated_at值为空这说明程序已经给注册用户生成了激活码而且此时用户还没有激活帐号。 当我们注册成功后打开邮箱却并没有看到激活帐号的邮件这是怎么回事呢 因为测试程序跑到是test环境而我们手工测试的时候程序是运行在development环境下的我们没有针对development环境配置邮件服务器。下面我们采用SMTP的发信方式这里的SMTP SERVER用的是GMAIL而且是SSL验证登录方式三次握手发信速度没sendmail那么快呵呵 $ gedit config/environment.rb config.action_mailer.delivery_method :smtp config.action_mailer.default_charset utf-8 config.action_mailer.smtp_settings { :address smtp.gmail.com, :port 25, :domain YOUR_DOMAIN, :user_name YOUR_GMAL_USERNAME, :password YOUR_GMAIL_PASSWORD, :authentication login, :enable_starttls_auto true } 该配置中大写部分自行替换即可。 清空users表我们重新注册404这个用户。 $ ruby script/console User.delete_all 然后去邮箱看看 这回我们打开邮箱看到了激活帐号的邮件信息不过邮件内容中的链接标签没有生效我们期望发送到用户邮箱的是HTML格式的邮件。ActionMailer可以让我们发送多种格式的邮件只需要按相应的内容类型修改邮件模板的文件名格式即可。基本上邮件模板的文件名的格式像这样name[.content.type].renderercontent.type 可选缺省情况下为文本格式你也可以手工指定为 text.plain要发送HTML格式的邮件就需要指定为 text.html文件后缀 renderer 一般情况下都是 erb如果你用了HAML插件模板后缀名应该是haml。下面我们将之前文本格式的邮件模板修改为网页形式的 $ mv app/views/user_mailer/confirm.erb app/views/user_mailer/confirm.text.html.erb 再次清空users表重新注册404这个用户然后前往邮箱看看我们收到的邮件是否是网页格式的。 我们看到激活帐号的超链接生效了(没有将超链接标签明文显示)这说明系统发送出去的确实是HTML邮件。 接下来我们点击邮件中的链接来到了激活帐号的页面我们看到帐号激活成功的提示信息。 如果激活成功数据库中的activation_token字段应该是空值且activated_at字段的值应该为一时间戳在之前的程序中我们确实是按此逻辑编码的。虽然测试成功而且我们也非常顺利地亲历了一遍注册流程那么是否就说明我们的应用程序没有程序上的漏洞了吗我们真的激活帐号了吗我们不妨看看数据库这只黑匣子此时应该是让数据说话的时候了。 $ ruby script/console User.find_by_username(404, :select username, activation_token, activated_at) 哎呀记录居然没被更新看来我们被表面现象给忽悠了。我想你此时也和我一样迷惑为什么数据记录没有被更新呢这中间到底发生了什么这让我不由自主地想象起来也许Rails的ORM真的修改了User实例对象的activation_token和activated_at属性的值只不过还没有成功地写入到数据库里边而已。果真如此吗如何证明这一说法成立呢我们来看看User模型类的 email_confirm! 方法下面是email_confirm! 方法的源码 # 激活帐号 def email_confirm! update_attributes(:activation_token nil, :activated_at Time.now) end 我们知道update_attributes 方法还有一个和自己长得差不多一样的方法即 update_attributes!后者比前者仅仅多一个感叹号而已两者都是更新当前模型对象所指向的数据记录只不过前者更新失败会返回false后者更新失败则会抛出异常信息并停止程序运行我们不妨用 update_attributes! 替换 email_confirm!方法中的update_attributes如果问题真的出现在这里至少我们也可以看见抛出的错误信息这些错误调试信息对开发人员来说是那么的重要。 $ gedit app/models/user.rb 修改 email_confirm! 方法如下 def email_confirm! update_attributes!(:activation_token nil, :activated_at Time.now) end 保存然后重新访问或刷新激活帐号的页面我们看到系统捕获到了非常实用的情报如图 看来问题还真的出在User模型类的email_confirm!方法这里当程序尝试更新 activation_token 和 activated_at 这两个字段时系统告诉我们密码不能为空并就此打住程序抛出错误并停止执行后面当然不会更新数据库里边的记录了。找到出错的原因后我们马上就明白 update_attributes或update_attributes! 会更新当前对象所指向的记录的所有字段并在更新之前执行数据校验如果校验失败就会打断程序的运行。想到此针对问题的解决方案也初现轮廓只要程序更新指定的字段并在更新这些指定字段的时候不去校验其他字段的数据有效性就行了。OK我们有非常适合用于email_confirm!的替代写法不妨修改email_confirm!方法如下 $ gedit app/models/user.rb # 激活帐号 def email_confirm! self.activation_token nil self.activated_at Time.now save(false) end 上述代码中的 save 方法会更新这些字段的值第一个参数的值指明为false后将不会执行数据校验看来这一切和我们的想法非常吻合不妨保存user.rb再刷新几次浏览器看看。第一次刷新和我们初次访问激活链接看到的效果一样都是提示帐号激活成功后面几次就看不到激活成功的消息了因为帐号只需要激活成功一次就足够了效果确实很理想我们去访问下数据库让它给我们做个见证。 $ ruby script/console User.find_by_username(404, :select username, activation_token, activated_at) 哈哈数据记录已经更新了这意味着程序已经可以按照我们之前的意愿运行了。用户提交注册资料后会收到一封关于激活帐号的邮件然后点击其中的链接可以成功激活他的帐号。 至此我们在 email_activation 分支上的开发工作已经顺利完成可以将工作成果归并到主干中去了。### 提交工作成果到GIT仓库 ### $ git add . $ git commit -m People can activation their accounts by the confirm emails. $ git checkout master $ git merge email_activation $ git branch -d email_activation $ git tag v2 注意真正的开发中可不是到功能开发完毕了才commit而是边开发边add和commit。为了方便演示编码过程文章中没有一一列举。### 小结 ### 在这篇教程中我们的开发工作遇到了不小的挫折尤其是在人工测试那里经过我们自己动手测试后才知晓我们的程序漏洞百出。之所以这样是由于笔者有意而为之其实笔者的用意非常简单就是想告诉开发者亲临现场做人工测试的重要性。也许确实让您受挫了觉得好像是为了测试而测试似的大可不必有如此想法如果您是位Rails熟手想必也不会犯那些低级错误比如update_attributes和save(false)这种区别及其应用场合也会知晓 test/development/production 这几种环境的区别那也就避免了些不必要的麻烦。经验是慢慢积累的过程可以帮我们汲取经验。等您自己应用熟练了我相信您能体会到测试带来的好处。### 下节预告 ### 接下来我们依然是借助cucumberrspec来驱动用户登录功能的开发看测试跟session和cookie打交道。如果有兴趣期待您能够下次光临如果有好的建议和经验非常希望能够与您交流您可以在下面发表留言或者和我email联系我的邮箱是 xuliicomgmail.com。 标签 Cucumber, Rails, Rspec, TDD Posted by 404 转载于:https://www.cnblogs.com/ToDoToTry/archive/2011/09/10/2173390.html