Some helpers for CFWheels DBMigrate Plugin

Here is a handy controller that I use with my CFWheels application deployments & for debugging my database migrations with Troy Murray’s invaluable CFWheels DBMigrate Plugin.

I use a git branching strategy that utilises feature branches, which can sometimes lead to database migration files being out of sequence based on their creation date.. So I needed a way to test that all current migration are in the correct order… Enter the magical DBMigrate.cfc controller.

To get this up and running, You’ll need to install the CFWheels DBMigrate Plugin, the /models/DBMigrateVersion.cfc model file and the /controllers/DBMigrate.cfc controller from https://github.com/chapmandu/cfwheels-dbmigrate-plugin-tools.

Once these files are in place, you can call the following URLs and see the JSON they return:

http://yoursite/index.cfm?controller=dbmigrate&action=ping

{
db: "okay"
}

http://yoursite/index.cfm?controller=dbmigrate&action=migrations

{
totalMigrationCount: 4,
isOrdered: true,
latest: "20160205201245",
versions: [
{
version: 20140527111730,
cfc: "20140527111730_create_tables",
migrated: true,
details: "create tables"
},
{
version: 20160205201146,
cfc: "20160205201146_insert_lookup_rows",
migrated: false,
details: "insert lookup rows"
},
{
version: 20160205201233,
cfc: "20160205201233_add_plain_text_password_field_to_users_table",
migrated: false,
details: "add plain text password field to users table"
},
{
version: 20160205201245,
cfc: "20160205201245_create_credit_card_number_columns",
migrated: false,
details: "create credit card number columns"
}
],
current: 20140527111730,
migratedCount: 1,
notMigratedCount: 3,
isMigrated: false
}

http://yoursite/index.cfm?controller=dbmigrate&action=current

{
current: 20140527111730
}

http://yoursite/index.cfm?controller=dbmigrate&action=latest

{
latest: 20160205201245
}

http://yoursite/index.cfm?controller=dbmigrate&action=ismigrated

{
ismigrated: false
}

The main one I use for my automated tests is the big sexy action=migrations packet. If any of the migrations are not in the correct sequence, the isordered node in the JSON will have a value of false Here is the RocketUnit test that I run to check if all migration are in the right order (and can be run automatically).

public void function test_db_checks_awaiting_migration_files_should_be_in_order()
{
    loc.args.action = "migrations";
    loc.response = getResponse(argumentCollection=loc.args);
    loc.substring = '"isordered":true'
    assert('loc.response contains loc.substring');
}

Disclaimer: Please be careful leaving this controller lying around in your production environment.. It doesn’t perform any database changes, but can give some basic information about your database migrations to anyone who may stumble upon your URL..

If you use the DBMigrate plugin, you may like this blog post… If you don’t use the plugin.. You really should.

Advertisement

Create globally unique URLs within CFWheels applications

One great feature of the CFWheels framework is being able to override inbuilt functions, yet still be able to call the core function from within. This allows you to slightly tweak core functions without having to copy, paste & butcher core code, which would 1) go against the DRY principle, and 2) create possible legacy issues when the core functions are updated.

The example below tweaks the urlFor() function by adding a random token to all urls generated within a CFWheels application. This can be handy to avoid page caching as every url is unique.

Because the urlFor() function is used within linkTo(), redirectTo() and startFormTag(), this single tweak trickles down to all CFWheels functions that generate urls.

// controllers/Controller.cfc

/**
 * Hint: Appends a token to all urlFor() calls
 */

public any function urlFor() {

	// create your random string
	var randomToken = LCase(Replace(CreateUUID(), "-", "", "all"));

	// defaults
	param name="arguments.params" default="";
	param name="arguments.token" default="true";

	// add the token to the URL unless told otherwise.
	if (arguments.token) {
		arguments.params = ListAppend(arguments.params, randomToken, "&");
	}

	// pass any args to the core urlFor helper
	return core.urlFor(argumentCollection=arguments);
}

Your urls that once looked like this /mypage, now magically look like /mypage?d9e4bd5a3b78427dab33fe02e0c14b5a=

Easily build where clauses for the CFWheels ORM with this whereify helper

Rather than stuff around building up a string when one’s WHERE clause is conditional.. I make heavy use of this simple little helper which takes an array, and returns a finished string ready to pass to the CFWheels ORM.

// helpers.cfm

/**
 * Hint
 * I surround each array element in brackets and return delimited by an operator
 */
public string function whereify(required array array, string operator="AND") {
	var loc = {};
	loc.array = [];
	for (loc.i=1; loc.i <= ArrayLen(arguments.array); loc.i++) {
		loc.array[loc.i] = "(#arguments.array[loc.i]#)";
	}
	return ArrayToList(loc.array, " #arguments.operator# ");
}

And here’s how I use it..

// YourController.cfc

var loc = {};
// build a sexy where clause array..
loc.where = [];
ArrayAppend(loc.where, "foo = 'bar'");
if (StructKeyExists(params, "slayer")) {
	ArrayAppend(loc.where, "somecolumn IN (1,2,3)");
}

// use the whereify helper in your ORM call
// Produces: (foo = 'bar') AND (somecolumn IN (1,2,3))
users = model("User").findAll(whereify(loc.where));

// You can also specify an operator
// Produces: (foo = 'bar') OR (somecolumn IN (1,2,3))
users = model("User").findAll(whereify(loc.where, "OR"));

Deploying a Lucee application to an Amazon Elastic Beanstalk Environment

If there was a piece of interwebs kit I would like to buy flowers, it would without doubt be Amazon’s Elastic Beanstalk with autoscaling.. Couple it with the Lucee scripting language and I’d happily take it home to meet my parents..

After much experimentation and many iterations (failed attempts and hair-pulling), I’ve managed to deploy Lucee applications (with my fave framework.. CFWheels) to the AWS Elastic Beanstalk platform and have it running reliably in production for a number of months.

My setup is a bit more complex than described below, but hopefully these bare bones will get you started on the path to automated utopia.. Those bare bones will result in a finished .war file that you can upload to EB using their deploy by file upload feature. You could also use the AWS command line tools, AWS toolkit for Eclipse, a Jenkins plugin or one of the many SaaS deployment platforms around.

I personally use Jenkins (on an AWS EC2 instance) to pull, build, test and deploy. I prefer this type of setup as there is much less margin for human error, and the resulting 60Mb+ war file is transferred within your Virtual Private Cloud rather than travelling across the interwebs if you were to manually upload it for every deployment.. oh, and it’s open source.

At a high level, I will outline how to download the Lucee server war file, combine your application’s code, and use some AWS sorcery to configure your Lucee application. There are some pre-requisites:

  • The settings that you would normally configure in Lucee admin, should be in Application.cfc (See the Export feature in Lucee admin)
  • The commands to create the finished war file are written for Linux (If you’re Windows inclined , you could either ‘translate’ them to Windows commands, try Cygwin or a Virtual Machine)
  • You’ll need a src/ directory in your application containing the files outlined below
  • An AWS account with an empty Tomcat 8 Elastic Beanstalk environment

In a nutshell, what happens is the Lucee jars, your code and some config files are all copied to the “build” directory, then zipped into a sexy war file ready for upload/deploy to the Elastic Beanstalk environment.

src directory
src/eb-tomcat/web.xml
src/eb-tomcat/urlrewritefilter-4.0.3.jar
src/eb-tomcat/urlrewritefilter.xml
src/eb-tomcat/.ebextensions
src/eb-tomcat/.ebextensions/lucee-server.xml
src/eb-tomcat/.ebextensions/01-copy-lucee-server-xml.config

src-directory-structure

These are configuration files.. for a production application, you’ll need to provide your own lucee-server.xml and most likely your own rewrite rules in urlrewrite.xml. If you don’t need url rewriting, then remove all reference to them. The .ebextensions execute EB container commands.. the one provided simply copies your lucee-server.xml file into place. To create your finished war file, cd to your application’s root directory then run these commands. I’d suggest creating a shell script.. and if you feel adventurous, allow an ‘environment’ option that will create different environment builds for your application’s deployment pipeline

# some vars..
DOWNLOADS_PATH="/tmp/downloads/"
LUCEE_VERSION="4.5.1.000"
LUCEE_WAR="lucee-$LUCEE_VERSION.war"
LUCEE_WAR_PATH="$DOWNLOADS_PATH$LUCEE_WAR"
FINISHED_WAR_PATH="/var/www/myapp/war/"

# ensure the required packages are installed
sudo apt-get install unzip wget
# sudo yum install unzip wget # (or using yum)

# create required directories
mkdir build
mkdir -p $DOWNLOADS_PATH
mkdir -p $FINISHED_WAR_PATH
# only download the lucee war file once
if ! [ -e "$LUCEE_WAR_PATH" ]
then
  wget -O $LUCEE_WAR_PATH http://bitbucket.org/lucee/lucee/downloads/$LUCEE_WAR
fi

# unzip the lucee war file into the build utility directory
unzip $LUCEE_WAR_PATH -d build
rm -rf build/assets

# explicitly copy required code (this is cfwheels specific.. but just plonk in your own directory names)
cp -r config controllers customtags events images javascripts models plugins  stylesheets tests views wheels build
cp -r Application.cfc index.cfm rewrite.cfm root.cfm build
# remove any peksy files that you don't want deployed to production
rm -r build/favicon.ico build/License.txt
# copy config files
cp -r src/eb-tomcat/.ebextensions build
cp -f src/eb-tomcat/web.xml build/WEB-INF/web.xml
# copy tuckey url rewrite jar if required
cp src/eb-tomcat/urlrewritefilter-4.0.3.jar build/WEB-INF/lib/urlrewritefilter-4.0.3.jar
cp src/eb-tomcat/urlrewrite.xml build/WEB-INF/urlrewrite.xml

# this is how I force my app into 'production' mode.. again very cfwheels-centric
rm build/config/environment.cfm
echo 'set(environment="production");' > build/config/environment.cfm
# create le war file
cd build && zip -r -q $FINISHED_WAR_PATH"app-production-master.war" . && cd ..
# cleanup
rm -r build

This should give you a production ready war file located at: build/app-production-master.war. Now you can manually upload your app-production-master.war and be on your way..

Some of my recommendations:

  • Build in the cloud using Jenkins (or your choice of CI server)
  • Host the Lucee war file in your own S3 bucket.. you’re less at the mercy of 3rd parties connectivity issues.
  • You can easily incorporate additional jar files as per the tuckey rewrite tool.. just copy them into build/WEB-INF/lib/

Any suggestions, improvements & feedback welcome.

Enforcing the use of routes in CFWheels

If you really like routing (you’d find that really funny if you’re Australian) and want to enforce their use in all linkTo(), urlFor() and startFormTag() function calls.. Do as I do..

Put the function below into your /controllers/Controller.cfc

<!--- /controllers/Controller.cfc --->
<cffunction name="urlFor" access="public" output="false" hint="ensure routes are used for all urls">
    <cfif ! StructKeyExists(arguments, "noroute") && ! ( (StructKeyExists(url, "controller") && url.controller == "wheels") || StructKeyExists(arguments, "url") )>
      <cfif ! (StructKeyExists(arguments, "route") && Len(arguments.route) gt 0)>
        <cfthrow message="Please use a route rather than an action.. Thanks!">
      </cfif>
    </cfif>
    <cfreturn core.urlFor(argumentCollection=arguments)>
  </cffunction>
</code>

This overrides, the core urlFor() method, checking for the existence of a route argument.. UNLESS, the controller is “wheels” which probably means you’re using the test framework or managing a plugin. There is also a way to bypass the route nazi.. by using the noroute=true arguments.

Happy routing.. Teehee!

CFWheels Calculated Properties on Steroids using an afterFind Callback

One limitation with using calculated properties in your CFWheels models is that one is limited to what can be achieved using SQL. CFML has almost infinite possibilities for formatting strings which simply cannot be done easily (or at all) in SQL.

A technique that I use to get around this limitation is to create new properties using an afterFind callback for both objects AND queries in a single callback.

In the code below I create two new properties, createdAtShort and createdAtLong which are just different date masks applied to the trusty createdat property. It’s a fairly simple example, but highlights the huge potential of keeping this type of logic in your model and out of your views. Hopefully this will get your brain moist.

<!--- /models/User.cfc --->

<cfcomponent extends="Model" output="false">

  <cffunction name="init">
    <cfset afterFind("$$setStuffAfterFind")>
  </cffunction>

  <cffunction name="$$setStuffAfterFind" access="private">
    <cfif StructKeyExists(arguments, "createdAt")>
      <cfset arguments.createdAtShort = DateFormat(arguments.createdAt, "d/m/yy")>
      <cfset arguments.createdAtLong = DateFormat(arguments.createdAt, "dddd, dd mmmm yyyy") & " " & TimeFormat(arguments.createdAt, "hh:mmtt")>
      <cfreturn arguments>
    </cfif>
  </cffunction>

</cfcomponent>

Now you can call your model and the result should contain your new properties..

<!--- /controllers/Users.cfc --->

<cfset user = model("User").findOne()>
<cfset users = model("User").findAll(maxrows=5)>

<cfdump var="#user.properties()#">
<cfdump var="#users#">

Notes:

  1. Avoid performing further database operations inside the callback when using findAll.. y’know the whole querying within a recordset loop thing..
  2. afterFind callbacks are not called on included models. So.. model("Company").findAll(include="Users") won’t contain your new properties.

UPDATE: Chris Peters has since shown me the error of my ways.. it turns out that the arguments scope can be used for both queries and objects. This has simplified the entire concept considerably (and made ‘my’ code look eerily similar to the official CFWheels docs! The code and description above have been modified to use arguments scope.

Run your CFWheels database migrations automatically on application start

It’s no secret that I’m a massive fan of the Coldfusion on Wheels framework.. and there are a couple of tools that I use in my deployment process that are invaluable to me.

One of those is the DBMigrate plugin. I recently came up with a way to automatically migrate to the latest database version when the application first starts.

This can be useful/essential when:

  • Your deployment process doesn’t use post-deploy hooks
  • You want to make your application more portable
  • You want to simplify your deployment script

Essentially, it calls a couple of the plugin’s methods whilst doing a little array-fu.

/events/onapplicationstart.cfm

<cfscript>
// migrate database
_dbm = {}
// create a pointer to the dbmigrate plugin
_dbm.plugin = application.wheels.plugins.dbmigrate
// create an array of available migrations sorted by version in descending order
_dbm.available = []
for(item in _dbm.plugin.getAvailableMigrations()){
_dbm.available.Append(item.version)
}
ArraySort(_dbm.available, "numeric", "desc")
// migrate to the most recent version
_dbm.plugin.migrateTo(_dbm.available[1])
</cfscript>

There are a few caveats:

  • It will execute every time the application loads OR is manually reloaded via the reload=true parameter (though it’s fairy lightweight when there are no migrations to execute)
  • It will execute on every server in your cluster
  • There is (currently) no error handling
  • It could disable your application if your migrations fail

Passing a list of strings to CFWheels’ findAll() where argument

Use the ListQualify() function to pass a list of strings to the where argument in CFWheels ORM finders. ListQualify() encloses each list element in with the string specified (in this case, a single quote). Without it, CFWheels interprets the list as a single value.

<cfscript>
names = "Foo,Bar";
users = model("User").findAll(where="firstname IN ('#names#')");
/*
 * Generates SQL Query.. Not so good
SELECT firstname, lastname FROM users WHERE firstname IN ('Foo,Bar')
*/

users = model("User").findAll(where="firstname IN (#ListQualify(names, "'")#)");
/* 
 * Generates SQL Query. Great success!
SELECT firstname, lastname FROM users WHERE firstname IN ('Foo','Bar')
*/
</cfscript>

Great success!

Ensuring Full Unit Test Coverage In Your CFWheels App

Ensuring that you have unit tested all (or most) of your code in your CFWheels app is an important part of any test suite. As you hopefully know by now, the CFWheels framework features an in-built unit test framework.

The code below is a test itself, that will scan your controllers, models and views folders and make sure that you have a corresponding “test” cfc for each. If it discovers that a test is missing, it will fail.. so your app will not pass unless it has full test coverage.

To get it working, just copy the code below and save it into a file in /tests/TestsExist.cfc (or anywhere under the tests folder actually). You can retro-fit it to any app, though you may need to re-organise your test files somewhat.

There are some conventions that you need to follow to have it work properly, but I consider this being well organised (and you should be used to conventions by now). Your folder structure under /myapp/tests/ should look like this... Say you have a Login.cfc controller, your controller test cfcs themselves should be named like this TestLogin.cfc.. but you could also have multiple test files for your Login.cfc, just as long as the file BEGINS with “TestLogin”, eg: TestLoginRedirects.cfc, TestLoginBusinessLogic.cfc.. you get the idea.

The same goes for models and views. There is much one could do to improve/re-factor/customise this code, but it will hopefully catch anything you may have forgotten to test. Feel free to contribute to this project on GitHub.

So to summarise:

  • Re-organise your folders as per the example above
  • Re-name your test cfc files using Test{YourControllerName}.cfc
  • Copy the code into a TestsExist.cfc file under the /tests/ folder
  • Run your unit tests
<cfcomponent extends="wheelsMapping.Test">
	
	<cffunction name="setup">
		<cfset testsPath = ExpandPath("tests")>
		<cfset controllersPath = ExpandPath("controllers")>
		<cfset modelsPath = ExpandPath("models")>
	</cffunction>
	
	<cffunction name="test_00_setup_and_teardown">
		<cfset assert("true")>
	</cffunction>

	<cffunction name="test_01_all_controller_tests_exist">
		<cfdirectory action="list" directory="#controllersPath#" filter="*.cfc" name="controllers" />
		<cfdirectory action="list" directory="#testsPath & '/' & 'controllers'#" filter="*.cfc" name="controllerTests" />
		<cfset assert("controllerTests.recordCount gt 0")>
		<cfloop query="controllers">
			<cfif ListFindNoCase("Controller.cfc,Wheels.cfc", controllers.name) eq 0>
				<cfset abbrev = ListFirst(controllers.name, ".")>
				<cfset isTestFileFound = false>
				<cfloop query="controllerTests">
					<cfif FindNoCase("Test" & abbrev, controllerTests.name) gt 0>
						<cfset isTestFileFound = true>
						<cfbreak>
					</cfif>
				</cfloop>
				<cfset assert("isTestFileFound", "controllers.name")>
			</cfif>
		</cfloop>
	</cffunction>
	
	<cffunction name="test_02_all_model_tests_exist">
		<cfdirectory action="list" directory="#modelsPath#" filter="*.cfc" name="models" />
		<cfdirectory action="list" directory="#testsPath & '/' & 'models'#" filter="*.cfc" name="modelTests" />
		<cfset assert("modelTests.recordCount gt 0")>
		<cfloop query="models">
			<cfif ListFindNoCase("Model.cfc,Wheels.cfc", models.name) eq 0>
				<cfset abbrev = ListFirst(models.name, ".")>
				<cfset isTestFileFound = false>
				<cfloop query="modelTests">
					<cfif FindNoCase("Test" & abbrev, modelTests.name) gt 0>
						<cfset isTestFileFound = true>
						<cfbreak>
					</cfif>
				</cfloop>
				<cfset assert("isTestFileFound", "models.name")>
			</cfif>
		</cfloop>
	</cffunction>

	<cffunction name="test_03_all_view_tests_exist">
		<cfdirectory action="list" directory="#controllersPath#" filter="*.cfc" name="controllers" />
		<cfdirectory action="list" directory="#testsPath & '/' & 'views'#" filter="*.cfm" name="viewTests" />
		<cfset assert("viewTests.recordCount gt 0")>
		<cfloop query="controllers">
			<cfif ListFindNoCase("Controller.cfc,Wheels.cfc", controllers.name) eq 0>
				<cfset abbrev = ListFirst(controllers.name, ".")>
				<cfset isTestFileFound = false>
				<cfloop query="viewTests">
					<cfif FindNoCase("Test" & abbrev, viewTests.name) gt 0>
						<cfset isTestFileFound = true>
						<cfbreak>
					</cfif>
				</cfloop>
				<cfset assert("isTestFileFound", "controllers.name")>
			</cfif>
		</cfloop>
	</cffunction>

	<cffunction name="teardown">
	</cffunction>

</cfcomponent>

Automated Testing with CFWheels, Jenkins & Ant Tutorial

Automated Testing / Continuous Integration sounds complex and intimidating, but its like brain surgery.. its not that hard once you know the concepts and how the different elements fit together. Well maybe brain surgery wasn’t such a good example.. but you get the idea. Just think of a CI server as a fancy scheduled task manager.. with a few extra bits & pieces bolted on.

One of my pet projects this year has been to completely automate the testing and build of a CFWheels application. This guide is a bare-bones, no-frills installation to get you up and running. Assuming you already have written your unit tests, you should be able to get this up and running in under an hour.

There are many ways to skin this cat, but for this example I have used Jenkins as the CI server, Git as the version control system and Ant to do the builds. All elements are open source, so you can get up and running for zero dollars. I’m sure there are arguments for or against these choices and it’s quite possible to substitute any of them with your preferred method. There are an extensive array of plugins available for different source control flavours and build ‘tools’ (Ant, Maven, Command Line). My installation is on a 64bit version of Windows 7 using default installation paths.

Ideally, you would have a dedicated CI server which has its own application server (ColdFusion/Railo) and database. This is not essential.. in fact, my working example was installed on my laptop.. the only drawback with having the installation on your development machine, it that you will have to deploy your app to your webroot using a different foldername. Eg: “{webroot}/wheelsapp” source code will have to be deployed to “{webroot}/wheelsorama” so it can be tested without affecting your source code.

If you wanted to make your configuration even simpler, you could remove the Git repository configuration, and just run the unit tests directly on your source code.. but this is not ideal. * Be VERY careful with taking this approach as the example build script deletes the destination app folder before each deployment.. and that would be bad mmkay.

The example below will enable you to test your application, your plugins, the core wheels framework or a combination of the three (see the build.xml file).

Software Installation

As a general rule, I have install all software in their default locations, as you will see in the Jenkins configuration steps.

1. Install Java JDK
2. Install Ant (I recommend Winant if you’re on Windows)
3. Install Git
4. Install Jenkins

Windows users may need to re-boot (surprise surprise!) if the Jenkins service is not running (Check your Windows Services to confirm), if the service still fails to start, you may need to execute this from a command prompt (Run as administrator) An inconvenient workaround!

cd C:\Program Files (x86)\Jenkins
java -jar jenkins.war --ajp13Port=8010

5 .Visit your Jenkins server at http://localhost:8080 (or port 8010 if you had to start Jenkins via the command prompt)

You should have a brand-spanking new Jenkins server with which to unleash your automated wrath!

Configure Jenkins

1. Click “Manage Jenkins” > “Manage Plugins” > “Available” Tab
2. Find the Git Plugin and install it
3. Click the “Installed” tab and ensure you have the Git and Ant plugins. (Ant plugin should be bundled with the Jenkins install)

4. Return to the Jenkins dashboard and click “Manage Jenkins” > “Configure System”
5. In the JDK section, uncheck the “Install automatically” option and enter a name and the path to your JDK folder.

6. In the Git section, uncheck the “Install automatically” option and enter the path to your git executable

7. In the Ant section, click the “Add Ant” button, uncheck the “Install automatically” option and enter a name and the path to your Ant folder (or WinAnt if installed).

Create your Job

1. Return to the Jenkins dashboard and click “New Job”
2. Enter a job name and select “Build a free-style software project”

3. Under the “Source Code Management” section, check the “Git” option
4. Specify your app’s Git repository. If you are unfamiliar with this, it’s time to click here. This can be a local path, a network path or an ssh address.

5. Under the “Build Triggers” section, choose your preferred build interval. I like to have Jenkins poll my SCM (Git repository)  for changes every few minutes and build automatically if it finds any commits since the last build. */5 * * * * means every 5 minutes.. Click on the help icon for more info about intervals.

6. Under the build section, click the “Add Build Step” button and select the “Invoke Ant” option. Select the “Default Ant” configuration you added earlier. Since we will be using the default Ant convention (using build.xml), this is all that’s required.. but more about that shortly.

7. In the Post-build action section, click the “Add post-build action” button and select the “Publish JUnit test result report” option. Specify where your unit test result xml files will be written to. In this case build-report/test_result_*.xml, a folder called build-report with the files being called test_result_{something}.xml. Don’t worry too much about any warning messages here.. as the folders don’t exist yet.

Prepare your Wheels app 
There are a few steps here..

  • Initialise your wheels app as a Git repository
  • Download the JUnify Wheels plugin (the most dangerous plugin in the galaxy) which will return your unit tests in JUnit format (Ties in very nicely with Jenkins)
  • (Optional) Download the DBMigrate plugin for making automated database changes during the build
  • Now we need to tell Ant what to do to build and test your app. We will do this by creating 2 files in your app’s root folder. The 2 files are ant.properties and build.xml. ant.properties is for setting variables for use in build.xml. build.xml tells Ant what to do. * You will most likely need to modify the ant.properties file to suit your environment

ant.properties

# for running start/stop bat files
railo_bin=C:/railo/tomcat/bin
# cf_bin=C:/ColdFusion9/bin
# your webroot.. where jenkins will deploy your code (Apache/IIS)
web_root_dir=C:/railo/tomcat/webapps/ROOT
# web_root_dir=C:/inetpub/wwwroot
# where jenkins will git pull your code.. then deploy from
jenkins_workspace_dir=C:/Program Files (x86)/Jenkins/jobs/Wheels-O-Rama Deploy/workspace
# where jenkins will deploy to
destination_app_dir=${web_root_dir}/wheelsorama
# where unit test results will be written to
build_report_dir=${jenkins_workspace_dir}/build-report
# your app's url
app_root_url=http://localhost:8888/wheelsorama/index.cfm
# the unit test base url
unit_test_url=${app_root_url}?controller=JUnify&amp;action=index&amp;

build.xml

<project default="initialize">

	<!-- this is the default target (function) -->
	<target name="initialize">

		<!-- import properties -->
		<property file="ant.properties" />

		<!-- stop railo service.. not always needed! -->
		<!-- <exec dir="${railo_bin}" executable="cmd" os="Windows NT">
			<arg line="shutdown.bat"/>
		</exec> -->

		<!-- once jenkins has pulled the code into its workspace, i want to delete the previous deployment, but keeping root folder -->
		<mkdir dir="${destination_app_dir}" />
		<delete includeemptydirs="true">
			<fileset dir="${destination_app_dir}" includes="**/*" />
		</delete>

		<!-- then get a fresh copy of the latest code into the webroot for testing -->
		<copy todir="${destination_app_dir}">
	    	<fileset dir="${jenkins_workspace_dir}"/>
	    	<!-- exclude any files you don't want/need -->
	    	<!-- <exclude name="**/.gitignore"/> -->
	    	<!-- <exclude name="**/*.php"/> -->
	    	<!-- <exclude name="**/*.asp"/> -->
		</copy>

		<!-- make a folder in the workspace to keep my build reports -->
		<mkdir dir="${build_report_dir}" />

		<!-- start railo service -->
		<!-- <exec dir="${railo_bin}" executable="cmd" os="Windows NT">
			<arg line="startup.bat"/>
		</exec> -->

		<!-- migrate database using a call to the dbmigrate plugin url -->
		<!-- <get src="${app_root_url}?controller=wheels&amp;action=wheels&amp;view=plugins&amp;name=dbmigrate&amp;password=yourpassword&amp;migrateToVersion=999999999" dest="${build_report_dir}\dbmigrate_result.html" /> -->

		<!-- make a get request to your unit test url, writing the result to a file. Jenkins will use it to determine the success of your build
			== possible type param values ==
			# type=app		: runs the tests in /myapp/tests
			# type={myplugin}	: runs the tests in /myapp/plugins/{myplugin}/tests
			# type=core		: runs the tests in /myapp/wheels/tests
		-->
		<get src="${unit_test_url}type=app" dest="${build_report_dir}\test_result_app.xml" />
		<!-- <get src="${unit_test_url}type=myplugin" dest="${build_report_dir}\test_result_myplugin.xml" /> -->
		<!-- <get src="${unit_test_url}type=core" dest="${build_report_dir}\test_result_core.xml" /> -->

	</target>

</project>

Unit Tests

This is the final piece of the puzzle. The Wheels team have done a great job documenting unit testing your app using the Wheels test framework, and there is a post by someone about unit testing your plugins.. So get on it.

Build your app

Now that you are all configured and unit tests written.. its time to build! Click on the “Wheels-O-Rama Deploy” job then in the left menu click “Build Now”. You should see a progress bar appear under the menu.. it should either return a blue (good) or red (bad) icon. If you mouseover the build link and click the “Console Output” link you will see the output that was produced by Jenkins & Ant. You can watch the console output in real time by clicking the link whilst the build is in progress. If the build fails, this is where you will find the clues to its resolution.

Umm.. so WTF is actually happening?

I’m glad you asked..

  • Jenkins polls your Git repository looking for commits
  • When a commit it found, it will pull your source code into its workspace
  • Jenkins will use Ant to run your build.xml file which will…
    • Delete the previous code deployment from your webroot
    • Copy the latest source code from Jenkins workspace into your webroot
    • Call your unit tests via a URL and write the results to the Jenkins workspace
  • Jenkins will analyse the unit test results
  • Jenkins will report whether the build passed or failed

Where to from here?

This post is really just a minimal working example of automated testing. Here are a few ideas to play around with:

  • Setup a post build action to alert you of build failures
  • Break the existing job into separate “Deploy” and “Test” jobs (using a “Post Build Action” to kick off the Test job)
  • Build and test with different CFML engines, CFWheels framework versions and database engines for wide compatibility
  • Use the dbmigrate plugin to automatically make database changes upon building (See build.xml example)
  • Create another job that uses Ant to FTP your successful build to your production server
  • Use Selenium to do automated functional browser testing
  • Push your successful build to GitHub
  • Extending Jenkins further with the smorgasbord of plugins. The Chuck Norris plugin is a favourite of mine!

Feel free to comment regarding your experiences using this guide.. Thanks for reading!