跨越边界: 在集成框架中进行测试,第 1 部分

在 Ruby on Rails 中进行单元测试

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送



级别: 中级

Bruce Tate (bruce.tate@j2life.com), 总裁, RapidRed

2006 年 6 月 27 日

Java ™ 社区在推进自动单元测试方面已经做了一项激动人心的工作。越来越多的开放源码框架支持在构建项目的同时构建自动测试套件。Spring framework、JUnit、TestNG 和其他几个框架的一些或全部灵感都来自自动测试的思想。尽管如此,一些非 Java 语言和框架具有更多的测试动机、更合适的测试工具和更统一的测试视角。通过观察其他框架的测试方式,可以改进 Java 语言中的测试方式,甚至使用更合适的语言来测试 Java 代码。这篇文章是关于在 Ruby on Rails 上进行测试的两篇文章中的第一篇,将介绍 Rails 单元测试的方式。

捕获 bug

我还记得当我第一次得到自动测试的 bug 时的情况。在一次大会上,当我做完叫做 Bitter Java 的演讲之后,Mike Clark(Java 社区的自动测试大师,性能调整工具 JUnitPerf 的作者(请参阅 参考资料),现在是 Ruby on Rails 专家)走近我。Mike 告诉我有一种方法可以通过自动测试改进我的演讲。在那次大会的剩余时间里,我跟着他四处走,看到了我能看到的尽可能多的他的测试会议。我开始使用他推荐的技术,并对把红条(代表测试失败)变成绿条(代表测试通过)上了瘾。自动测试改变了我思考软件开发的方式。

关于本系列

跨越边界 系列中,作者 Bruce Tate 提出了这样一个观点:如今的 Java 程序员可以通过学习其他方法和语言得到很好的其他思路。自从 Java 明显成为所有开发项目的最佳选择以来编程前景已经改变。其他的框架正影响构建 Java 框架的方式,从其他语言学到的概念可以影响您的 Java 编程。您编写的 Python(或 Ruby、Smalltalk ... )代码可以改变您处理 Java 编码的方式。

本系列为您介绍与 Java 开发根本不同,但也可以直接应用于 Java 开发的编程概念和技术。在一些例子中,需要对技术进行集成以利用它。在另外一些例子中,您将能够直接应用这些概念。单独的工具不及其他语言和框架能够影响 Java 社区中的开发人员、框架甚至基本方法的思想那么重要。

Java 社区绝对有自动测试的 bug。坦白地说,我们别无选择。竞争压力迫使许多公司编写越来越多的代码,而测试人员越来越少,同时每个开发人员的又必须有更高的生产率。如果不进行自动测试,得到测试的内容就会更少,面对现代应用程序不断增长的复杂性,较少的测试不是一个可行的选择方案。

在过去十年中,我们已经看到了对测试工具和技术的研究。JUnit 和 TestNG 都是支持自动单元测试的优秀工具,而且由日常的开发人员所驱动。Selenium 是改进集成和功能测试的工具。一套称作敏捷技术 的新开发过程告诉人们要更加重视自动测试,不要太多地依赖正式的设计工具,将它们作为提高质量的惟一工具。Java 社区已经走了很长的路。 (请参阅 参考资料,获得这里讨论的工具与技术的附加信息。)

其他编程社区也有 bug 工具, 其中一些社区使用的自动测试要比 Java 开发人员还有多,他们使用自动测试经验有完全不同的原因:

  • Smalltalk 程序员使用自动测试已经几乎有 30 年的时间了,所以通过动态类型化语言使用的一些技术更加先进。

  • 集成框架的开发人员的优势是了解框架元素的结构和组合。有些框架,例如 Ruby on Rails,能够生成测试用例,而且在默认情况下提供测试特性。

  • 具有高级元编程(metaprogramming)能力的语言,例如 Ruby and Lisp,允许使用其他语言不支持的一些测试技巧,例如更容易访问 mock 对象。

在这一篇和下一篇文章中,将全面理解在 Ruby on Rails 集成开发框架中的测试方式。第 1 部分侧重于测试模型对象,并提供一些从 Rails 获得启发的策略,可以用这些策略使 Java 单元测试更有效。第 2 部分把更多时间花在功能测试和集成测试上。作为 Java 程序员,您对一些概念可能比较熟悉,特别是在测试的时候,而其他一些概念可以拓展您的理解。





回页首


补漏

在这个系列的 前一期 中,了解了动态类型化会带来某些 bug 种类,静态类型化语言将在编译时捕捉到这些 bug。清单 1 的 Ruby 代码片段包含四个不同的 bug,这四个 bug 在运行时之前都不会显露出来:


清单 1. 带 bug 的 Ruby 代码
 
position = "2"               #string, where a number was intended
position = positoin + 4      #position is misspelled, evaluates to 0
puts "The position is:" + 
      position.to_string     #The method should be to_s  

如果编译器能够捕捉 bug,那么这类 bug 解决起来是小菜一碟,但是如果依赖解释器,那么管理这些 bug 就困难得多。为了处理这些微妙的错误,动态语言的用户长期以来一直依赖于自动测试。在进行测试的时候,比起其他语言,动态语言及其集成环境在一般意义和特殊意义上都具有显著的优势:

  • 语言更简洁。测试基本上是脚本编程,许多最好的脚本语言都是动态类型化的。

  • 集成环境支持的假设可以让集成测试更容易,也可能更强大。在 Rails 环境中将看到一些示例。

  • 动态语言允许使用更松散的耦合,使一些测试格式更容易实现。

在了解动态语言开发人员为什么这么热衷于测试之后,现在是构建一个需要一些真正测试的实际应用程序的时候了。





回页首


构建一个快速 Rails 应用程序

为了进展得快些,我采用了一个保存山地摩托车路线数据库的 Rails 应用程序。我将模型的几个测试放在一起。如果想和我一起编写代码,那么所有需要的工具就是一个数据库引擎(我使用的是 MySQL)和 Ruby on Rails 1.1 或更新版本(请参阅 参考资料)。第一步是创建 Rails 项目。在命令提示符下输入 rails trails 命令,清单 2 显示了命令和结果:


清单 2. 构建 Rails 应用程序
 
> rails trails
create  
create  app/controllers
create  app/helpers
create  app/models
create  app/views/layouts

...partial results deleted...

create  test/fixtures
create  test/functional
create  test/integration
create  test/mocks/development
create  test/mocks/test
create  test/unit
create  test/test_helper.rb

...partial results deleted...

create  config/environment.rb
create  config/environments/production.rb
create  config/environments/development.rb
create  config/environments/test.rb

...partial results deleted...

create  log/server.log
create  log/production.log
create  log/development.log
create  log/test.log


Rails 除了生成空项目什么都没做,但是可以看到它正在为您工作。清单 2 创建的目录中包含:

  • 应用程序目录,包括模型、视图和控制器的子目录
  • 单元测试、功能测试和集成测试的测试目录
  • 为测试而明确创建的环境
  • 测试用例结果的日志

因为 Rails 是一个集成环境,所以它可以假设组织测试框架的最佳方式。Rails 也能生成默认测试用例,后面将会看到。

现在要通过迁移创建数据库表,然后用数据库表创建新数据库。请键入 cd trails 进入 trails 目录。然后生成一个模型和迁移(migration),如清单 3 所示:


清单 3. 生成一个模型和迁移
 
> script/generate model Trail
          exists  app/models/
          exists  test/unit/
          exists  test/fixtures/
          create  app/models/trail.rb
          create  test/unit/trail_test.rb
          create  test/fixtures/trails.yml
          create  db/migrate
          create  db/migrate/001_create_trails.rb

注意,如果使用 Windows,就必须在命令前加上 Ruby,这样命令就变成了 ruby script/generate model Trail

如清单 3 所示,Rails 环境不仅创建了模型,还创建了迁移、测试用例和测试 fixture。稍后将看到 fixture 和测试的更多内容。迁移让 Rails 开发人员可以在整个开发过程中处理数据库表中不可避免的更改(请参阅 跨越边界:研究活动记录)。请编辑您的迁移(在 001_create_trails.rb 中),以添加需要的列,如清单 4 所示:


清单 4. 添加列
 
class CreateTrails < ActiveRecord::Migration
   def self.up
      create_table :trails do |t|
         t.column :name, :string
         t.column :description, :text
         t.column :difficulty, :string
   end
 end

   def self.down
      drop_table :trails
   end
end

您需要创建和配置两个数据库:trails_testtrails_development。如果想把这个代码投入生产,那么还需要创建第三个数据库 trails_production,但是现在可以跳过这一步。请用数据库管理器创建数据库。我使用的是 MySQL:


清单 5. 创建开发和测试数据库
mysql> create database trails_development;
Query OK, 1 row affected (0.00 sec)

mysql> create database trails_test;
Query OK, 1 row affected (0.00 sec)

然后编辑 config/database.yml 中的配置,以反映数据库的优先选择。我的配置看起来像这样:


清单 6. 将数据库适配器添加到配置中
development:
   adapter: mysql
   database: trails_development
   username: root
   password:
   host: localhost


test:
   adapter: mysql
   database: trails_test
   username: root
   password:
   host: localhost

现在可以运行迁移,然后把应用程序剩下的部分搭建(scaffold)在一起:


清单 7. 迁移和搭建
> rake migrate

...results deleted...
    
> script/generate scaffold Trail Trails
...results deleted...

    create  app/views/trails

    ...results deleted...

    create  app/views/trails/_form.rhtml
    create  app/views/trails/list.rhtml
    create  app/views/trails/show.rhtml
    create  app/views/trails/new.rhtml
    create  app/views/trails/edit.rhtml
    create  app/controllers/trails_controller.rb

    create  test/functional/trails_controller_test.rb

    ...results deleted...
 

再次注意,Rails 已经为您创建了测试用例。框架不仅为这个简单的小程序生成了视图和控制器,而且还生成了有助于测试用户界面的功能性测试。





回页首


对 Rails 应用程序进行单元测试

现在是运行一些测试的时候了。请看第一个测试,它已经在 test/unit/trail_test.rb 中写好了:


清单 8. 第一个测试
require File.dirname(__FILE__) + '/../test_helper'

class TrailTest < Test::Unit::TestCase
   fixtures :trails

   # Replace this with your real tests.
   def test_truth
      assert true
   end
end


确实,这个测试用例算不了什么,但您可以从中看出如何构架测试代码,而且自己的测试用例的模板也已经就位。请运行测试,如清单 9 所示(包括结果):


清单 9. 运行第一个测试
> ruby test/unit/trail_test.rb 
    Loaded suite test/unit/trail_test
    Started
    EE
    Finished in 0.027314 seconds.

      1) Error:
    test_truth(TrailTest):
    ActiveRecord::StatementInvalid: Mysql::Error: #42S02Table 
      'trails_test.trails' doesn't exist: DELETE FROM trails

...results deleted...


测试用例失败,但是请看输出。第一行执行测试。第三行 EE 显示测试的结果。如果测试用例通过,会得到 “.” 字符。如果测试用例产生错误,会看到 E。如果某个断言不是 true,那么将看到 F。接下来,可以看到所请求的全部测试都将完成,以及完成这些测试需要的时间。最后,将看到每个失败的详细原因。在这个示例中没有表,这是有一定原因的,因为在测试数据库中还没有创建任何表。通过将开发方案复制到测试环境,再重新运行测试,可以修复错误,如清单 10 所示:


清单 10. 复制方案,重新运行测试
> rake clone_schema_to_test          (in /Users/batate/rails/trails)
> ruby test/unit/trail_test.rb 
    Loaded suite test/unit/trail_test
    Started
    .
    Finished in 0.038578 seconds.

    1 tests, 1 assertions, 0 failures, 0 errors

这样更好。但是测试还是太简单,所以是构建一个真正的测试用例的时候了。请添加下面这个新测试用例 test_truth,如清单 11 所示:


清单 11. 添加测试用例
    def test_truth
      assert true
    end

    def test_new
      trails = Trail.find_all
      Trail.new do |trail|
        trail.name = "Barton Creek"
        trail.description = "A little water in the Spring. You'll get wet."
        trail.difficulty = "medium"
        trail.save
      end
      bc = Trail.find_by_name("Barton Creek")
      assert_equal "medium", bc.difficulty
      assert_equal trails.size + 1, Trail.find_all.size

    end

这个代码惊人的紧凑。只需要键入上述代码以及两个断言,就可以操纵持久模型。这种经济的投入正是脚本语言在其他环境中如此流行的原因。测试也是需要经济投入的地方。

现在可以运行测试用例,您将看到两个新断言显示在测试报告中。使用 Ruby 时,只需保存并编译测试即可。清单 12 显示了测试运行的结果:


清单 12. 测试结果
> ruby test/unit/trail_test.rb 
    Loaded suite test/unit/trail_test
    Started
    .
    Finished in 0.038578 seconds.

    1 tests, 1 assertions, 0 failures, 0 errors
    bruce-tates-computer:~/rails/trails batate$ ruby test/unit/trail_test.rb 
    Loaded suite test/unit/trail_test
    Started
    ..
    Finished in 0.182043 seconds.

    2 tests, 3 assertions, 0 failures, 0 errors

Fixture 和回滚

Java mock 对象

在解决测试数据库支持代码的困扰时,Java 开发人员经常使用 mock 对象而不是实际的数据库代码。Mock 对象设置起来比较难,通常难于理解,而且对于在数据库环境中工作的代码,也无法提供良好的理解。Ruby on Rails 支持不同的方式。

有三个问题影响了对数据库支持代码的测试。它们都与两个特性有关:性能和重复性。与内存中的操作相比较,数据库调用的性能是非常低的。如果测试运行需要太长时间,那么您可能就不想运行它们了。另一个问题是一个测试用例对另一个测试用例的影响。因为数据库调用在性质上是持续的,所以要把一个测试在数据库中的变化与另一个数据库中的隔离开。最后的问题是前两个问题的组合。为了让数据库测试用例可重复而增加设置和拆卸的负担时(为每个新的测试用例添加记录、运行测试并删除这些记录),带来的开销可能是让人无法接受的。与这种开销相比,测试用例开销简直是小巫见大巫。

Ruby on Rails 用 fixture 和事务回滚来帮助解决这些问题。在 Rails 中,一个 fixture 就是一个包含测试用例数据的文件。在创建这个简单应用程序时,同时还创建了一个开发数据库和一个测试数据库。创建开发数据库是很正常的;但是您可能不想让生产代码和开发环境共享同一个数据库。而创建测试数据库因为另一个原因也很重要。每个测试都在测试用例开始时装入 fixture 中的测试数据。然后,测试用例对数据库进行修改,并测试这些修改的结果。最后,Rails 回滚这些变化,将数据库返回到测试方法运行之前的状态。

现在要制作一个测试 fixture 并为它编写一个测试。请编辑 test/fixtures/trails.yml 文件,添加一个记录,如清单 13 所示:


清单 13. 添加记录
    first:
      id: 1
      name: "Emma Long"
      description: "A real bike breaker."
      difficulty: "hard"
    another:
      id: 2
      name: "Bear Creek"
      description: "Too many downed trees."
      difficulty: "easy"

清单 13 使用叫做 YAML 的语言,这个语言描述结构化的数据(请参阅 参考资料)。此文件对空格很敏感,所以该当用空格代替制表符并完全按原样键入数据项时,请确保删除了所有尾部空格。

同样,还要把这个测试用例添加到 trails_test.rb 中:

    def test_find
      assert_equal "Emma Long", Trail.find(1).name
      assert_equal "easy", Trail.find(2).difficulty
    end

同样,可以用 5 个 passing 断言运行这些测试。如果您愿意,还可以按名称引用每个 fixture。例如,要根据名为 first 的 fixture 来创建对象,可以使用 Ruby 代码 trails[:first]。让 fixture 对所有测试用例或只对需要它们的测试用例可用,这极大地简化了创建或毁坏数据库数据所需要的代码。





回页首


在 Java 编程中测试

知道了测试在其他语言中如何发生,就可以改进在 Java 平台上进行测试的方式。具体地说,使用这些想法中的一项或多项可以对测试产生显著而直接的影响:

  • 可以把测试用例的生成添加到任何现有代码生成当中。Ruby on Rails 通过在默认情况下创建一些简单的测试用例来取得了巨大优势,您也可以这么做。

  • 可以用事务-回滚技术让数据支持的测试运行得更快。Spring 框架有一些现有的拦截器,可以让这项技术易于使用。

  • 实际上可以用动态语言驱动测试。Jython、Ruby 和 Groovy 是三个实际可能。

如果觉得愿意采用其他语言进行测试,那么可以使用某种 JVM 语言,例如 JRuby(请参阅 参考资料)。JRuby 还没有高级到可以运行 Ruby on Rails,但是它是 Java 应用程序卓越的测试平台。只是作为尝试,JRuby 的开发人员 Charles O'Nutter 提供了以下测试 EJB 的示例:


清单 14. 用 JRuby 测试 EJB 组件
    require 'test/unit'
    require 'java'

    include_class "my.pkg.EJBHomeFactory"

    class TestMyBean < Test::Unit::TestCase 
      def test_finder
        wh = EJBHomeFactory.widget_home
        w = wh.find_by_color("blue")
        assert_not_nil(w)
      end

      def test_widget
        wh = EJBHomeFactory.widget_home
        w = wh.find_by_name ("superWidget")

        assert_equal("blue", w.color)
        assert_equal(14, w.id)
      end
    end

可以看到,用 Ruby 编写执行 Java 代码的测试用例实际上非常容易。在这个示例中,Ruby 代码发现一个 EJB 组件,并为用户返回的 bean 提供了一些断言。测试用例当然比多数 Java 测试都容易,使用 Ruby 编写测试用例是一个获得更高的生产率和速率的一种好方法。我还看到针对 Jython 或 Groovy 的类似策略(请参阅 参考资料)。

第 2 部分将进一步深入查看 Rails 的测试,包括运行更高层次测试(叫做功能测试和集成测试)的代码。



参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 超越 Java(O'Reilly,2005):本文作者编写的一本书,讲述 Java 语言的提高和稳定发展,以及在某些方面能够挑战 Java 平台的技术。

  • Java To Ruby: Things Your Manager Should Know (Pragmatic Bookshelf,2006):本文作者编写的一本书,讲述何时何处从 Java 编程转变到 Ruby on Rails 以及如何完成这种转变。

  • Programming Ruby(Dave Thomas et al.,Pragmatic Bookshelf,2005):一本关于 Ruby 编程的受欢迎的书。

  • Running your Rails App Headless”(Mike Clark's Weblog,2006 年 4 月):Mike Clark 介绍了 Ruby on Rails 的集成测试框架。

  • YAML fixtures:学习关于 Rails fixture 的更多内容。

  • YAML:机器可以解析的数据格式,专门对数据序列化、配置设置、日志文章、Internet 消息传递和过滤进行优化。

  • Demystifying Extreme Programming (developerWorks):XP 是软件开发最流行的敏捷方式。

  • al.lang.jre (developerWorks):这一系列介绍了 Java 运行时环境的替代语言(包括关于 JRuby、Jython 和 Groovy)的文章。

  • 实战 Groovy: 用 Groovy 更迅速地对 Java 代码进行单元测试”(Andrew Glover,developerWorks,2004 年 11 月):学习用 Groovy 和 JUnit 对 Java 代码进行单元测试的简单策略。

  • Introduction to the Spring framework”(Rod Johnson,TheServerSide,2005 年 5 月):不要错过这篇文章的这一部分,它介绍了如何在成功完成测试用例时回滚事务。


获得产品和技术
  • Ruby on Rails:下载开放源码的 Ruby on Rails Web 框架。

  • Ruby:从 Ruby 项目的 Web 站点得到它。

  • JUnit:开始了 Java 平台自动测试热浪的 Java 测试框架。

  • TestNG:Java 开发的下一代测试框架。

  • JRuby:运行在 JVM 中的 Ruby 实现。

  • Selenium:用于 Web 应用程序的集成测试框架。

  • JUnitPerf:用来测试 JUnit 测试中的性能和伸缩性的 JUnit 测试修饰器集。


关于作者

Bruce Tate 居住在德克萨斯州的首府奥斯汀,他是一位父亲,同时也是山地车手和皮艇手。他是三本 Java 畅销书的作者,包括荣获 Jolt 大奖的 Better, Faster, Lighter Java。最近他又出版了 Beyond Java 一书。他在 IBM 工作了 13 年,现在是 RapidRed 顾问公司 的创始人,在这里他潜心研究基于 Java 技术和 Ruby on Rails 的轻量级开发策略和架构

::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


订阅 substack 体验古早写作:


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::