不客气地说,这个测试写得很糟。测试也是要当文档的,是给别人和自己看的,测什么东西,过什么步骤,需要一目了然。测试的代码就是你调用 API 的过程,简洁好用的 API 不需要递归、元编程、循环一大堆逻辑来调用。
就这个具体测试而言,用户输入什么回复,系统就会给出什么回复。这是对系统的要求,所以必须明确定义在测试中,什么流程返回什么信息,不需要过度抽象。如果靠读取系统自己的 key/value 来验证,那样是自己测自己,永远不会出错,也就失去了测试的意义。
我改一下你看怎么样:(有些具体业务逻辑调用不一定对,大致反映个意思)
describe "system responds according to user input" do
before do
setup_system # For example instantiate WxMessage
user_send '菜单'
expect(response).to match("请回复对应功能序号")
user_send "1"
expect(response).to match("子功能")
end
it "responds sub function A" do
user_send "A"
expect(response).to eq("AAAAA")
end
it "responds sub function B" do;
# Do similar
end
end
如果是因为发布次数十分频繁,每次都要编译觉得麻烦,可以考虑 cap 命令加选项,在 assets 没改变的情况下手动跳过编译。
如果不是上述原因,而真的是编译时间会长到难以接受的话,可能需要审查一下了。
CSS,JS 方面,如果时间太长,那前端 js, css 只怕会也会非常大了。需要看一看。
如果是图片编译耗费时间多,那就检查一下是否这些图片都是必需的,是不是有一些应该是数据范畴而不是 assets 范畴。如果是数据,那就移出 assets, 放到云里,由应用直接管理。如果是 assets,数量非常多,变动又很少,可以考虑跳过某些文件夹而直接复制到服务器。
@QueXuQ 谁也不能保证不出错 :) 不过 crontab 是 linux 的系统服务,正常情况下都很稳定的。如果有偶然性的错误,应该由系统的监控来负责,应用程序只要测试符合要求可以不必考虑别的部件的状况。
另外,这个 model 其实是一个 aggregation, 不包含原始数据。实在有问题也可以根据原始数据重新生成。
@shangrenzhidao 不用这么客气 :) 我见识有限,算法也比较弱,实际中应用具体算法的机会很少。但我觉得算法书里面分析问题和解决问题的思路还是经常可以用到的。
可以的。你强制自己只使用 Pseudo language 允许的语法就可以了。for, while, if 等等,用一下 each 也是可以的,再复杂的不用就是。
练完了记得把思路调回来,不然以后写的 ruby 代码会比较难看,哈哈。
SQL 可以满足现在需求,因为你的描述看起来不复杂。
但是,假设需求将来增加了呢?比如说,需要你画出一个图表展示每天的成本,销售和利润变化曲线图。有点傻眼了吧,我也是。好嘛,过一阵老板又说:"我还想看看每周以及每月的变化"。啊.....
虽然不会现在就写将来的需求,但最好能保证将来需求发生合理的变化或者增加时,我不会太傻眼。
这个需求如果是我来写的话,很简单,不用 SQL。
新建一个 model
rails g model Stat sale_amount:integer mean_amount:interger sale_cost:interger total:integer
或者更灵活,直接使用 json field(PostgreSQL) 或者 text field 做 serialize
rails g model Stat data:text
然后 Stat model 加一个 Class methods,计算当天的数据,这些你都写好了,然后新建数据。再做一个 rake task 调用这个 method。
这样就可以了,然后做一个 cron job,每天定时写进去,发送报表给相关人,收工。
算法改变了?需要统计的项目改变了?没问题,写一个 rake task 或者 migrartion 更新所有数据。我不介意在 rake task 里面大量使用 SQL, 但我很介意在应用本身里面大量使用它们。
@rocknrollhacker 是有些别扭,但要拿到 asset 地址必须通过 Rails helper。我也是不太喜欢 js 代码和后端混在一起。
还有一个办法其实也可行,会比较干净。
首先,你在需要做评级的 div 加一个 data attribute,比如 data-image,这个可以在 Rails 的 view 里面正常写,引用 asset_path
<div class="rating" data-image="#{asset_path('something.png')}">
然后,你自己写一个 plugin 的补充函数,比如这样
$.fn.raty.defaults.getImage = ->
$('.rating').data('image')
最后,在设置图片时,直接调用这个函数
$.fn.raty.defaults.starOff = $.fn.raty.defaults.getImage()
这个就是 Unobtrusive Javascript 的写法了,分离比较干净。
@blacktulip 我想你都说的很详细了。具体的例子我记得不是很清楚了,类似这个
= simple_form_for(User.new,\
html: {class: "form-horizontal", id: "user_sign_in"},\
validate: true) do |f|
不确定这段代码没有加括号会不会出问题,总之我在这里碰到过问题,所以后来所有的form_for
到do
之间都加上括号。
这个括号坑在用 form helper 的时候遇到过 :)
我觉得你现在遇到的难题应该和架构有关。
如果之前预测到 Angular 只是轻量级的使用,那么 jamine-rails 加 Capybara 加肉测应该够用了,因为前端逻辑不是很多。jasmine-rails 我之前用过,测一点点 backbone 的单元测试一点问题都没有,但复杂的前端就会觉得不够力。Capybara 我觉得尽量少用,因为太慢,而且 coupling 太高,除非是既复杂又重要的流程否则不用它。
如果预见前端有大量的逻辑,那么就最好不要使用 assets pipeline,而是前后端彻底分离,使用 Angular/Grunt 的体系来做前端的一切,包括发布、测试等等。这样的话后端直接用 request spec 或者 controller spec 测 API。简单明了。前端自己测自己的,和后端无关,拥抱一切 Angular 最佳实践比如 karma 等等。
@turingbook 匆忙写的几句话被你引用了,不好意思 :) 谢谢你的文章,介绍很详细,链接也很有帮助。
写个服务不费事,一个 API 就可以了。Node, Sinatra, Grape, 啥都行。
CoffeeScript 也可以写成 erb 的。把文件名从 foo.coffee 改成 foo.coffee.erb 就可以了。
这个不科学。貌似重心不对。
暂时发现几个特点:
1, 颠覆 Rails 命名系统,直接使用 Ruby namespace 来管理。我觉得这个好,又简单又灵活。
不明白的地方:
@fengzhilian818 这个意思我明白啊,就是主贴的意思一样。我和@dorentus都回答了一样的看法,就是后台直接取出适当批量的数据效率会比较 高,无论是处理速度还是网络传输。
@iBachue 晚些我再研究一下。
@springwq 不客气:)不过不确定这能不能叫 feature, 貌似是基本功能吧。这个 has_secure_password 真是非常好用,简洁干净。
@vincent 多谢你的观点!
楼主很细心啊。这个流程确实可以过,没问题,其实也不是 bug。道理是@yumu01提到的那句话,但实现起来是弯弯绕绕的。我来详细说一下。
主要的疑问应该在于,第一次的 create 和第二次的 save 都会触发 validation,但为什么第二次可以过呢?
首先我们需要明白 validation 是怎么实现的。主要原理是通过一个隐藏的 hash。
class_attribute :_validators
self._validators = Hash.new { |h,k| h[k] = [] }
然后,第一次过 validate 的时候,如果这个 hash 的其中一个键值 attributes 是空 array, 那么每个在 model 里面定义过的 validates 都会被加进去:
if validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes.each do |attribute|
_validators[attribute.to_sym] << validator
end
else
_validators[nil] << validator
end
validate(validator, options)
你也看到上面这段代码了,如果不是空的,那么后面的 validation 根本就不会添加任何新东西。
那好了,第一次create
的时候有没有添加password_confirmation
这个 attribute 呢?答案是否定的。原因在这:
if options.fetch(:validations, true)
# This ensures the model has a password by checking whether the password_digest
# is present, so that this works with both new and existing records. However,
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
record.errors.add(:password, :blank) unless record.password_digest.present?
end
validates_confirmation_of :password, if: ->{ password.present? }
end
你看到了,如果 password 是空的,那么 password_confirmation 就不会有任何检查动作。
综合上面原理。你这个情况的流程是这样的
User.create(name: 'foo')
。这个 validation 肯定是失败的。但是,因为 attributes 里面没有 password,所以 password_confirmation 不会加到_validators[:attributes] 里面去。
第二次 save, validation 也触发。但是_validators[:attributes] 已经存在,所以程序不会再增加任何新的 attributes。而这个里面又没有 password_confirmation, 所以这一项不会检查。
不过在实际中,基本上不会有先 create,失败后再 save 的 use cases。所以这个并不是 bug。
开始还以为楼主是医生同学,暗自感叹 8 个月就这么厉害。后来细看不是。虽然楼主和医生同学仍旧很厉害。
想请教楼主,为什么你选型会选 MongoDB? MongoDB 有什么 PostgreSQL 不能做到的吗?
用 Google doc 吧。可以评论,可以版本控制,什么都有。
@lithium4010 原因可能是 before 执行在 let 的前面。
两个解决方法和一个改进意见。
方法一:把let!
放到更高层的 context 或者 describe。这样就可以保证在这个 before 之前执行。
方法二:不用 let!,直接用 instance variable,在sign in...
之前加入@lithium = create(:lithium)
改进意见:你这个 spec 其实不是测的 login 而是其他,所以你其实不必要把 login 的步骤加进去,因为那个已经测过了。如果你用 Devise,可以用 sign in helper 代替这个过程。其他库比如 Authlogic 或者手写 authentication 也可以做,不过我不太记得了。
把let
改成let!
问题就解决了。
这种情况是从数据库里面调最近的 5 个 docs,但也不算动态的,因为这个数据其实和当前的 controller action 无关。
做法很简单,不要在 controller 里面定义@doc_recent
,直接在 partial 里面做。Controller 里面只定义和当前 action 直接相关的 instance variable.
# _nav partial
<% Doc.recent.each do |doc| %>
<li><%= doc.title %>
<% end %>
然后 Doc model 里面
def self.recent
order('updated_at DESC').limit(5)
end
这种做法可以达到要求。但有一个小问题就是流量大之后 cache 可能会更新很快,如果经常有新的 doc 创建的话。当然你现在可以暂时不用关心这个问题。
以后如果改进,可以在用户点击"Doc“菜单后,用 Javascript 动态调取数据,这样 cache 就可以维持比较久。
@fengzhilian818 不太明白你的意思,能否说详细点。
@Rei 这个是风格不同。我第一位考虑的是 model 和 model 的 API。
@zacker330 没错,如果 events 初步定在存在数据库里面,大致是这样。也有可能我在跑第一个命令时不写数据字段或写得不到位,然后再打开 migration 文件修改,这个不影响流程。
我没有说不需要考虑存储,我是说以 model 为中心思考问题。你现在需要拿到 events,就可以
user.events
而不是
User.find_by_sql("SELECT * FROM events ....")
以后也许考虑 performance 或其他原因,把 Event 放到其他服务。比如一个 Node.js 的 API。但你所做的只需要迁移 Event 自身,不需要过多考虑其他 model 直接的关系。之前的user.events
仍然有效,但User.find_by_sql...
就会失效了。