




已阅读5页,还剩16页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
14.4 应用程序的集成测试Integration Testing of Applications下一个层面的测试是要验证应用程序的工作流程。在某种意义上,这就是在测试客户交给我们的用户故事我们正是根据这些故事来开发应用程序的。譬如说,有这样一个故事:“用户进入商店首页。用户选择一件货品,将其放入购物车。用户结账,在表单中填入详细信息。用户提交表单之后,数据库中创建一份订单,其中包含用户详细信息,以及与购物车中所有货品对应的订单项。”这正是集成测试的理想材料。集成测试需要模拟一个或多个虚拟用户与应用程序之间的一组连续的会话,你可以在其中发送请求、监控应答、跟踪重定向,等等。当创建模型,控制器的同时,Rails就会创建对应的单元测试/功能测试。集成测试却不是自动创建的,你需要自己动手来创建它们。depot ruby script/generate integration_test user_storiesexists test/integration/create test/integration/user_stories_test.rb可以看到,Rails自动地给测试文件的名称加上了_test后缀。现在来看看这个生成的文件。require #File.dirname(_FILE_)/./test_helperclass UserStoriesTest ruby_book.idassert_response :successcart = session:cartassert_equal 1, cart.items.sizeassert_equal ruby_book, duct话锋一转,用户故事的第三句话说“用户结账”。这很容易测试depot_r/test/integration/user_stories_test.rbpost /store/checkoutassert_response :successassert_template checkout此刻,用户需要在结账表单中填入详细信息。在他们提交表单之后,应用程序会创建一份订单,并将用户重定向到商店首页。我们先来测试HTTP这边:将表单数据提交到save_order action,然后验证是否被重定向到首页,同时还要检查购物车是否为空。post_via_redirect辅助方法可以生成一个POST请求,然后跟随重定向,直到收到常规的200应答为止。depot_r/test/integration/user_stories_test.rbpost_via_redirect /store/save_order , :order = :name = Dave Thomas , :address = 123 The Street , :email = , :pay_type = check assert_response :successassert_template indexassert_equal 0, session:cart.items.size最后,我们会察看数据库,确定其中有新建的订单与对应的订单项存在,并且其中的数据也正确。由于在测试开始时已经清空了orders表,现在其中应该只包含我们新建的订单。depot_r/test/integration/user_stories_test.rborders = Order.find(:all)assert_equal 1, orders.sizeorder = orders0assert_equal Dave Thomas , assert_equal 123 The Street , order.addressassert_equal , order.emailassert_equal check , order.pay_typeassert_equal 1, order.line_items.sizeline_item = order.line_items0assert_equal ruby_book, line_duct就是这样了。下面是完整的集成测试代码。depot_r/test/integration/user_stories_test.rbrequire #File.dirname(_FILE_)/./test_helperclass UserStoriesTest ruby_book.id assert_response :success cart = session:cart assert_equal 1, cart.items.size assert_equal ruby_book, duct post /store/checkout assert_response :success assert_template checkout post_via_redirect /store/save_order , :order = :name = Dave Thomas , :address = 123 The Street , :email = , :pay_type = check assert_response :success assert_template index assert_equal 0, session:cart.items.size orders = Order.find(:all) assert_equal 1, orders.size order = orders0 assert_equal Dave Thomas , assert_equal 123 The Street , order.address assert_equal , order.email assert_equal check , order.pay_type assert_equal 1, order.line_items.size line_item = order.line_items0 assert_equal ruby_book, line_duct endend更高层面的测试Even Higher-Level Tests(本节包含的内容相当高阶,读者可以放心地跳过它们。)Rails提供的集成测试功能确实很棒,我们还没见过别的框架内建如此高层面的测试功能。不过我们还可以进行更高层面的测试:可以给QA们提供一种专门用于测试这个应用程序的小型语言有时候也被称为领域专用语言(domain-specific language,DSL)。使用这种语言,前面看到的集成测试可以写成这样:depot_r/test/integration/dsl_user_stories_test.rbdef test_buying_a_product dave = regular_user dave.get /store/index dave.is_viewing index dave.buys_a ruby_book dave.has_a_cart_containing ruby_book dave.checks_out DAVES_DETAILS dave.is_viewing index check_for_order DAVES_DETAILS, ruby_bookend上述代码使用了DAVES_DETAILS这样一个hash,其定义如下:depot_r/test/integration/dsl_user_stories_test.rbDAVES_DETAILS = :name = Dave Thomas , :address = 123 The Street , :email = , :pay_type = check从文学的角度,这段代码也许算不上优秀;但它至少根容易读懂。那么,如何提供这样的功能?不算困难,只要使用Ruby提供的一个漂亮的小工具:singleton方法(singleton method)。假设obj是一个变量,它引用任意Ruby对象。使用下列语法,我们就可以定义一个方法,该方法只存在于obj这个对象上。def obj.method_name# .end然后,我们就可以在obj上调用method_name(),就像调用别的方法一样。这就是我们创造这种测试语言的办法:首先用open_session()方法新建一个测试会话,然后在会话中定义所有的辅助方法在我们的例子中,这一切都是在regular_user()方法中完成的。depot_r/test/integration/dsl_user_stories_test.rbdef regular_user open_session do |user| def user.is_viewing(page) assert_response :success assert_template page end def user.buys_a(product) xml_http_request /store/add_to_cart , :id = product.id assert_response :success end def user.has_a_cart_containing(*products) cart = session:cart assert_equal products.size, cart.items.size for item in cart.items assert products.include?(duct) end end def user.checks_out(details) post /store/checkout assert_response :success assert_template checkout post_via_redirect /store/save_order , :order = :name = details:name, :address = details:address, :email = details:email, :pay_type = details:pay_type assert_response :success assert_template index assert_equal 0, session:cart.items.size end endendregular_user()方法返回增强后的会话对象,我们在测试过程中会使用这个对象。定义好这种小语言之后,编写别的测试就容易多了。譬如说,我们需要这么一个测试:如果两个用户同时购买同一件货品,两人之间不应该有任何交互。(我们把“mike”这个用户相关的代码行缩进了,以便读者看清程序的流程。)depot_r/test/integration/dsl_user_stories_test.rbdef test_two_people_buying dave = regular_user mike = regular_user dave.buys_a ruby_book mike.buys_a rails_book dave.has_a_cart_containing ruby_book dave.checks_out DAVES_DETAILS mike.has_a_cart_containing rails_book check_for_order DAVES_DETAILS, ruby_book mike.checks_out MIKES_DETAILS check_for_order MIKES_DETAILS, rails_bookend在第676页,我们列出了完整的、实现了小语言的测试类源代码。集成测试支持Integration Testing Support集成测试看上去和功能测试很像,在单元测试和功能测试中使用的那些断言在集成测试中也同样管用。尽管如此,你仍然需要小心,因为很多辅助方法存在一些微妙的差异。集成测试经常涉及到“会话”的概念,后者代表了用户在浏览器上与应用程序的交互。控制器中有一个session变量,概念和这里的“会话”有些相似:但同样是“session”这个词,两处的意义却有所不同* 译者注:因此译者也采用了不同的方式处理。谈论控制器中的HTTP session时,“session”一词不翻译;谈论测试会话时,“session”一词译作“会话”。当开始集成测试时,你会得到一个默认会话(如果需要的话,通过integration_session实例变量就可以访问会话对象)。集成测试中用到的方法(例如get方法)实际上都是会话对象上的方法:测试框架会帮你将方法调用委派给会话对象。不过你也可以显式创建会话对象(使用open_session()方法),然后直接调用这些方法,这样就可以同时模拟多个用户的交互(或者为不同的人物创建不同的会话,然后在测试中先后而不是同时使用它们)。在第209页,我们就看到了“在测试中使用多个会话”的例子。集成测试的会话对象有下列属性。请注意,在集成测试中如果要对它们赋值,必须明确指定赋值的接收者。self.accept = text/plain # worksopen_session do |sess| sess.accept = text/plain # worksendaccept = text/plain # doesnt work-local variable在下列代码中,sess变量代表一个会话对象。accept要发送给服务器的accept头信息。sess.accept = text/xml,text/htmlcontroller指向最后一个请求使用的控制器实例。cookies一个hash,其中包含所有的cookie。在这个hash中放入内容就会随请求发送cookie;读取其中的值则可以看到应答中设置了哪些cookie。headers一个hash,其中包含最后一次应答所返回的头信息。host通过这个值设置下一次请求所针对的主机名。如果应用程序的行为与主机名有关,则可以用这个属性来进行测试。sess.host = fred.blog_per_path最后一次请求的URI。remote_addr下一次请求所使用的IP地址。如果应用程序区别对待本地请求和远端请求,这个属性口可能会有用。sess.remote_addr=request最后一次请求所使用的请求对象。response最后一次请求所使用的应答对象。status最后一次请求的HTTP状态码(200、302、404,等等)。status_message最后一次请求的状态码所对应的状态信息(OK、Not found,等等)。集成测试的便利工具Integration Testing Convenience Methodss在集成测试中可以使用下列辅助方法:follow_redirect!()如果控制器处理最后一次请求时进行了重定向,则跟随重定向。get(path, params=nil, headers=nil)post(path, params=nil, headers=nil)xml_http_request(path, params=nil, headers=nil)使用给定参数执行GET、POST或者XML_HTTP请求。path参数应该是一个字符串,其中包含要调用的URI。path参数不需要包含URL中的“协议”和“主机”部分,如果提供了这些内容并且协议部分为HTTPS,则集成测试会模拟https请求。params参数应该是一个hash;或者是一个字符串,其中包含经过编码的表单数据 application/x-www-form-urlencoded or multipart/form-data。get /store/indexassert_response :successget /store/product_info , :id = 123, :format = longget_via_redirect(path,args=)post_via_redirect(path,args=)执行get或者post请求。如果应答是重定向,则跟随重定向以及后续的 所有重定向,直到收到不是重定向的应答为止。host!(name)设置下一次请求针对的主机名,效果和设置host属性一样。https!(use_https=true)如果传入true(或者不传入参数),后续的请求会使用https协议。https?返回https标志的值。open_session|sess|)新建一个会话对象。如果在参数中传入了一个代码块,则会话对象会被传递给该代码块,否则直接返回会话对象。redirect?()如果最后一个应答是重定向,则返回true。reset!()重置会话,这样测试就可以复用会话对象。ur1_for(options)根据指定选项构造一个URL。此方法可以用于生成GET或者POST方法的参数。get url_for(:controller = store , :action = index )14.5 性能测试Performance Testing测试不仅要检查程序的功能是否正确,还要检查程序运行是否够快。在继续深入之前,应该先给你一个警告。大部分程序在大部分时候都没有性能问题,而且出现性能问题的地方常常是出乎我们意料的。正因为如此,在开发早期就关注性能通常不是个好主意。我们推荐,只有在两种情况下才进行性能测试都是在开发阶段的晚期:l 当规划负载量时,你需要知道一些实际数据,例如需要多少台机器才能承受预期的负载。性能测试可以帮助你获得(并调优)这些数据。l 如果应用程序部署上线之后性能不佳,性能测试可以帮助你找到问题的根源。找到原因之后,测试还可以防止同一问题再次出现。有一个典型的例子,那就是与数据库相关的性能问题。应用程序可能连续运行好几个月都没问题,直到有一天,有人给数据库加上了索引。虽然索引可以帮助解决某个特定的问题,但它却会带来一些意料之外的副作用,会严重影响应用程序其他部分的性能。从前(其实也就是去年的事),我们推荐的做法是用单元测试来监控性能问题。这样做的基本想法是:当性能出现局限时,测试可以及早给我们提出警告;我们可以在测试过程中了解到性能状况,不必等到部署之后。实际上,正如稍后将会看到的,我们仍然推荐这样做。但这种隔离的性能测试还不是全部,在本节的最后部分我们还会推荐另一种性能测试的方式。我们先从一个不那么真实的场景开始:需要知道store控制器是否能够在3秒钟内创建100份订单,在进行测试时数据库中应该有1000种货品(因为我们认为货品种类有可能相当多)。那么,应该如何针对这个场景编写测试呢?要生成这么多货品,我们需要用到动态夹具。depot_r/test/fixtures/performance/products.yml product_: id: title: Product Number description: My description image_url: product.gif price: 1234可以看到,我们把夹具文件放在fixtures/performance目录下了。夹具文件的名称必须与数据库表名匹配,因此无法在同一个目录下保存多份针对products表的夹具。我们把单元测试使用的夹具文件放在fixtures目录下,性能测试使用的products.yml文件则放在performance子目录下。我们从1循环到1000以便生成测试数据。一开始我们打算用1000.times do |i|.这样的写法,但这是不对的,因为times()方法会生成从0到999的数值,如果我们把0作为id值传递给MySQL,数据库则会忽略这个值,转而使用自动生成的主键值。这有可能导致主键冲突。现在该编写性能测试了。同样,我们希望将性能测试与普通测试区分开,所以我们把order_speed_test.rb文件放在了test/performance目录中。由于是对控制器进行测试,这个测试将会基于标准的功能测试。我们从store_controller_test.rb拷贝了一份样板文件,再经过简单的修改,现在它看上去就像这样:require File.dirname(_FILE_) + /./test_helperrequire store_controller# Reraise errors caught by the controller.class StoreController; def rescue_action(e) raise e end; endclass OrderSpeedTest DAVES_DETAILS , :cart = cart assert_redirected_to :action = :index end end assert_equal 100, Order.count assert elapsed_time ruby test/performance/order_speed_test.rb.Finished in 3.840708 seconds.1 tests, 102 assertions, 0 failures, 0 errors我们的性能测试在测试环境下运转良好。但性能问题总是在投入生产运行之后才冒出头来的,所以我们希望能够监控生产环境。还好,在生产环境下我们也有一些工具可以使用。性能监控与评测Profiling and Benchmarking如果你只想知道一个特定方法(或者语句)的性能,你可以使用Rails提供的script/profiler和script/benchmarker脚本。benchmarker脚本可以告诉你一个方法耗费多少时间,profiler则可以告诉你一个方法把时间耗费在什么地方。benchmarker可以给出相对精确的时间值,而profiler则会增加较大的开销它所给出的绝对时间值并不重要,重要的是各个操作所需时间的相对比例。下面来看一个故意营造的例子:假如我们感觉User.encrypted_password()方法耗费了太长的时问,首先需要判断情况是否的确如此。depotruby script/performance/benchmarker User.encrypted_password(secret,salt) user system total real#1 1.650000 0.030000 1.680000 ( 1.761335)哇!区区一个方法就耗费了1.8秒,这确实有点过分!我们再用profiler来一探究竟。depot ruby script/performance/profiler User.encrypted_password(secret,salt)Loading Rails.Using the standard Ruby profiler.% cumulative self self totaltimesecondssecondscallsms/callms/callname78.6558.6358.63158630.0074530.00Integer#times21.3374.5315.9010000000.020.02Math.sin1.2575.460.931930.00930.00Profiler_.start_profile0.0175.470.01120.830.83Symbol#to_sym.0.0075.480.0010.000.00Hash#update这很奇怪:大半的时间似乎都被耗在times()和sin()方法上了。我们来看看源代码:def self.encrypted_password(password, salt) 1000000.times Math.sin(1) string_to_hash = password + salt Digest:SHA1.hexdigest(string_to_hash)end哎哟!顶上的那个循环是我在进行某个手工测试时加上的,以便让程序运行得慢一点,然后我就忘了在部署之前把它去掉。我一定是忘了给自己留下一张小纸条最后,别忘了日志文件。如果你需要了解与时间相关的信息,它们能够提供大量宝贵的信息。14.6 使用Mock对象Using Mock Objects未来的某个时候,我们肯定需要在Depot应用中加上一些代码,以便真正收到来自顾客的付款。所以,假设我们已经搞定了所有那些文案工作,并可以把那些信用卡数字变成我们银行账户上实实在在的钱了。然后,我们创建了一个PaymentGateWay类(位于lib/payment_gateway.rb文件中),它可以和负责处理信用卡的网关交互。最后,我们在StoreController的save_order()这个action中添加下列代码,就可以让Depot应用处理信用卡了。gateway = PaymentGateway.newresponse = gateway.collect(:login = username , :password = password , :amount = cart.total_price, :card_number = order.card_number, :expiration = order.card_expiration, :name = )当collect()方法被调用时,这些信息会通过网络发送给后端的信用卡处理系统。这对于我们的钱包有好处,但对于功能测试就不那么好了,因为这样一来,StoreController就必须能够连接到真正的信用卡处理系统才行。而且,即便网络连接和信用卡系统都不成问题,我们也不能每次运行功能测试就提交一堆信用卡交易事务。所以,我们并不想真的用PaymentGateWay对象进行测试,而是想用一个mock对象来替换它。在mock的帮助下,测试就不必依赖于网络连接,从而确保结果的一致性。还好,Rails让对象的模拟替换也易如反掌。为了在测试环境模拟collect()方法,我们只需要在test/mocks/test目录下创建payment_gateway.rb文件即可。下面就来看看这个名字中的奥妙吧。首先,文件名必须与试图替换的文件名相同。我们可以模拟模型、控制器或者库文件:唯一的要求就是文件名必须相同。第二,看看占位文件的路径,我们将它放在了test/mocks目录的test子目录下,这个子目录用于存放所有用于测试环境的占位文件。如果我们希望在开发环境中替换某些文件,就应该将占位文件放在test/mocks/development目录下。现在,看看占位文件本身。require lib/payment_gatewayclass PaymentGateway # Im a stubbed out method def collect(request) true endend请注意,占位文件实际上加载了原来的PaymentGatway类(通过调用require()方法),然后对其进行了修改,覆盖了其中的collect()方法。也就是说,我们不必模拟出PaymentGateway的所有方法,只需要修改那些运行测试时有必要修改的方法即可。在这里,修改后的collect()方法直接返回一个伪造的应答信息。有了这个文件以后,StoreController就会试用我们模拟出来的PaymentGateway类。之所以如此,是因为Rails把mock路径放在整个搜索路径的最前面,因此它会加载test/mocks/test/payment_gateway.rb而不是lib/payment_gateway.rb。这就是全部了。使用占位程序,我们可以集中注意力来测试最重要的东西,而Rails则让这一切变得无比轻松。Stub vsMock也许你已经注意到了,在前面我们一直用占位(stub)这个词来称呼那些用于模拟真实的类和方法,但Rails却把它们放在test/mocks子目录下。在这一点上,Rails确实有些不太注意用词,这里所说的mock实际上只是占位程序:它们是伪造出的代码块,用在测试中以避免用到某些资源。不过,要是你真的想使用mock对象用于检查“程序如何使用某些对象”的对象Rails也能够满足你。在1.2版本中,Rails包含了Flex Mock /software/flexmock/Jim Weirich的Rubymock对象库。在所有测试中都可以使用这个库,不过你需要明确导入它:require flexmock我们做了了什么What We Just Did我们为Depot应用写了一些测试,但并没有测试到所有东西。然而,我们现在知道,确实可以测试所有东西。实际上,Rails对测试驱动开发提供了绝佳的支持,可以帮助你写出更好的测试。尽早、尽可能频繁地进行测试你可以在投入正式运行之前找到bug你的设计会得到改善,你的Rails应用会因此而感谢你。第3部分 Rails框架Part III The Rails Framework第15章深入RailsRails in Depth Depot项目已经搞定了,现在正是深入研究Rails的好时机。在本书的剩下部分里,我们将一个主题一个主题地或者说,一个模块一个模块地探索Rails。本章将为读者拉开大幕。在这一章里,我们将从较高的层面向读者介绍一些必要的指示:目录结构、配置、环境、支撑类,以及调试提示。不过首先,我们必须回答几个重要的问题15.1 Rails在哪儿So,WheresRails?Rails的一个有趣之处在于它的组成方式。从开发者的角度,你所有的时间都是在跟ActiveRecord或是ActionView这些高层的东西打交道。这里确实有一个名叫Rails的组件,不过它位于所有其他组件之下,默默地安排协调着它们的工作。要是没有Rails组件,就不会有前面这个成功的Depot应用。但与此同时,Rails的底层基础设施只有很少一部分与开发者的日常工作相关,在本章的余下篇幅里,我们就要介绍这些部分。15.2 目录结构DirectoryStructureRails要求一个特定的运行时目录结构。图14.1展示了rails my_app命令生成的顶级目录结构。我们看看每个目录中都放了什么东西(不过不一定按顺序来)。config和db这两个目录值得花一点时间来介绍,所以我们为它们分别准备了一小节的篇幅。顶层目录下还有一个Rakefile文件,你可以使用它来运行测试、创建文档、生成数据库结构,等等。在命令行输入rake -tasks就可以看到完整的任务列表。图15.1 rails my_app命令的结果app/和test/我们的工作大多在app和test这两个目录中进行。应用程序的主要代码都位于app目录(见图15.2)。在稍后详细讨论ActiveRecord、ActionController和ActionView的时候,我们还会深入介绍app目录的内部结构。此外,在第14章“任务T:测试”(第177页)中,我们已经介绍过test目录了。图15.2 app/目录doc/doc目录是用来存放文档RDoc自动生成的文档的。如果你运行rake doc:app命令,在doc/app目录下就会有HTML格式的文档。你可以编辑doc/README_FOR_APP文件,为自己的文档定制一个首页。第175页上的图12.3已经展示出了我们这个“在线商店”应用的文档首页。lib/lib目录用于存放那些不属于模型、视图和控制器的应用代码。譬如说,你可能编写了一个库用于创建PDF格式的收据以便顾客下载1 我们在新版的Pragmatic Programmer在线商店中就是这样做的。,控制器会直接用send_data()方法将收据发送给浏览器。此时,用于创建PDF收据的代码就很自然地位于lib目录下。lib目录还很适合放置模型、视图和控制器之间共享的代码。譬如说你可能需要用一个库来检查信用卡号的校验和,或是执行某些财务计算,或是计算复活节的日期* 译者注:复活节是3月21日或其后月满之后的第一个星期天,因此是需要计算的。简而言之,
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 汽电双托机组主厂房项目施工进度的控制研究
- 新媒体时代下KOL传播对旅游消费者的影响研究-以“淄博烧烤火爆”
- 社交媒体平台个人隐私权保护问题研究
- 五年级英语单元考试复习资料
- 建筑结构设计原理复习资料
- 甘肃省2015年高考数学诊断试题分析
- 项目进度管理工具应用教程
- 小学教学反思与课堂效率提升
- 医院病案统计工作总结报告
- 五年级下册语文生字表及组词练习
- 制作扒鸡的流程
- 居间房屋租赁合同模板
- 2025年度典型火灾案例及消防安全知识专题培训
- 《智慧化工园区系统运维管理要求》
- 外研版九年级英语上册期中综合测试卷含答案
- 肝癌中医治疗新进展
- 药品类体外诊断试剂专项培训课件
- 高中数学新教材选择性必修第二册《4.2等差数列》课件
- 建筑识图与构造 课件 项目8 识读建筑详图
- 《湖南省职工基本医疗保险门诊慢特病基础用药指南(第一批)》
- 四年级上册道德与法治学科质量分析报告
评论
0/150
提交评论