UML软件工程组织

 

 

在集成框架中进行测试,第 1 部分
 
作者:Bruce Tate 出处:IBM
 
本文内容包括:
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 测试修饰器集。
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号