Part 7: Continuous Integration - The QtpBuild - From the software development trenches

Part 7: Continuous Integration - The QtpBuild

Published 17 September 07 09:33 PM | cjlotz

This is the seventh post in a series where I document how to setup a Continuous Integration (CI) process using open source tools like CruiseControl.NET, NCover, NUnit, Subversion and Sandcastle together with MSBuild and VS 2005. Part 7 covers the QtpBuild and the targets and tasks used by the QtpBuild. The QtpBuild shows how to run the regression test pack using Mercury's QuickTest Professional and Mercury Quality Center

Post Updates

  • 11/12/2007: Updated the CC.Net configuration to use build queues and entity references. 
  • 28/09/2007: Changed the QtpBuild to make use of incremental builds to install/uninstall the application and to run the test cases, identified in a file, from within Mercury Quality Center.

MSBuild Scripts

As mentioned in Part 1, QTP runs on a dedicated QtpServer machine as the regression test pack takes quite some time to run through (3 hours in our scenario).  This begs the question how to invoke Qtp remotely from the BuildServer. I considered the following options:

  1. Remotely invoke Qtp using psexec from our BuildServer.
  2. Remotely invoke Qtp using DCOM from our BuildServer.  This seems possible using QTP's automation object model.
  3. Instead of remotely invoking QTP, install CC.NET on the QtpServer machine itself and create a project that monitors and triggers a build once the DeploymentBuild has been completed on the BuildServer.

Depending on your environment scenario you may choose either option 1 or 2 as it requires less configuration, but in the end I chose to not use a remove invocation mechanism in favour of option 3.  The down side is that I have to install CC.NET on the QtpServer as well.  In addition to deploying the build onto our file server, the DeploymentBuild also copies the msi onto a share for the QtpBuild to use.

Uninstall

Before running the tests the old version of our application needs to be uninstalled.

1 <Target Name="Uninstall" 2 Inputs="@(RunQtpInstallInput)" 3 Outputs="@(RunQtpInstallOutput)"> 4 5 <!-- Launch the Uninstall in silent mode --> 6 <Exec Command="msiexec /x $(DOUBLE_QUOTES)$(QtpUninstallFolder)\$(MsiInstallFile)$(DOUBLE_QUOTES) /passive /l* $(DOUBLE_QUOTES)$(QtpUninstallFolder)\Uninstall.log$(DOUBLE_QUOTES)" 7 ContinueOnError="true" /> 8 9 </Target>

Observations:

  • Lines 2-3: We use MSBuild's incremental builds feature to ensure that we do not uninstall the application if the latest version of the application is already installed on the machine.  This is accomplished by comparing the timestamp of the QtpLastInstallSucceededFile to the timestamp of the latest msi package.  If the msi package timestamp is later we know that we have a new version to install and thus need to do the uninstall.
  • Line 4: The old version of our application is uninstalled by launching Windows Installer with the /x and /passive parameters.  The /passive parameter ensures that no user interaction is required.  We also log the uninstall output to a uninstall.log file. ContinueOnError is set to true for those scenarios where the machine hasn't got an installed version of our application.

Install

Before running the tests the new version of our application needs to be installed.  As mentioned earlier, the msi is copied into the QtpInstallFolder share by the Deploy target that runs as part of the DeploymentBuild.

1 <Target Name="Install" 2 Inputs="@(RunQtpInstallInput)" 3 Outputs="@(RunQtpInstallOutput)"> 4 5 <Error Text="No QtpDeployFolder has been specified" Condition="'$(QtpDeployFolder)' == ''" /> 6 7 <!-- Copy the latest install from the build server --> 8 <Copy SourceFiles="$(QtpDeployFolder)\$(MsiInstallFile)" 9 DestinationFolder="$(QtpInstallFolder)"/> 10 11 <!-- Launch the install in silent mode --> 12 <Exec Command="msiexec /i $(DOUBLE_QUOTES)$(QtpInstallFolder)\$(MsiInstallFile)$(DOUBLE_QUOTES) /passive /l* $(DOUBLE_QUOTES)$(QtpInstallFolder)\Install.log$(DOUBLE_QUOTES) CLEAN_DATABASE=1"/> 13 14 <!-- Copy the msi to the Uninstall folder to use for later uninstallation --> 15 <Copy SourceFiles="$(QtpInstallFolder)\$(MsiInstallFile)" 16 DestinationFolder="$(QtpUninstallFolder)"/> 17 18 <Touch Files="@(RunQtpInstallOutput)" AlwaysCreate="true" /> 19 20 </Target>

Observations:

  • Lines 2-3: We use MSBuild's incremental builds feature to ensure that we do not uninstall the application if the latest version of the application is already installed on the machine.  This is accomplished by comparing the timestamp of the QtpLastInstallSucceededFile to the timestamp of the latest msi package.  If the msi package timestamp is later we know that we have a new version to install and thus need to do the install.
  • Lines 8-9: The latest msi package is copied from the server to the local machine to use for installation.
  • Line 12: The new version of our application is installed by launching Windows Installer with the /i and /passive parameters.  The /passive parameter ensures that no user interaction is required.  We also log the install output to an install.log file.  Also notice the CLEAN_DATABASE=1 statement that we pass onto the installer.  As the installer is launched in passive mode, we need to set any installer options that would normally be selected by the user via the GUI now via the command line.
  • Line 15-16: After installing the application, a copy of the msi is put into the QtpUninstallFolder to use for the next Uninstall.
  • Line 18: We touch the timestamp of the QtpLastInstallSucceededFile to support future incremental builds.

Qtp Automation

Fortunately, the help file of Mercury's QTP product has some good examples of how to automate QTP using COM automation.  I used extracts from their VBScript examples as a base for creating a RunQTP.vbs file that we use for doing our QTP automation.  As QTP stores the result for every test in XML format, the idea is to create a single XML file that contains the combined results from all the individual tests and to merge this file into the CC.NET build report.

1 Dim arTestCases, qcServer, qcUser, qcPassword 2 Dim testCasesFile, resultsDir, resultsSummaryFile 3 Dim fso, folder, subFolders, testResults, testCases 4 Dim qtApp, qtTest, qtResults 5 Dim testsPassed 6 7 testCasesFile = WScript.Arguments(0) 8 resultsDir = WScript.Arguments(1) & "\" 9 resultsSummaryFile = WScript.Arguments(2) 10 qcServer = WScript.Arguments(3) 11 qcUser = WScript.Arguments(4) 12 qcPassword = WScript.Arguments(5) 13 14 Set fso = CreateObject("Scripting.FileSystemObject") 15 Set testCases = fso.OpenTextFile(testCasesFile) 16 Set testResults = fso.CreateTextFile(resultsSummaryFile, True) 17 18 ' Build an array of test cases to use 19 arTestCases = Split(testCases.ReadAll, vbCrLf) 20 21 testCases.Close 22 Set testCases = Nothing 23 24 ' Launch QuickTest 25 Set qtApp = CreateObject("QuickTest.Application") 26 qtApp.Launch 27 qtApp.Visible = False 28 qtApp.Options.Run.RunMode = "Fast" 29 qtApp.Options.Run.ViewResults = False 30 31 Set qtResults = CreateObject("QuickTest.RunResultsOptions") 32 33 ' Connect to Quality Center 34 qtApp.TDConnection.Connect qcServer, "CCNET.DEMO", "CCNet.Demo", qcUser, qcPassword, False 35 If Not qtApp.TDConnection.IsConnected Then 36 WScript.Echo "Could not connect to Quality Center!" 37 WScript.Quit 1 38 End If 39 40 testsPassed = True 41 testResults.WriteLine("<QtpResults>") 42 43 ' Iterate through all the test cases 44 testsPassed = RunTests(arTestCases, resultsDir, testResults) 45 46 ' Close the output file 47 testResults.WriteLine("</QtpResults>") 48 testResults.Close 49 50 Set fso = Nothing 51 Set testResults = Nothing 52 53 If Not qtApp Is Nothing Then 54 ' Disconnect from Quality Center 55 qtApp.TDConnection.Disconnect 56 57 ' Close QuickTest Professional. 58 qtApp.Quit 59 End If 60 61 ' Release the created objects. 62 Set qtResults = Nothing 63 Set qtTest = Nothing 64 Set qtApp = Nothing 65 66 If (Err.Number > 0) Then 67 WScript.Echo "Run-time error occured!! " & Err.Description 68 WScript.Quit 1 69 ElseIf Not testsPassed Then 70 WScript.Echo "Tests Failed!!" 71 WScript.Quit 1 72 End If 73 74 Function RunTests(ByVal arTestCases, ByVal resultsDir, ByVal testResults) 75 76 Dim success, index 77 78 On Error Resume Next 79 success = True 80 81 ' Loop through all the tests in the arTestCases. 82 For index = 0 To UBound (arTestCases) - 1 83 If Len(arTestCases(index)) > 0 Then 84 ' Display Status 85 WScript.Echo "Running Test Case: " & arTestCases(index) 86 87 ' Open the test in read-only mode 88 qtApp.Open arTestCases(index), True 89 90 ' Get the test object and set the results location 91 Set qtTest = qtApp.Test 92 qtTest.Settings.Resources.ObjectRepositoryPath = "[QualityCenter] Subject\CCNet.Demo\Object Repository\CCNet.Demo_Obj_Rep.tsr" 93 qtTest.Settings.Resources.Libraries.RemoveAll 94 qtTest.Settings.Resources.Libraries.Add("[QualityCenter] Subject\CCNet.Demo\Functions\CCNet.Demo_Functions.txt") 95 96 ' This statement specifies a test results location. 97 qtResults.ResultsLocation = resultsDir & qtTest.Name 98 99 ' Execute the test. Instruct QuickTest Professional to wait for the test to finish executing. 100 qtTest.Run qtResults 101 102 ' Display Status 103 WScript.Echo vbTab & "Test Case finished: " & qtTest.LastRunResults.Status 104 105 success = success And (qtTest.LastRunResults.Status = "Passed" Or qtTest.LastRunResults.Status = "Warning") 106 107 ' Close the test. 108 qtTest.Close 109 110 ' Strip the unrequired results and append the test results to the overal result file 111 FormatTestResults fso, testResults, qtResults.ResultsLocation 112 End If 113 Next 114 115 RunTests = success 116 117 End Function 118 119 Sub FormatTestResults(ByVal fso, ByVal outputResults, ByVal testCaseDir) 120 121 Dim found, line, fileStream 122 123 Set fileStream = fso.OpenTextFile(testCaseDir & "\Report\Results.xml") 124 125 Do 126 line = fileStream.ReadLine 127 found = (InStr(1, line, "<Report", vbTextCompare) <> 0) 128 Loop While (found = False) 129 130 outputResults.WriteLine line 131 Do 132 outputResults.WriteLine(fileStream.ReadLine) 133 Loop While (fileStream.AtEndOfStream = False) 134 135 fileStream.Close 136 Set fileStream = Nothing 137 138 End Sub

Observations:

  • Lines 7-12:  The script file receives the files and Quality Center (QC) credentials to operate on as command line arguments.  The arguments include a file that contains the list of test cases to run; the directory to store the test results in; the results summary file that will be used to aggregate all the individual test results and lastly the QC server and QC user logon credentials.
  • Lines 14-22: We use the Scripting.FileSystemObject to build an array that contains the names of all the test cases to run.   As all the test cases are stored on our QC Server, we create a file that contains the server path of all the QC test cases to run, i.e. [QualityCenter] Subject\CCNet.Demo\Executable Scripts\Create Intermediaries.  We store a single test case per line and we parse the file to create an array of test cases to run.
  • Lines 25-38: We use the COM Automation model of QTP to start QTP and connect to the QC server.  If we are unable to connect to the QC server, we exit the script with a non-zero exit code.
  • Lines 41+48: The individual test results are added to a <QtpResults> tag within the QTP results summary file.
  • Lines 44, 74-113: We iterate through the test cases contained within the array and run these using the QTP automation objects.  For every QTP test, we open the test case, set the test result location and run the test to finally append the test result (stored in a Report\result.xml file) to the overall result summary file.
  • Lines 111, 119-138: The xml file produced by QTP contains a lot of unrequired DTD information that we do not want to include in the test result summary file.  We use the FormatTestResults procedure to strip this information from the file before appending the remainder of the test results to the overall test result summary file.
  • Lines 66-68: If a Windows Script Error occurred, we indicate this to the calling process by exiting the script with a non-zero exit code.
  • Lines 69-72: If one of the tests failed, we indicate this to the calling process by exiting the script with a non-zero exit code.

Qtp

As the RunQTP.vbs script file takes care of all of the QTP automation, all that we need to do is to invoke the script file using Windows Script Host.

1 <Target Name="Qtp" 2 Condition=" '$(CCNetProject)' != '' "> 3 4 <Error Text="No QCUser has been specified" Condition="'$(QCUser)' == ''" /> 5 <Error Text="No QCPassword has been specified" Condition="'$(QCPassword)' == ''" /> 6 7 <Message Text="$(NEW_LINE)Running QTP Regression Tests" Importance="high"/> 8 9 <CreateItem Include="$(QtpResultsFolder)\$(CCNetProject)"> 10 <Output TaskParameter="Include" ItemName="QtpOutputFolder"/> 11 </CreateItem> 12 13 <!-- Clear existing test results --> 14 <RemoveDir Directories="$(QtpOutputFolder)"/> 15 <MakeDir Directories="$(QtpOutputFolder)"/> 16 17 <!-- Run the QTP tests --> 18 <Exec Command="cscript.exe /nologo $(DOUBLE_QUOTES)$(QtpTestCasesFolder)\$(CCNetProject).TestCases.txt$(DOUBLE_QUOTES) $(DOUBLE_QUOTES)$(QtpOutputFolder)$(DOUBLE_QUOTES) $(DOUBLE_QUOTES)$(QtpOutputFolder)\$(QtpResultsSummaryFile)$(DOUBLE_QUOTES) $(DOUBLE_QUOTES)$(QCServer)$(DOUBLE_QUOTES) $(QCUser) $(QCPassword)" 19 ContinueOnError="true"> 20 <Output TaskParameter="ExitCode" ItemName="QtpExitCode"/> 21 </Exec> 22 23 <!-- Copy the QTP test results for the CCNet build before we possibly fail the build because of Qtp test failures --> 24 <CallTarget Targets="CopyQtpResults" /> 25 26 <!-- Fail the build if any test failed --> 27 <Error Text="Qtp test error(s) occured" Condition=" '%(QtpExitCode.Identity)' != '0'"/> 28 29 </Target>

Observations:

  • Lines 9-11: We create a unique test results output directory based on the name of the test suite that is being run.
  • Lines 13-15: We clear the old test results by deleting the test results output folder.
  • Line 18-21: Windows Script host is invoked to execute the RunQTP.vbs script file.  The test cases file, test result folder and test results summary file as well as Quality Center credentials are passed as command line parameters.  ContinueOnError is set to true and the result of the test run is stored in a QtpExitCode property.
  • Line 24: The CopyQtpResults target (see below) is invoked manually to copy the test results for inclusion in the CC.NET build report.
  • Line 27: We fail the build if any of the tests failed.

CopyQtpResults

1 <Target Name="CopyQtpResults" 2 Condition=" '$(CCNetProject)' != '' "> 3 4 <CreateItem Include="$(CCNetArtifactDirectory)\$(QtpResultsSummaryFile)"> 5 <Output TaskParameter="Include" ItemName="ExistingQtpResults"/> 6 </CreateItem> 7 8 <Delete Files="@(ExistingQtpResults)"/> 9 <Copy SourceFiles="$(QtpOutputFolder)\$(QtpResultsSummaryFile)" 10 DestinationFolder="$(CCNetArtifactDirectory)" 11 ContinueOnError="true"/> 12 13 </Target>

Observations:

  • Line 2: We test on the CCNetProject property to determine if the build was invoked via CC.NET.  The CCNetProject property is one of the properties created and passed on by CC.NET when using the MSBuild CC.NET task. 
  • Lines 4-8: The existing Qtp results summary file is deleted.
  • Line 9: The new Qtp results summary file is copied to the CCNetArtifactDirectory.

CruiseControl.NET Configuration

One dilemma you will face with the CC.NET configuration is that the current deployment of CC.NET does not include any style sheets for formatting the QTP results to display as part of the Web dashboard reports.  QTP itself uses different style sheets to format its own xml test results, so one option is to tweak their style sheets to conform to the CC.NET style sheet requirements.  Another option is to use the style sheets included by Owen Rogers in a post on the CC.NET Google user group - search for a post with the title "QuickTest Pro Integration".   Regardless of what style sheets you choose, remember to include these style sheets into the dashboard.config to display it on the webdashboard and also in either the ccservice.exe.config/ccnet.exe.config for publishing via the EmailPublisher.

The QtpServer uses the following CC.NET configuration.  The config is really self explanatory if you are familiar with the different CruiseControl.NET configuration options. 

1 <!DOCTYPE cruisecontrol [ 2 <!ENTITY pub "<statistics> 3 <statisticList> 4 <statistic name='Qtp Success' xpath='sum(//QtpResults/Report/Doc/Summary/@passed)' /> 5 <statistic name='Qtp Warnings' xpath='sum(//QtpResults/Report/Doc/Summary/@warnings)' /> 6 <statistic name='Qtp Errors' xpath='sum(//QtpResults/Report/Doc/Summary/@failed)' /> 7 </statisticList> 8 </statistics> 9 10 <xmllogger /> 11 12 <email from='qtpserver@yourdomain.co.za' mailhost='smtp.yourdomain.co.za' includeDetails='true'> 13 <users> 14 <user name='BuildGuru' group='buildmaster' address='developer1@yourdomain.co.za'/> 15 <user name='Developer2' group='developers' address='developer2@yourdomain.co.za'/> 16 <user name='QtpTester' group='qtptesters' address='qtptester1@yourdomain.co.za'/> 17 </users> 18 <groups> 19 <group name='developers' notification='change'/> 20 <group name='buildmaster' notification='always'/> 21 <group name='qtptesters' notification='always'/> 22 </groups> 23 </email>"> 24 25 <!ENTITY links "<externalLinks> 26 <externalLink name='Wiki' url='http://wikiserver.yourdomain.co.za/wiki' /> 27 </externalLinks>"> 28 29 <!ENTITY build "<tasks> 30 <msbuild> 31 <executable>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe</executable> 32 <workingDirectory>C:\Projects\CCNet.Demo</workingDirectory> 33 <projectFile>CCNet.Demo.proj</projectFile> 34 <buildArgs>/noconsolelogger /v:normal /p:QCUser=fred;QCPassword=password</buildArgs> 35 <targets>Uninstall,Install,Qtp</targets> 36 <timeout>14400</timeout> <!-- 4 hours --> 37 <logger>C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MSBuild.dll</logger> 38 </msbuild> 39 </tasks>"> 40 41 <!ENTITY header "<webURL>http://qtpserver.yourdomain.co.za/ccnet</webURL> 42 <category>RegressionTesting</category> 43 <workingDirectory>_dir</workingDirectory>"> 44 45 ]> 46 <cruisecontrol> 47 48 <project name="TestSuite1" queue="Qtp.CCNet.Demo" queuePriority="1"> 49 &header; 50 <artifactDirectory>_dir\Builds\TestSuite1\Artifacts</artifactDirectory> 51 52 <triggers> 53 <projectTrigger serverUri="tcp://buildserver.yourdomain.co.za:21234/CruiseManager.rem" project="DeploymentBuild"> 54 <triggerStatus>Success</triggerStatus> 55 <innerTrigger type="intervalTrigger" seconds="30" buildCondition="ForceBuild"/> 56 </projectTrigger> 57 </triggers> 58 59 <state type="state" /> 60 61 <labeller type="defaultlabeller"/> 62 63 &build; 64 65 <publishers> 66 <merge> 67 <files> 68 <file>_dir\Builds\TestSuite1\Artifacts\QtpResultsSummary.xml</file> 69 </files> 70 </merge> 71 72 &pub; 73 </publishers> 74 75 &links; 76 </project> 77 78 <project name="TestSuite2" queue="Qtp.CCNet.Demo" queuePriority="2"> 79 &header; 80 <artifactDirectory>_dir\Builds\TestSuite2\Artifacts</artifactDirectory> 81 82 <triggers> 83 <projectTrigger serverUri="tcp://qtpserver.yourdomain.co.za:21234/CruiseManager.rem" project="TestSuite1"> 84 <triggerStatus>Success</triggerStatus> 85 <innerTrigger type="intervalTrigger" seconds="30" buildCondition="ForceBuild"/> 86 </projectTrigger> 87 </triggers> 88 89 <state type="state" /> 90 91 <labeller type="defaultlabeller"/> 92 93 &build; 94 95 <publishers> 96 <merge> 97 <files> 98 <file>_dir\Builds\TestSuite2\Artifacts\QtpResultsSummary.xml</file> 99 </files> 100 </merge> 101 102 &pub; 103 </publishers> 104 105 &links; 106 </project> 107 108 </cruisecontrol>

Observations:

  • Lines 4-6: We use the Statistics provider to include some additional statistics for every build that includes the number of QTP tests that failed, number of tests that passed and the number of tests that raised warnings.
  • Line 35: The build uninstalls and installs the new version of the application that has been copied onto a share on the local machine before running Qtp.
  • Line 52-57: We setup TestSuite1 to fire once a successful DeploymentBuild has been completed.
  • Line 82-87: We setup TestSuite2 to fire once a successful TestSuite1 has completed.
  • Line 69+98: The combined test results summary file is merged into the CC.NET build report.

Using the above mentioned configuration it is quite possible to setup additional QTP builds on the same and different machines to create a grid of machines to run through all of your test cases more quickly.  Every build will run through a different set of test cases as identified by different Qtp test case files.  In our setup at work we have 3 machines running through all our test cases concurrently.  I leave that as a simple exercise for the reader to complete.

Next Steps

This is probably the most complex part of the whole CI process, but thankfully the COM automation interface provided by QTP makes it not too difficult to automate the whole regression test run.  The next and final post will highlight some community extensions that you can use to add some further panache to your CI Build to make it even more useful. smile_regular

 

 

 

 

 

 

 

Comments

# From the software development trenches said on November 7, 2007 10:00 AM:

This is the second post in a series where I document how to setup a Continuous Integration (CI) process

# From the software development trenches said on November 7, 2007 10:59 AM:

During the past few months I created a series of posts about setting up Continuous Integration in a

# From the software development trenches said on November 8, 2007 04:06 PM:

Performance is a very critical non-functional requirement for the SmartClient application that we are

# Part 1: Continuous Integration using MSBuild, CruiseControl.NET, FxCop, NUnit, NCover + Subversion - From the software development trenches said on November 14, 2007 08:49 PM:

Pingback from  Part 1: Continuous Integration using MSBuild, CruiseControl.NET, FxCop, NUnit, NCover + Subversion - From the software development trenches

# From the software development trenches said on December 11, 2007 10:41 PM:

This is the sixth post in a series where I document how to setup a Continuous Integration (CI) process

# Running HP QuickTest Professional UI tests with Team Build « Grant Holliday said on February 28, 2008 11:06 PM:

Pingback from  Running HP QuickTest Professional UI tests with Team Build &laquo; Grant Holliday

# http://dotnet.org.za/cjlotz/archive/2007/09/17/part-7-continuous-integration-the-qtpbuild.aspx said on March 24, 2008 07:54 AM:

Pingback from  dotnet.org.za/.../part-7-continuous-integration-the-qtpbuild.aspx

# Ram said on May 13, 2008 07:41 PM:

Hi there- will this work with the Starter Edition of QC or do we need to have the Enterprise edition?

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above: