AICon全球人工智能与机器学习技术大会周四开幕,点击查看完整日程>> 了解详情
写点什么

软件开发地基

  • 2011 年 9 月 10 日
  • 本文字数:10064 字

    阅读完需:约 33 分钟

自动化脚本之于软件开发,犹如地基之于建筑。

在软件开发过程中,缺乏一个好的自动化脚本,与之相伴的往往是日常的开发工作举步维艰:

  • 只有少数人能够把整个软件构建起来,因为构建所需的那些东西不太容易弄全。
  • 为了能在自己机器上写代码,开发人员要花大量时间把工程在 IDE 上配出来。
  • 提交代码之前,开发人员总是忘了在验证。

在本文中, 我们将以一个 Java 的 web 项目为例,展示一个好的“地基”应具备的一些基本素质。在这里,用做自动化的工具是 buildr

buildr 是一种构建工具,它专为基于 Java 的应用而设计,也包括了对 Scala、Groovy 等 JVM 语言的支持。相比于 ant 和 maven 这些 Java 世界的“老人”,buildr 算是小字辈,也正是因为年轻,它有着“老人”们不具备的优势:

  • 相比于 ant,遵循着 Convention over Configuration 原则的 buildr,让“编译、测试、打包”之类简单的事做起来很容易。
  • 相比于 maven,我们无需理解强大且复杂的模型,而采用 Ruby/Rake 作为脚本的基础,也让我们可以定制属于自己的脚本。

简而言之,它满足了我们选择工具的基本原则:“易者易为,难者可为”。

请注意:下面所有的内容并不只是 buildr 的独家专利,而是每个构建工程都应该具备的,差异只在于,选择不同的工具,实现的难度略有差异而已。

易者易为

让我们从一个简单的 buildfile——buildr 的脚本——起步:

复制代码
GUAVA <b>=</b> 'com.google.guava:guava:jar:r09'
define 'killer' <b>do</b>
project.version <b>=</b> '0.0.1'
define 'domain' <b>do</b>
compile.with GUAVA
package <b>:jar</b>
<b>end</b>
define 'web' <b>do</b>
DOMAIN <b>=</b> project('killer:domain').packages
compile.with DOMAIN
package(<b>:war</b>).with(<b>:libs</b>=>DOMAIN)
<b>end</b>
<b>end</b>

我们先来看看从这个简单的 buildfile 中,我们可以得到什么。

分模块项目

这个项目里有两个子项目:domain 和 web。从架构的角度来看,一个项目从一开始就划分出这样的模块是有好处的:

  • 给未来扩展留下接口,比如要提供一个 Web Service,可以从 domain 部分开始即可。
  • 给开发人员一个好的规划,有助于引导他们思考程序的模块化,降低代码的耦合度。

使用 buildr 划分模块是非常简单的,只要在 buildfile 里声明模块,项目的根目录下同名的子目录就是对应的模块。

文件布局

虽然在 buildfile 没有直接体现出来,但这里有个缺省的文件布局。一个统一的规则省去了我们从头规划的苦恼。遵循缺省的布局规则,buildr 自己就会找到相应的文件,进行处理。

这个布局规则实际上就是 Maven 的布局规则,如图所示,两个子项目都拥有自己的目录,其结构基本一致:

  • src/main/java,源代码文件目录
  • src/main/resources,资源文件目录
  • src/test/java,测试代码目录
  • src/main/webapp,web 相关文件目录

此外,这里还有稍后会提及的:

  • profiles.yaml,环境相关的配置
  • tasks,自定义任务的目录

这就是所谓的 Convention over Configuration。当然,buildr 是支持自定义文件布局的,详情请参见文档

基本命令

有了这个基本的 buildfile,我们就可以开展日常的工作了。buildr 自身支持很多命令,比如:

  • buildr compile,编译项目
  • buildr package,项目打包
  • buildr test,运行测试

想要了解更多的命令,可以运行下面的命令:

复制代码
buildr -T

测试

在这个不测试都不好意思自称程序员的年代,测试,尤其实现级别的测试,诸如单元测试、集成测试,已经成了程序员的常规武器。

诚如上面所见,src/test/java 就是我们的测试文件存放的目录。对于 Java 项目,JUnit 是缺省的配置,只要在这个目录下的 Java 类继承自 junit.framework.TestCase(JUnit 3),或是,在类上标记了 org.junit.runner.RunWith,抑或在方法上标记了 org.junit.Test(JUnit 4)。buildr 就会找到它们,并帮我们料理好编译运行等事宜。约定的力量让我们无需操心这一切。

依赖管理

依赖管理一直是一项令人头疼的问题,也是让许多开发人员搭建纠结于开发环境搭建的一个重要因素。

各种语言的社区分别给出了自己的依赖管理解决方案,对于 Java 社区而言,一种比较成熟的解决方案来自于 Maven。它按照一定规则建立起一个庞大的中央仓库,成熟的 Java 库都会在其中有一席之地。

于是,很多新兴的构建工具都会建立在这个仓库的基础之上,buildr 也不例外。在前面的例子里面,domain 依赖了 Guava 这个库。当我们开始构建应用时,buildr 会自动从中央仓库下载我们缺失的依赖。

不仅仅是依赖,我们还可以拿到对应的文档和源码:

  • buildr artifacts,下载依赖
  • buildr artifacts:javadoc,下载 javadoc
  • buildr artifacts:sources,下载源码

如果不知道如何在 buildfile 里编写依赖,那 mvnrepository.com 是个不错的去处,那里针对不同的构建工具都给出了相应的依赖写法。

与 IDE 集成

除非这个工程是用 IDE 创建出来的,否则把工程集成到一个 IDE 里通常要花费很大的力气。所幸,buildr 替我们把这些工作做好了。我们只要键入一个命令即可,比如与 IntelliJ IDEA 集成,运行下面的命令:

复制代码
buildr idea

它会生成一个 IDEA 的工程文件,我们要做的只是用 IDEA 打开它。同样的,还有一个为 Eclipse 准备的命令:

复制代码
buildr eclipse

不知道你是否有这样的经验,初到一个项目组,开始为一个项目贡献代码之前,先需要花几天时间,在不同的人的协助之下把环境搭出来,为的只是在自己的机器上能够把应用构建出来。

而现在,有了这样的自动化脚本,一个项目组新人的行为模式就变成了:

  • 初入一个项目组,他从源码管理系统上得到检出代码库。
  • 调用 buildr artifacts,其所依赖的文件就会下载到本机。
  • 调用 buildr idea:generate(或是 buildr eclipse),生成 IDE 工程。
  • 打开工程,开始干活。

迄今为止,我们看到的只是一个基本的 buildfile,这些命令也是 buildr 内置的一些基本能力,也就是所谓的“易者易为”。

难者可为

接下来,我们将超越基础,做一些“难者可为”的东西。

不同的环境

在实际的开发中,我们经常会遇到不同的环境,比如,在开发环境下,数据库和应用服务器是在同一台机器上,而在生产环境下,二者会部署到机器上。这里所列举的配置功能,只是最简单的例子,而实际情况下,不同的环境下,会有各种差异,甚至需要执行不同的代码。

一种解决方案是为数不少的“直觉式”设计采用的方案,在代码里根据条件进行判断,可想而知,无处不在的 if…else 很快就会把代码变成一团浆糊,更糟糕的是,这些信息散落在各处。

另一种方案是在自动化脚本中支持,buildr 让这个工作变得很简单。

配置信息

使用 buildr,配置信息可以放到一个名为 profile.yaml 的文件里,下面是一个例子:

复制代码
<b>development:</b>
<b>db:</b>
<b>url:</b> jdbc:mysql://localhost/killer_development
<b>driver:</b> com.mysql.jdbc.Driver
<b>username:</b> root
<b>password:</b>
<b>jar:</b> mysql:mysql-connector-java:jar:5.1.14
<b>production:</b>
<b>db:</b>
<b>url:</b> jdbc:mysql://deployment.env/killer_production
<b>driver:</b> com.mysql.jdbc.Driver
<b>username:</b> root
<b>password:</b> ki1152
<b>jar:</b> mysql:mysql-connector-java:jar:5.1.14

我们看到,针对不同的环境,有不同的数据库配置,在 buildfile 里可以这样引用这些配置:

复制代码
db_settings <b>=</b> <b>Buildr</b>.settings.profile['db']

随后,我们就可以使用这个配置,比如生成一个配置文件:

复制代码
task <b>:config</b> <b>do</b>
CONFIG_PROPERTIES = <<EOF
jdbc.driverClassName= #{db_settings['driver']}
jdbc.url=#{db_settings['url']}
jdbc.username=#{db_settings['username']}
jdbc.password=#{db_settings['password']}
EOF
<b>File</b>.open('config.properties'), "w") <b>do </b>|f|
f.write config
<b>end</b>
<b>end</b>

有了这样的基础,只要我们指定不同的环境就会产生不同的配置。

系统组件

在 buildr 里,有一个叫做 ENV[‘BUILDR_ENV’] 的变量,这是 buildr 内置的一个变量,通过它,我们可以获得当前环境的名字,在这个例子里,它可以是 development 或是 production。

有了这个变量,我们可以进行更加深度的配置,比如,在测试环境下,我们可以采用一些假的实现,让整个系统运行的更快。

下面是一个例子,有一个搜索组件的配置文件,在生产环境下,它会采用真实的搜索引擎实现,而在开发环境时,它只是一个简单内存实现。我们把不同环境的实现放到不同的配置文件里。

复制代码
<<b>beans</b> xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<<b>bean</b> id="searcher" class="com.killer.SuperSearcher"/>
</<b>beans</b>>

(searcher.production.xml)

复制代码
<<b>beans</b> xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<<b>bean</b> id="searcher" class="com.killer.InMemorySearcher"/>
</<b>beans</b>>

(searcher.development.xml)

要使用这个组件只要引用 searcher.xml,把 searcher 拿过来用就好了:

复制代码
<<b>import</b> resource="searcher-context.xml"/>
...
<<b>property</b> name="searcher" ref="searcher"/>

接下来,用一个 task 就可以处理这些差异:

复制代码
task <b>:searcher</b> <b>do</b>
cp "searcher.#{ENV['BUILDR_ENV']}.xml", "searcher.xml"
<b>end</b>

其实,实现这一点真正困难的并不在于配置文件,而在于这些组件的设计。只有识别出这些组件,把它们独立出来,才会可能根据不同的环境进行配置。没有恰当的抽象,自动化脚本也无能为力。

有了上面准备的基础,我们可以通“-e”这个选项,在命令行中指定我们的环境。下面的命令会让我们得到 production 的配置,不论是配置信息,还是系统组件。:

复制代码
buildr -e production

静态检查

在团队开发中,统一的代码风格很重要。即便我们很认真,疏忽也在所难免。Java 世界里, checkstyle 常常扮演了代码检察官角色。

坏消息是,buildr 并不提供对 checkstyle 的直接支持。好消息是,buildr 可以集成 ant task,所以,我们通过 ant task 集成 checkstyle。

复制代码
CHECKSTYLE <b>=</b> transitive('checkstyle:checkstyle:jar:4.4')
task <b>:checkstyle</b> <b>do</b>
<b>begin</b>
ant('checkstyle') <b>do </b>|ant|
rm_rf 'reports/checkstyle_report.xml'
mkdir_p 'reports'
ant.taskdef <b>:resource</b>=>"checkstyletask.properties",
<b>:classpath</b>=><b>Buildr</b>.artifacts(CHECKSTYLE)
.each(<b>&:invoke</b>).map(<b>&:name</b>).join(<b>File</b>::PATH_SEPARATOR)
ant.checkstyle <b>:config</b>=>"tasks/checkstyle_checks.xml",
<b>:maxWarnings</b>=>0 <b>do</b>
ant.formatter <b>:type</b>=>'plain'
ant.formatter <b>:type</b>=>'xml',
<b>:toFile</b>=>"reports/checkstyle_report.xml"
ant.property <b>:key</b>=>'javadoc.method.scope', <b>:value</b>=>'public'
ant.property <b>:key</b>=>'javadoc.type.scope', <b>:value</b>=>'package'
ant.property <b>:key</b>=>'javadoc.var.scope', <b>:value</b>=>'package'
ant.property <b>:key</b>=>'javadoc.lazy', <b>:value</b>=>'false'
ant.property <b>:key</b>=>'checkstyle.cache.file',
<b>:value</b>=>'target/checkstyle.cache.src'
ant.property <b>:key</b>=>'checkstyle.header.file',
<b>:value</b>=>'buildconf/LICENSE.txt'
ant.fileset <b>:dir</b>=>"domain/src/main/java",
<b>:includes</b>=>'**/*.java',
ant.fileset <b>:dir</b>=>"web/src/main/java",
<b>:includes</b>=>'**/*.java'
<b>end</b>
ant.xslt <b>:in</b>=>'reports/checkstyle_report.xml',
<b>:out</b>=>'reports/checkstyle_report.html',
<b>:style</b>=>'tasks/checkstyle-noframes.xsl'
<b>end</b>
<b>end</b>
<b>end</b>

这里的绝大多数内容 checkstyle 的 ant task 介绍都可以很容易了解到,唯一需要注意的点在于 checkstyle 任务的 maxWarnings 属性。

maxWarning 表示可以容忍的最大警告数,也就是说,当 checkstyle 检查的警告数大于这个数字时,构建就会失败。在这里,我们将其设置为 0,换句话说,我们不接受任何警告。

在实践中,对低级错误容忍度越低的团队,代码质量往往也会越高。

像这种独立性很强的任务,我们通常会把放到一个独立的文件中。在《文件布局》一节,我们曾提及 tasks 目录,其中存放的就是自定义任务,buildr 在启动时会自动加载该目录下的这些任务。

如果熟悉 rake,你会发现上面的 task 就是一个标准的 rake task。实际上,buildr 是集了多个构建工具的本领于一身的。把上面的代码放到 checkstyle.rake,然后放到 tasks 目录下。运行下面的命令,就可以对代码执行静态检查:

复制代码
buildr checkstyle

测试覆盖率

测试覆盖率这东西,高了不代表有多好,但低了肯定是有问题。

当我们把测试覆盖率限制为 100%,其结果是全部内容都会有测试覆盖。坚持这样的标准,在做重构的时候,可以比较放心。

工具支持

buildr 缺省支持一个测试覆盖率工具——cobertura,所以,一切做起来很简单。

复制代码
<b>require</b> 'buildr/java/cobertura'
cobertura.check.branch_rate <b>=</b> 100
cobertura.check.line_rate <b>=</b> 100

(buildfile)

在命令行里运行如下命令就可以运行测试,进行检查,生成报告:

复制代码
buildr cobertura:html cobertura:check

如果覆盖率不达标,构建就会失败。这时,我们可以通过它生成的报告来看,到底是哪里没有覆盖。报告位于

复制代码
reports/cobertura/html/index.html

例外情况

真的是所有代码都会在这个 100% 的监控之下吗?不一定。有一些代码只是为了调用一个特定的 API,这样的代码是否 100% 意义不大,这样的测试本质上是在 API 写测试,而非自己的代码逻辑。所以,这种代码会被排除在外。

复制代码
cobertura.exclude /.*.integration.*/

但是,这样的代码同样会有相应的集成测试,只是不在单元测试的层面。一个基本的原则是,被排除的代码要尽可能少。

提交脚本

作为一个专业程序员,我们应该保证自己提交的代码不会对代码库造成破坏。但怎么才算是不破坏呢?也许我们要编译、运行单元测试、打包,也许我们还要进行静态检查,查看测试覆盖率,也许还要进行更多的检查,运行各种各样的测试。有了前面的基础,我们可以做一个 task,把所有这些都依赖上去。

好,有了这个 task,要提交代码,我们会怎么做呢?

  • 如果用的 git 这样的分布式版本管理系统,现在本地提交代码。
    • 如果有新增文件,需要把文件纳入版本控制之中。
  • 从远端代码库更新出代码,如果有需要冲突,需要合并代码
  • 运行 task,进行检查和测试
  • 运行成功,则提交代码

显然,这是一个说多不多,说少也不少的操作,对于这种繁琐的操作,如果能够自动化,自然是最好的选择。下面就是一段这样的脚本,这里用的到版本控制系统是 git。

首先看到的是版本控制系统部分:

复制代码
namespace <b>:git</b> <b>do</b>
<b>def</b> <b>sys</b>(cmd)
puts cmd
<b>raise</b> "System execution failed!" <b>unless</b> system(cmd)
<b>end</b>
<b>def</b> <b>get_info</b>(name, prompt)
<b>begin</b>
value <b>=</b> <b>File</b>.open(".#{name}") { |f| f.read }.strip
<b>rescue</b>
value <b>=</b> ""
<b>end</b>
prompt <b>+=</b> " (#{value})" <b>unless</b> value.empty?
new_value <b>=</b> <b>Readline</b>::readline("[#{prompt}]: ").strip
value <b>=</b> new_value <b>unless</b> new_value.empty?
<b>File</b>.open(".#{name}", "w") { |f| f.write(value) }
value
<b>end</b>
<b>def</b> <b>commit</b>
dev_name <b>=</b> get_info('pair', 'Pair')
story_number <b>=</b> get_info('story', 'Story #')
comment <b>=</b> get_info('comment', 'Comment')
commit_cmd <b>=</b> %Q(git commit -am "#{dev_name} - KILLER-#{story_number} - #{comment}")
sys(commit_cmd)
<b>end</b>
<b>def</b> <b>add</b>(files)
files.each { |file| sys("git add #{file}") }
<b>end</b>
<b>def</b> <b>add_files</b>(files)
puts "Add the following new files:\n#\t#{files.join("\n#\t")}\n"
reply <b>=</b> <b>Readline</b>::readline("[Y/N]")
<b>return</b> add(files) <b>if</b> reply.strip.downcase.start_with?('y')
<b>raise</b> 'new files should be added before commit'
<b>end</b>
task <b>:add</b> <b>do</b>
files <b>=</b> `git status -s | awk '/\\?\\?/ {print $2}'`
files <b>=</b> files.split("\n")
add_files(files) <b>if</b> files.size <b>></b> 0
<b>end</b>
<b>def</b> <b>nothing_to_commit?</b>
`git status -s`.empty?
<b>end</b>
task <b>:pull</b> <b>do</b>
sys('git pull')
<b>end</b>
task <b>:push</b> <b>do</b>
sys('git push')
<b>end</b>
task <b>:status</b> <b>do</b>
sys('git status')
<b>end</b>
task <b>:commit</b> => <b>:add</b> <b>do</b>
commit <b>unless</b> nothing_to_commit?
<b>end</b>
<b>end</b>

这里,我们看到了 git 的一些基础操作,比如 pull、push、commit 等等,并且,我们在基础操作之上进行了封装,让使用更便捷。比如:

  • 在提交之前,根据当前的状态,确定是否要添加文件。
  • 如果代码曾经手工提交过,则继续。
  • 提示之前,提示开发人员填写名字、开发的 Story 号,以及相应的注释。

之所以在这里还要填写名字,而不仅仅是利用 git 自有的用户名,因为在开发的过程中,我们可能是结对开发,显然一个用户名是不够的。

有了这个基础,我们继续向前,创建一个 task,把提交之前要做的事情都放在这里:

复制代码
task <b>:commit_build</b> => [<b>:clean</b>, <b>:artifacts</b>, <b>:checkstyle</b>,
"cobertura:html", "killer:domain:cobertura:check",
"killer:web:cobertura:check"]

以上面这个 task 为例,实际上包括编译、测试、静态检查和测试覆盖率检查,我们可以根据自己的需要加入更多的东西。

接下来,把版本控制部分结合进去,就是我们的提交脚本了。

复制代码
task <b>:commit</b> => ["git:commit", "git:pull",
<b>:commit_build</b>, "git:push", "git:status"]

有了这个基础,我们就可以提交代码了,而不用担心忘记了什么:

复制代码
buildr commit

在执行过程中,任何一步失败都会让整个提交过程停下来,比如,当 pull 代码产生冲突,或是运行测试之后,push 代码时,我们发现又有人提交,提交过程就会停止,错误的代码是不会提交的。

数据库迁移

数据库脚本通常不像代码那样受人重视,但在实际的发布过程中,它常常把我们弄得焦头烂额。

dbdeploy 为我们提供了一种管理数据迁移的方式,buildr 没有提供对 dbdeploy 直接的支持,但如我们之前所见,ant task 可以帮我们打造一条直通之路。

复制代码
namespace <b>:db</b> <b>do</b>
DB_SETTINGS <b>=</b> <b>Buildr</b>.settings.profile['db']
<b>def</b> <b>dbdeploy</b>(options)
ant('dbdeploy') <b>do </b>|ant|
ant.taskdef(<b>:name</b> => "dbdeploy",
<b>:classname</b> => "com.dbdeploy.AntTarget",
<b>:classpath</b>=>DBDEPLOY_CLASSPATH)
ant.dbdeploy(options)
<b>end</b>
<b>end</b>
task(<b>:migrate</b>) <b>do </b>|t, args|
dbdeploy(<b>:driver</b> => <b>DB_SETTINGS</b>['driver'],
<b>:url</b> => <b>DB_SETTINGS</b>['url'],
<b>:userid</b> => <b>DB_SETTINGS</b>['username'],
<b>:password</b> => <b>DB_SETTINGS</b>['password'],
<b>:dir</b> => "#{db_script_dir}/migration")
<b>end</b>
<b>end</b>

这里,我们用到之前提及的管理配置的方式,配置信息存放在 profile.yaml 里。另外,数据库迁移文件存放在数据库脚本目录(db_script_dir)的 migration 子目录下。

有了这段 task,我们就可以进行数据库迁移了:

复制代码
buildr db:migrate

集成 Web 服务器

对于一个 Web 应用而言,仅仅测试打包还是不够的,我们还要把它部署到 Web 服务器上。这是一个基础任务,有了它,我们就可以使用一些 web 测试框架,对我们的系统进行验收测试。

通常在开发过程中,我们会选择一个部署起来很容易的 Web 服务器,在 Java 的世界里,jetty 往往扮演着这样的角色。当然,这样做的前提是,我们编写的是一个标准的 Java WebApp,没有用到特定于某个具体应用服务器的 API,换句话说,我们的 Web 应用是跨应用服务器的,事实上,这种做法是值得鼓励的。

使用特定于具体应用服务器的 API,其结果只能是与这个服务器产生耦合,而通常提供这个 API 的服务器对于开发并不那么友好,在其上部署的周期会很长,这无疑会大大降低开发效率。

一个好消息是,buildr 有很好的对 jetty 的集成。

复制代码
<b>require</b> 'buildr/jetty'
define 'killer' <b>do</b>
define 'web' <b>do</b>
<b></b>
task("jetty-deploy"=>[package(<b>:war</b>), jetty.use]) <b>do </b>|task|
jetty.deploy("http://localhost:8080", task.prerequisites.first)
<b>end</b>
task("jetty"=> ["jetty-deploy"]) <b>do </b>|task|
<b>Readline</b>::readline('[Type ENTER to stop Jetty]')
<b>end</b>
<b>end</b>
<b>end</b>

这里,我们启动了一个 jetty,启动之前,我们需要确保已经有了可以部署的 WAR,所以这个部署任务要依赖于 package(:war)。最后的 jetty task 给了我们一个手工停止 Jetty 的机会。我们可以这样将启动它:

复制代码
buildr killer:web:jetty

验收测试

写好代码,完成部署,还少不了验收测试。

对于一个 Web 项目,我们可能会考虑采用 selenium 或是 waitr 之类的自动化测试框架进行测试。不过,通常直接用这些框架去写通常会把业务需求和测试实现细节混合起来,结果往往不如我们预期。

Cucumber 的出现给了我们一种分离业务需求和实现细节的方式。对我们这个项目而言,不好的消息是,想到 Cucumber,出现在我们脑子里的是 Ruby 语言,好消息是 buildr 就是 Ruby 语言。下面就是一段脚本,把它放到 buildfile 里,就可以运行 acceptance/features 目录下那堆 feature 文件了。

复制代码
<b>require</b> 'cucumber'
<b>require</b> 'cucumber/rake/task'
define 'killer' <b>do</b>
define 'web' <b>do</b>
<b></b>
<b>Cucumber</b>::<b>Rake</b>::<b>Task</b>.<b>new</b>(<b>:acceptance</b> => "jetty-deploy") <b>do </b>|t|
t.cucumber_opts <b>=</b> ["acceptance/features"]
<b>end</b>
<b>end</b>
<b>end</b>

有了它,我们就可以运行我们的验收测试了:

复制代码
buildr killer:web:acceptance

路还长

至此,我们已经展示了一个基本的自动化脚本。正如我们所见,作为地基的自动化脚本,仅仅有编译、测试、打包这些基本操作是远远不够的。即便是这里所列出的这些,也不过是一些通用的任务,每个项目都应该把项目内的繁琐操作自动化起来,比如,在我参与的实际项目中,我们会把部署到用户验收测试(User Acceptance Testing,简称 UAT)环境的过程自动化起来。

不过,除了脚本自身,为了让地基真正的发挥作用,还需要有一些实践与之配合,比如有了持续集成,提交脚本才好发挥最大的威力,比如有了合适的测试策略,开发人员才能在一个合理的时间内完成上的本地构建。

软件开发永远都不是一个单点,只有各方各面联动起来,才会向着健康的方向前进,而所有一切的基础,就是自动化。


感谢张凯峰对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011 年 9 月 10 日 00:007126
用户头像

发布了 22 篇内容, 共 12.4 次阅读, 收获喜欢 40 次。

关注

评论

发布
暂无评论
  • 手把手教你依赖管理

    当项目越来越庞大,你会依赖越来越多的第三方库,那怎么管理这些依赖呢?

    2018 年 7 月 14 日

  • Maven 实战(三)——多模块项目的 POM 重构

    在本专栏的上一篇文章<a href="http://www.infoq.com/cn/news/2010/12/xxb-maven-2-pom">POM重构之增还是删</a>中,我们讨论了一些简单实用的POM重构技巧,包括重构的前提——持续集成,以及如何通过添加或者删除内容来提高POM的可读性和构建的稳定性。但在实际的项目中,这些技巧还是不够的,特别值得一提的是,实际的Maven项目基本都是多模块的,如果仅仅重构单个POM而不考虑模块之间的关系,那就会造成无谓的重复。本文就讨论一些基于多模块的POM重构技巧。

  • Gradle 在大型 Java 项目上的应用

    本文是作者在一个大型Java项目上使用Gradle的部分经验。在Java构建工具的世界里,先有了Ant,然后有了Maven。Gradle作为新的构建工具,获得了2010 Springy大奖,并入围了2011的Jax最佳Java技术发明奖。它是基于Groovy语言的构建工具,既保持了Maven的优点,又通过使用Groovy定义的DSL,克服了 Maven中使用XML繁冗以及不灵活等缺点。

  • 构建 iOS 持续集成平台(一)——自动化构建和依赖管理

    持续集成理念经过10多年的发展,已经成为了业界的标准。而对于iOS领域来说,因为技术本身相对比较年轻和苹果与生俱来的封闭思想,在持续集成方面的发展相对滞后一些,但是,随着越来越多的iOS开发者的涌入,以及各个互联网巨头加大对iOS开发的投入,诞生了一大批非常好用的持续集成工具和服务,本文的目的就是介绍一下如何有效的利用这些类库,服务快速构建一个iOS开发环境下的持续集成平台。

  • 如何构建高效的 Flutter App 打包发布环境?

    在今天的分享中,我与你介绍了如何通过Travis CI,为我们的项目引入了持续交付能力。

    2019 年 10 月 3 日

  • 快速构建持续交付系统(三):Jenkins 解决集成打包问题

    通过今天这篇文章,我和你分享了如何快速安装和配置一套有效的Jenkins系统,以及如何打通Jenkins与GitLab之间的访问通道。

    2018 年 9 月 25 日

  • Eclipse Ganymede:深入 PDE(Plugin Development Environment)

    作为预定6月25日发布的Eclipse Ganymede的一部分,Infoq将推出一系列Eclipse子项目的相关报道。今天,我们将探讨的子项目是插件开发环境(Plugin Development Environment——PDE)(目前正处于3.4版)。Infoq采访了PDE的技术领导人及Code9的主要顾问——Chris Aniszczyk,以了解更多关于PDE及其功用的信息。

  • 自动化构建的那些事儿:金融网站虚拟案例

    本系列的第一部分讨论了将你的构建和部署流程自动化的部分好处。本篇文章中,我们将以一个虚拟的金融机构的企业网站作为示例,将其构建过程一步步完全自动化。

  • 项目管理:如何编写高质量的 Makefile?

    想要高效管理项目,使用Makefile是目前的最佳实践。今天,我会给你四个方法,帮你编写一个高质量的Makefile。

    2021 年 6 月 26 日

  • Mock 不是测试的银弹

    本文是ThoughtWorks实践集锦专题的第六篇。Mock能如你所愿帮助测试实践在团队的开展么?它可能给带来怎样的问题?在本文中,作者从亲身经历的项目的例子出发,分析了Mock局限性,并提出了一些原则来帮助团队在不使用的Mock前提下,交付具有良好健壮性,可以快速运行的测试。

  • Maven 实战(五)——自动化 Web 应用集成测试

    自动化测试这个话题很大,本文不想争论测试先行还是后行,这里强调的是测试的自动化,并基于具体的技术(Maven、JUnit、Jetty等)来介绍一种切实可行的自动化Web应用集成测试方案。当然,自动化测试还包括单元测试、验收测试、性能测试等,在不同的场景下,它们都能为软件开发带来极大的价值。本文仅限于讨论集成测试,主要是因为笔者觉得这是一个非常重要却常常被忽略的实践。

  • 案例学习:Jigsaw 模块化迁移

    这篇文章提供了一个学习案例,演示一个真实的应用程序需要做出哪些变更才能迁移到JPMS。对于Java开发者来说,了解模块化系统无疑是一个非常重要的技能。

  • 代码测试(下):Go 语言其他测试类型及 IAM 测试介绍

    今天,我来介绍下Go 语言中的示例测试、TestMain函数、Mock测试、Fake测试,并且介绍下IAM项目是如何编写和运行测试用例的。

    2021 年 8 月 19 日

  • 构建检测,无规矩不成方圆

    工欲善其事必先利其器, 好的工具可以解放大量的生产力,最重要的是构建检测后的交付让你我更有信心了。

    2018 年 8 月 7 日

  • Boson:超越 Rake 的另一选择?

    相信大多数Rubyist对Rake都不陌生,而那些Rails开发者更是每天都要与它打交道。每天看着task...do...end,你需要一些新鲜玩意儿了,Gabriel Horner为我们带来了一个新的命令/任务框架,那就是Boson。

  • 如何进行高效的 Rails 单元测试

    在笔者开发的系统中,有大量的数据需要分析,不仅要求数据分析准确,而且对速度也有一定的要求的。没有写测试代码之前,笔者用几个很大的方法来实现这种需求。结果可想而知,代码繁杂,维护困难,难于扩展。借业务调整的机会,笔者痛定思痛,决定从测试代码做起,并随着不断地学习和应用,慢慢体会到测试代码的好处。本文忠实的记录了在这个过程中所获得的经验,介绍了如何进行高效的Rails单元测试。

  • 开源项目 Junit-Ext 发布

    开源项目Junit-Ext前些日子发布了1.0 RC3版本,InfoQ中文站与项目发起人、ThoughtWorks咨询师胡凯就这次发布进行了简短的采访。

  • Maven 实战(八)——常用 Maven 插件介绍(下)

    笔者根据自己的经验介绍一些最常用的Maven插件,在不同的环境下它们各自都有其出色的表现,熟练地使用它们能让你的日常构建工作事半功倍。本文为下半部分。

  • Maven 实战(二)——POM 重构之增还是删

    无论是对POM内容进行增还是删,其目的都是一样的,就是为了让POM更清晰易懂且让构建更稳定。从这点来说,POM重构与一般的代码重构是类似的。需要谨记的是,重构的前提是完善的自动化测试和持续集成。本文介绍的单个POM规模的重构。

发现更多内容

魔晶面膜

飞亚科技

yyds!用飞桨玩明日方舟

百度大脑

百度 飞桨

颠覆微服务认知:深入思考微服务的七个主流观点

xcbeyond

微服务 4月日更

低代码之所以能火,离不开这些原因!

优秀

低代码

缓存系统设计精要

比伯

Java 互联网 面试 程序人生 技术宅

一个可递归遍历的Vue树型组件

空城机

JavaScript vue.js 前端 4月日更

真的香!Github一夜爆火被各大厂要求直接下架的Java面试题库也太全了

程序员小毕

Java spring 程序员 架构 面试

java基础集合之HashMap

false℃

一文搞懂MySQL体系架构!!

冰河

MySQL 数据库 程序员 系统架构 数据存储

颜色值JavaScript换算(HSV、RGB、十六进制颜色码)

空城机

JavaScript 前端 颜色值换算

你看起来很美味?独家揭露视频推荐系统AI秘方

脑极体

微信被单删或拉黑?这两个免打扰检测方法你要知道。

彭宏豪95

微信 工具 社交 数据备份 4月日更

如何完成日千万级别以上的订单对账(二)

谙忆

干货 | Redis进阶笔记

ninetyhe

redis 缓存 原理分析

阿里P9纯手打的JDK文档太吊了,微软都开始学习了!

Java架构师迁哥

MOOM集团模式

飞亚科技

活久见,58同城居然也有这么牛的Java内部教程

Java架构师迁哥

cat监控http请求-CatFilter

青乡之b

监控 cat

《中寰卫星导航项目管理部负责人卜钢:智能网联行业的问题与前景》(采访提纲):

谙忆

Scrum Patterns:小团队(译)

Bruce Talk

敏捷开发 译文 Agile Scrum Patterns

线上500万数据查询时间在37秒,作者将问题解决了,我却看到了更大的坑

谙忆

如何完成日千万级别以上的订单对账(一)

谙忆

大数据文件浏览器

聚变

[TcaplusDB知识库]TcaplusDB的高可用性和数据安全性介绍

TcaplusDB

数据库 nosql 数据 TcaplusDB

Spark测试用例生成apache iceberg结果

聚变

大数据 iceberg

感谢Github帮我斩获了8家大厂Offer

Java架构师迁哥

聪明人的训练(十三)

Changing Lin

4月日更

不确定的海浪中,更需要数字化转型的定海神针

脑极体

从程序员角度看湖南电信网络全崩,如何防范服务器被攻击以及解决方案

北游学Java

Java 网络安全 网络 服务器

混战的低代码江湖,如何区分「李逵」和「李鬼」?

ToB行业头条

重读《重构2》- 内联函数

顿晓

重构 4月日更

数据cool谈(第2期)寻找下一代企业级数据库

数据cool谈(第2期)寻找下一代企业级数据库

软件开发地基-InfoQ