Integration Test Structure Recommendation Guide
On this page we want to share with you how we run automated integration tests for our modules on a buildserver. This article explains the current state of our structure and might be changed in future.
We're running the 2.x version of Jenkins on our buildserver, with the Pipeline and Bitbucket Branch Source Plugin and we're using Atlassian Bitbucket for our Source Code Management.
Project structure
Our repositories containing the modules have a specific structure, here exemplary for the SMTP module:
-
/ (root directory of the checked out project)
-
/smtp/
-
/smtp-integrationtest/
-
/Jenkinsfile
-
As you can see, we use the Jenkinsfile feature to define the build pipeline.
Besides that file we have one folder containing the normal content of the module (such as src folder, pom.xml , etc.) and one folder named by the module name postfixed with "-integrationtest".
The latter folder contains a simple Maven project, which has no files in the src/main folder and contains only the test classes in the src/test folder. The tests are simple JUnit tests which are doing HTTP-Requests against our ApiOmat REST API. We can provide you our integration-helper library with classes that help you to make common requests against our API (like creating a backend, changing a module's configuration etc.). Feel free to contact us for that. The integration-helper is available on Maven Central.
If you're using the integration-helper library in your test cases and added it as a maven dependency to the pom.xml, you have to make sure that you've installed the helper library to your local repository.
To create such a project, you can either create a new Maven project in Eclipse and check the "Create simple project (Skip archetype selection)" box in the Dialog, or you create a project using Maven with the following command and remove the generated Java files afterwards.
mvn archetype:generate -DgroupId=com.apiomat.nativemodule.integrationtest -DartifactId=smtp-integrationtest -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Of course, you have to replace the values for the groupId and artifactId according to your project. After that you can start to write your tests in the src/test folder.
Build structure
For now, we have two different build jobs for the modules.
The Module Build Job
The first build job uses our maven integration and runs mvn clean package in the module folder and hence it will run all your simple unit tests contained in the test folder in the root of your module directory (in our example above: /SMTP/test) in the Maven phase test and then create the module jar in the target folder (in the example /SMTP/target/smtp-1.0.0-NM.jar). We've set up the Jenkinsfile for that build to publish the created module jar in the end of the process.
The build job item is created as "Bitbucket Team/Project" item and will scan for Jenkinsfiles in all repositories of the specified project on our Bitbucket server. Therefore, every module with a Jenkinsfile will automatically be added to the builds.
Our default Jenkinsfile downloads a template, which is publicly available on github, and loads it afterwards.
Jenkinsfile for the module artifact generation:
node { stage ( 'Checkout' ) { checkout scm } sh 'wget -O Jenkinsfile.template https://raw.githubusercontent.com/ApinautenGmbH/ModuleJenkinsfileTemplate/master/Jenkinsfile.template' load 'Jenkinsfile.template'}This template currently contains an additional Test stage where the implemeneted integrationtests of your project structure were used to run against an instance of ApiOmat Yambas.
The Modules Test Job
Another build copies the produced artifact of all modules (see module build job) to the workspace of the modules test build. After that all modules are uploaded and released to the running test instance of ApiOmat. Then the mvn test command will be executed for the <modulename>-integrationtest Maven project.
The Jenkinsfile for that build is located in another repository and also contains a simple script to deploy the module (including: creating the customer and application if necessary, creating and uploading the module, adding the module to the application, releasing the module and activate the application).
Jenkinsfile for integrationtests:
... main file that contains the definition of this build job and orchestrates all needed scripts and results.
import jenkins.*import jenkins.model.*import hudson.*import hudson.model.*/* request the jenkins API to get all native module builds to process */@NonCPSdef getNativeModulesBuilds() { def moduleNames = []; def authString = "jenkinsusername:password".getBytes().encodeBase64().toString() def url = "http://@buildserver/job/Modules/api/json"; def conn = url.toURL().openConnection(); conn.setRequestProperty( "Authorization", "Basic ${authString}" ) if( conn.responseCode == 200 ) { modules = new groovy.json.JsonSlurperClassic().parseText( conn.content.text ); for ( job in modules.jobs ) { moduleNames.put( job.name, dir ); } echo( "Found module names " + moduleNames ) versions = null } else { echo( "Something bad happened." ) echo( "url: ${url} - ${conn.responseCode}: ${conn.responseMessage}" ) build.status = "FAILURE" } conn = null return moduleNames}/* this method deploys the given module and runs the integration tests */def testModule( moduleName, realModuleName ) { echo ( "testModule $moduleName" ) // deploy step( [$class: 'CopyArtifact', filter: '**/*.jar', fingerprintArtifacts: true, flatten: true, target: 'dist', projectName: "Modules/$moduleName", selector: [$class: 'StatusBuildSelector', stable: true]] ) /* you may have to adapt the url to your ApiOmat instance */ exitCode = sh returnStatus: true, script: "./deployModule.sh 'http://localhost:8000' '$realModuleName'" if( exitCode != 0 ) { throw new Exception( "Failed to deploy module $realModuleName" ) } // test def folderName = "Modules/${moduleName}/${moduleName}-integrationtest" dir( "$folderName" ) { /* you may have to adapt the url to your ApiOmat instance */ sh returnStatus: true, script: "mvn -B -Dmaven.test.failure.ignore=true -DyambasHost=http://localhost:8000 -DyambasTestHost=http://localhost:8000 clean integration-test" }}stage 'Selector' properties ([ buildDiscarder(logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '', numToKeepStr: '20')), pipelineTriggers([]), parameters( [ /* define your build parameters here */ ] ) ])node { try { stage ( 'Setup VM' ) { /* do your VM setup here where you boot up your yambas testing instance */ } stage ( 'Module Tests' ) { sh 'mkdir -p dist' sh 'mkdir -p Modules' def moduleNames = getNativeModulesBuilds() sh 'chmod 777 getUsedModulesFromPom.sh' sh 'chmod 777 getRealModuleName.sh' def dependingModuleRealNames = [] def dependingModuleNames = [] def oldSize = 0 while ( dependingModuleRealNames.size() < moduleNames.size() ) { oldSize = dependingModuleRealNames.size() for( moduleName in moduleNames ) { if( dependingModuleNames.contains( "${moduleName}" ) ) { echo ( "Module already in list: ${moduleName}" ) continue; } try { // checkout module, for later tests and real module name /* you may have to adapt the branch name and url to your repository here */ checkout([$class: 'GitSCM', branches: branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: "Modules/$moduleName"]], userRemoteConfigs: [[url: "ssh://yourrepository/nm/${moduleName}.git"]]]) // get real module name /* you may have to adapt the path according to the workspace path for that build on your buildserver */ realModuleName = sh returnStdout: true, script: "./getRealModuleName.sh 'Modules/${moduleName}/${moduleName}/src/com/apiomat/nativemodule/${moduleName}' '${moduleName}'" // get used modules def fileName = "" def fileName = "Modules/${moduleName}/${moduleName}/pom.xml" usedModulesStr = sh returnStdout: true, script: "./getUsedModulesFromPom.sh $fileName" echo ( "Found used modules for module ${moduleName}: '$usedModulesStr'" ) def usedModules = usedModulesStr.split( ',' ) // look if depended modules are already added to the list contained = true for( um in usedModules ) { def depModule = um.replaceAll( ',', '' ) if( depModule != "Mandrill" && depModule != "" && depModule.startsWith( "documentationURL" ) == false ) { contained &= dependingModuleRealNames.any{it.toLowerCase().contains( "$depModule".toLowerCase() )} } } echo ( "Are all used modules already contained in list? Result: $contained" ) if( contained ) { dependingModuleRealNames.add( "$realModuleName" ) dependingModuleNames.add( "${moduleName}" ) } } catch( Exception e ) { echo ( "Could not use module ${moduleName}: " + e.getMessage() ) } } if( oldSize == dependingModuleRealNames.size() ) { echo( "No further dependency found!" ) break } } echo ("================================================================================="); echo ("Testing modules: ${dependingModuleNames}") echo ("================================================================================="); for( moduleName in dependingModuleNames ) { try { echo ( "Testing Modules/$moduleName" ) // get real module name /* you may have to adapt the path according to the workspace path for that build on your buildserver */ realModuleName = sh returnStdout: true, script: "./getRealModuleName.sh 'Modules/$moduleName/$moduleName/src/com/apiomat/nativemodule/$moduleName' '$moduleName'" testModule( moduleName, realModuleName ) } catch( Exception e ) { currentBuild.result = 'UNSTABLE' echo ( "Error testing module $realModuleName: " + e.getMessage() ) } } } } catch ( Exception e ) { echo ( "Error while testing the modules: " + e.getMessage() ) } finally { stage ( 'Cleanup VM' ) { /* clean up your created vm */ } stage ( 'Test results' ) { junit '**/surefire-reports/**/*.xml' } }}
deployModule.sh:
... is used to create customer and application such aus to deploy and to release the module
#!/bin/bashecho "starting deployModule.sh..."curlOpts="--write-out %{http_code} --silent --output /dev/null"# adapt the values according to your needscustomer=yourtestcustomeradminEmail="$customer@yourcompany.com"adminPassword="yourpassword"customerAuth="$adminEmail:$adminPassword"superadminEmail=yoursuperadmin@yourcompany.comsuperadminPassword=yoursuperadminpasswordappName=ModuleTestApplicationhostName=$1if [ -z "$hostName" ]then hostName="https://localhost:8080"fihost="$hostName/yambas/rest"outDir=$2if [ -z "$outDir" ]then outDir="./"fimoduleName="$3"if [ -z "$3" ]then echo -e "No modulename provided!" exit 1fiecho -e "--- Ready to rumble ---\nHost: $host\nOutDir: $outDir\nModule:$moduleName\n-------------"#check if customer (apps admin) exhttp_code=$(curl $curlOpts -X GET $host/customers/$customer -u $superadminEmail:$superadminPassword -H "Accept:application/json")echo "Get Customer: $http_code"if [ $http_code -ne 200 ]then #create customer http_code=$(curl $curlOpts -X POST $host/customers -d name=$customer -d email=$adminEmail -d password=$adminPassword -u $superadminEmail:$superadminPassword) echo "Create Customer Return: $http_code"fi#check if app exists and deletehttp_code=$(curl $curlOpts -X GET $host/customers/$customer/apps/$appName -u $customerAuth -H "Accept:application/json")echo "GET App: $http_code"if [ $http_code -eq 200 ]then http_code=$(curl $curlOpts -X DELETE $host/customers/$customer/apps/$appName -u $customerAuth -H "Accept:application/json") echo "Deleted App: $http_code"fi#create app with given nameapp_href=$(curl --silent -i -X POST $host/customers/$customer/apps -d name=$appName -u $customerAuth -H "Accept:application/json" | awk '/^Location:/ { print $2 }' | sed -e 's/[[:cntrl:]]//')if [ -z $app_href ]then echo "Can't find APP. abort" exit 1fi#get APP JSONecho "App Href: $app_href"app_json=$(curl --silent -X GET $app_href -u $customerAuth -H "Accept:application/json")#echo "$app_json"app_name=$(expr "$app_json" : '.*"applicationName":"\([^"]*\)"')appName=${app_name} api_key_live=$(echo $app_json | jq -r '.apiKeys.liveApiKey')echo "API KEY: $api_key_live"rm -f /tmp/apikeyecho $api_key_live >> /tmp/apikey# set app activehttp_code=$(curl $curlOpts -i -X PUT -H "Content-Type: application/json" -H "X-apiomat-system: LIVE" -u $customerAuth -d "{\"applicationStatus\":{\"LIVE\": \"ACTIVE\"}}" $app_href)if [ $http_code -eq 200 ]then echo "$moduleName module released"else echo "Can't release $moduleName module: $http_code" exit 1;fi# deploy module if [ ! -e "dist/$moduleName.jar" ]then echo "Module file does not exist at dist/$moduleName.jar!" exit 1;fihttp_code=$(curl $curlOpts -i -X POST -H "Content-Type: application/octet-stream" -u $customerAuth --data-binary @"dist/$moduleName.jar" $host/modules/$moduleName/sdk?update=OVERWRITE)if [ $http_code -eq 200 ]then echo "$moduleName module deployed"else echo "Can't deploy $moduleName module: $http_code" exit 1;fi# release modulehttp_code=$(curl $curlOpts -i -X PUT -H "Content-Type: application/json" -H "X-apiomat-system: LIVE" -u $customerAuth -d "{\"releaseState\": \"RELEASED\"}" $host/modules/$moduleName)if [ $http_code -eq 200 ]then echo "$moduleName module released"else echo "Can't release $moduleName module: $http_code" exit 1;fi
getRealModuleName.sh:
... is used to retrieve the correct case-sensitive name of the module
#!/bin/bash# 1. read file list from directory ($1=directory)# 2. find module class by ignoring case ($2=moduleName)# 3. remove trailing file ending# 4. delete last linebreakls "$1" | grep -i "${2}.java" | sed s/\.java//g | tr -d '\n'
getUsedModulesFromPom.sh:
... is used to retrieve all used module relations from pom.xml of the module
#!/bin/bash# 1. read in pom.xml# 2. search for all groupid com.apiomat.nativemodule and get next line with artifact# 3. trim trailing an leading spaces# 4. remove artifact tags# 5. remove first line because it is the parent module itself# 6. replace linebreaks by commas# 7. delete last linebreakcat "$1" | awk '/<groupId>com.apiomat.nativemodule<\/groupId>/{getline; print}' | awk '{$1=$1};1' | sed s/\<artifactId\>//g | sed s+\</artifactId\>++g | tail -n +2 | sed 'H;1h;$!d;x;y/\n/,/' | tr -d '\n'
Note on Scripts
Please note that these scripts should only give you an idea about how to set up automatic integration tests with a Jenkins buildserver.
They are highly dependant to our own infrastructure and configuration and probably won't work with your setup out of the box, but can save some time for thoughts about the general processes and some pitfalls (e.g. the used modules).