Tuesday, March 25, 2008

Automating .NET web application builds with CruiseControl.NET

I've been trying to get around a completely automated build process for sometime now. Finally, I got to work out through the details for a recent web application project in .NET. Now that I have most of the things out of the way I suppose its fair to say that its fairly straight forward but an occasional head cracking must happen. I hope that this post might help someone else starting out with CruiseControl.NET.

To understand what's happening I suppose its necessary to explain the environment in which this takes place:

  • ASP.NET application, developed with Visual Studio 2005, using VB.NET
  • NUnit for unit testing
  • NAnt for automated build (version 0.86-beta-1)
  • Selenium for automated web tests (version 0.8.2)
  • CruiseControl.NET for continuous integration (version 1.3.0.2918)
  • Subversion as a source code repository
Hm. Not too bad, I suppose. If I can figure out how to throw in an issue-tracking system into the pot and start distributing installer packages with automated release notes that would be just about as far as you can push that thing. But, lets see on the fundamentals first.

I have 3 projects in the Visual Studio solution: web app, class library (with the core set of classes) and an NUnit test project. Class library uses Microsoft Enterprise Library 3.1 (May 2007) - specifically its Data and Logging services.

Web application references the core class library (just like the NUnit project does).

You can see the top-level folder structure for the project. It was inspired by Jean Paul S. Boodhoo's series of articles on automating a build with NAnt.

Web application resides in src/app/WebApp folder. Core class library is in src/app/Core and NUnit is in src/test/NUnit.

tools/ folder contains NAnt and NUnit dlls. lib/ contains MS Enterprise Library DLLs referenced by the project.

build.bat batch file basically calls NAnt passing in the default.build as the build file where all (or most of) the juice happens.

NAnt

This is really the central piece of the puzzle that took the most time, so lets try and explain that bit.

The technique of using template files to generate actual files needed for certain things during the build process is borrowed from automating your build with NAnt (part 6) by Jean Paul S. Boodhoo. That is used to create the script file to generate a database as well as populate it with default seed data. In addition, app.config file is generated in the same way before deployment. Example target that does the merging:
<target name="convert.template.unicode">
<copy file="${target}.template" tofile="${target}" overwrite="true" inputencoding="Unicode">
<filterchain>
<replacetokens>
<token key="INITIAL_CATALOG" value="${initial.catalog}" />
<token key="ASPNETACCOUNT" value="${aspnet.account}" />
<token key="OSQL_CONNECTION_STRING" value="${osql.ConnectionString}" />
<token key="CONFIG_CONNECTION_STRING" value="${config.ConnectionString}" />
<token key="DBUSER" value="${db.user}" />
<token key="DBPASSWORD" value="${db.password}" />
<token key="DBPATH" value="${database.path}"/>
</replacetokens>
</filterchain>
</copy>
</target>
Example above is used to replace various tokens with the values required in the environment where build is taking place. The only difference from the original appearance in Boodhoo's article is in the use of inputencoding="Unicode" in the copy task which ensures that a file that is encoded in unicode gets copied into a file with the same encoding (by default OEM code page is used which means your unicode files become perhap latin-1252 encoded and makes the loose special characters).

Local.properties.xml is also used to merge user-specific properties into the build file - things like passwords, paths where SQL Server data/log files are located, etc.

I have .NET 1.1 and 2.0 both installed on my development machines. IIS runs with 1.1 being the default ASP.NET version on the default website. This means that when you deploy a new virtual directory on the site it picks up 1.1 as ASP.NET version. In this case, that would be a problem since the virtual directory must be 2.0. Luckily that is easily changable with the use of aspnet_regiis.exe. You will notice that the deploy target in the build file essentially executes it to get:
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe -s W3SVC/1/ROOT/WebApp
The above will essentially change the the ASP.NET version for the virtual folder that was created during deployment through the build script. In my case I have IIS 5.0 with a single instance and my web application's virtual folder name is WebApp.

I've found it useful to suppress warning messages from the compiler. You can do that within the vbc NAnt task with the following:

<vbc target="library" rootnamespace="NantCCProject.HR" output="build\NantCCProject.HR.dll" debug="${debug}" verbose="true">
<nowarn>
<!-- ignore all code paths don't return a value warning. -->
<warning number="42104"/>
<warning number="42105"/>
<warning number="40000"/>
</nowarn>
...
</vbc>


Selenium

To facilitate selenium testing, I've placed the core selenium files inside the web application as well as a folder with all the tests. selenium is in src/app/WebApp/resources/selenium/core and its tests are at src/app/WebApp/resources/selenium/tests folder.

Integrating Selenium into CruiseControl.NET means that the tests should get executed through NAnt. There is a good starting point on how to perform the integration in the OpenQA wiki. Essentially, we'll need a custom NAnt task that executes the selenium tests.

CruiseControl.NET configuration

Following is the CruiseControl.NET server configuration file for the project:

<cruisecontrol>
<!--
cruise control config file -->
<!--
<project name="
NantCCProject" />
-->
<project name="
NantCCProject">
<workingDirectory>E:\programming\DOT.NET\cruise\checkouts\
NantCCProject</workingDirectory>
<artifactDirectory>E:\programming\DOT.NET\cruise\builds\
NantCCProject\artifacts</artifactDirectory>
<webURL>http://localhost:8283/ccnet</webURL>
<modificationDelaySeconds>10</modificationDelaySeconds>
<labeller type="defaultlabeller">
<prefix>1.0.</prefix>
<incrementOnFailure>true</incrementOnFailure>
</labeller>
<triggers>
<intervalTrigger seconds="60" />
</triggers>
<state type="state" directory="E:\programming\DOT.NET\cruise\builds\
NantCCProject\state" />
<sourcecontrol type="svn">
<tagOnSuccess>true</tagOnSuccess>
<trunkUrl>http://localhost/svn-
NantCCProject/NantCCProject/trunk</trunkUrl>
<tagBaseUrl>http://localhost/svn-
NantCCProject/NantCCProject/releases</tagBaseUrl>
<workingDirectory>E:\programming\DOT.NET\cruise\checkouts\
NantCCProject</workingDirectory>
</sourcecontrol>
<tasks>
<nant>
<executable>nant.exe</executable>
<baseDirectory>E:\programming\DOT.NET\cruise\checkouts\
NantCCProject</baseDirectory>
<buildFile>default.build</buildFile>
<targetList>
<target>db</target>
<target>seed</target>
<target>compile</target>
<target>test</target>
<target>asp.compile</target>
<target>deploy</target>
<target>selenium</target>
</targetList>
<buildTimeoutSeconds>300</buildTimeoutSeconds>
</nant>
</tasks>
<publishers>
<buildpublisher />
<merge>
<files>
<file>E:\programming\DOT.NET\cruise\checkouts\NantCCProject\build\*Test-Result.xml</file>
</files>
</merge>
<xmllogger />
</publishers>
</project>
</cruisecontrol>


A few things to explain regarding cruise control config file:
  • I'm using SVN setup on localhost. This is served through an Apache 2.0 (config for that shortly), hence SVN access is through http://localhost/svn-... URL.
  • There are a few NAnt tasks that get executed - in the order specified: db (create database), seed (pump default data), compile, test, ASP.NET compile, deploy and finally selenium.
  • Every build is labeled as [prefix].[build] where prefix is "1.0." (I did not go into one of the more elaborate labeling schemes, but CC is quite happy to entertain more complicated requests on this)
  • This is using a simple publisher which essentially makes a folder upon every successful build named [prefix].[build] and copies into it whatever was checked-out from SVN
  • Upon successful builds, source code is tagged by way of actually making a copy of the snapshot of files inside the SVN's [ROOT]/releases/[prefix].[build] folder. SVN core files are obtained from the SVN's [ROOT]/trunk folder.
Misc notes
  • Make sure you use rootnamespace attribute in your vbc NAnt task. Set it as per what Visual Studio sets it, otherwise you'll get compilation problems.
  • Your NAnt installation may have a default .NET framework set to something other then the one you require. That can be controlled with the:
<property name="nant.settings.currentframework" value="net-2.0" />
  • hmmm. perhaps enough for now.
Resources

2 comments:

Anonymous said...

I guess I'm wondering why you put the Selenium tests under /src/app/WebApp/resources/selenium/tests

I would think based on the directory structure they would go under /src/tests/ rather than /src/app/

sjusic said...

I believe I needed to get the selenium under WebApp so that I can run them through the development web server. If they're under /tests there's no web app to bootstrap them from.