Case Study for Android CI -> CD -> CD = Continuous * ( Integration, Delivery, Deployment ) Part 3
Designed By Hafiz Waleed Hussain
WOW, we got one more day so it’s time to make this day awesome by learning something new. 🙂
Hello friends, hope you are doing good. So today we are going to start our part 3.
Motivation:
Here motivation is same which I already shared with you guys in part1.
Plan:
So, for my readers below is my plan for this case study.
- Optional Prerequisites
- What is Continuous Integration ( CI), Continous Delivery ( CD), Continous Deployment ( CD )?
- Version control system ( VCS )
- Continuous integration server
- Build System
- Application code on Github
- Git structure for the code
- Unit Tests
- Functional Tests
- Gradle build system and it’s commands
- Jenkins integration
- Part 3
- Web-hook
- Unit Test Report
- Unit Test Code Coverage Report
- Lint rules ( Skip )
- Artifacts
- Release notes
- Functional / UI / Espresso Unit Test Report
- Functional / UI / Espresso Unit Test Code Coverage Report
- Notifications may be on slack, hip chat
- Deliver APK using Fabric
- Deployment on play store beta
- Tag creation after publish
- Merge code with the master branch
- Part 5
- Case study ( App has three different flavors )
- Git structure for the code
- Unit Tests
- Functional Tests
- Acceptance Tests
- Gradle build system and it’s commands
Code for part 3 is available on Github branch part3.
Web-hook:
For those who are using this concept first time. This is the process in which we want once someone will do a push or pull request merged into our code base. Our Jenkins build should start automatically. To achieve this process we used Web-hook. In our case, we want when someone does Push code into a branch, Jenkins should run automatically our build.
“Web-hooks allow you to build or set up GitHub Apps which subscribe to certain events on GitHub.com. When one of those events is triggered, we’ll send an HTTP POST payload to the web-hook’s configured URL. Web-hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You’re only limited by your imagination.” GitHub
As we can see there is a communication between Jenkins and GitHub. So we need to implement things in both places.
First, on Jenkins, we need to install GitHub Integration plugin steps are shown below:
As you can see in above steps we installed new plugin successfully.
It’s time to prepare our GitHub repository for Web-hook. So open your GitHub repo and go to settings.
Now there is a problem :(. As you can see in above image. We need to give our Jenkins URL. But if you guys are new in CI, CD, CD and following my tutorial you are using localhost:8080 which is not a public URL. So we are not able to give here. Now, one thing I can skip this step by saying you need to give a public URL. So, you guys need to install Jenkins on some public host. Which I think will be a demotivation for a lot of newcomers. Now, I am going to use something which is called “ngrok”. This will help us to make our localhost URL into Public URL. Only I want to tell you, I don’t know about this a lot. So you should be careful. Download ngrok from the URL. https://ngrok.com/download
Once you download open your terminal and go into the directory where you downloaded that file and gave below command to test.
Next, we need to map our local URL to public URL for that gave the command:
If everything goes right, you will get your public URL in a terminal as shown below:
Now you can copy HTTP or https URL and run on a browser. You can see your Jenkins dashboard there. HURRAY
Now go back to GitHub and gave your URL there into web-hook location.
Now add this service and your this step will be complete.
Everything is done in perspective of Web-hook. Only we need to enable our Web-hook. For that open Jenkins — -> Configurations — -> Build Triggers — -> GitHub hook trigger for GIT Scm polling
It’s time to show you how this will work. So I did some changes in the development branch. After that, I pushed my code into development. As push is completed my Jenkins build start running automatically without any human intervention as shown below:
So you can see in above gif. There is build 4 starts.
Only one problem is here. From now anyone does a push in any branch Jenkins will start your build. But for the time being, played with Web-hook this issue we will resolve in next parts.
Unit Test Report:
As we already know to run unit tests we used command “testDebugUnitTest”. Once we run this command Gradle will generate a report of unit tests. Which we can see in app/build/reports/tests/index.html as shown below:
If you open above index.html into your browser you can see your unit test report.
Now it’s time to integrate this report with our Jenkins server.
Here I am going with a very simple approach but you can use any test report plugin if you want :). So, open your Jenkins — -> Manage Jenkins → Manage Jenkins — -> Available — -> HTML Publisher and download this plugin as shown below:
Once the installation is complete. Open the configuration of our build on Jenkins and fellow below steps:
Now add our path into HTML directory field as shown below: ( You can give your Report title what you want )
After saving, run Jenkins build:
Once I click Build Now, 5 number build start running. Now wait, once build process successfully complete you can see your Unit Test report on Jenkins as shown below:
Congratulations, Now Unit Test report is completed.
Unit Test Code Coverage Report:
If you don’t know what is Code coverage. You can search on Wikipedia for its definition. In simple words, we can say how many lines of code is going or pass through Unit Tests. Your confusion will be removed once we will see this report. It’s time to implement this concept.
Here we need to do some changes in our build.gradle file as shown below:
- apply plugin jacoco
- added debug build type with testCoverageEnabled true
- added testOptions into an android block
- added jacoco toolVersion
- added a custom task
Here first four changes are really easy to understand. But the fifth change is really complex if you are not good in gradle. Instead truly saying when I implemented first time this CI, CD, CD I copied this script from StackOverflow and later it takes time for me to learn what is going on in this script. So I have the same suggestion for you guys don’t try to learn this task now instead later you can learn. But I am going to copy paste whole gradle file below with custom task.
apply plugin: 'com.android.application'
// Kotlin related dependencies
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'apply plugin: 'jacoco'
android {
compileSdkVersion 26
buildToolsVersion '27.0.3'
defaultConfig {
applicationId "uwanttolearn.dagger2"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}// Currently Added for Retro lambda
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}buildTypes {
debug{
testCoverageEnabled true
debuggable true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
}
}
}
}jacoco {
toolVersion "0.7.1.201405082137"
}dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
exclude group: 'com.google.code.findbugs'
})
androidTestImplementation ("com.android.support.test.espresso:espresso-intents:2.2.2",{
exclude group: 'com.google.code.findbugs'
})androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'// Retrofit 2 for API call's
implementation 'com.squareup.retrofit2:retrofit:2.3.0'// JSON Converter. Which I used with Retrofit.
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'// Rx Java 2. I used for callback in adapters + with retrofit.
implementation 'io.reactivex.rxjava2:rxjava:2.0.2'// Rx Android mostly used for AndroidSchedulers.mainThread()
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'// Retrofit 2 work with Rx Java2. Only we need to add below adapter to make a connection
// between these two libs
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'// This is very useful to show all the response from the API to Logcat.
implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'// Dagger 2
implementation 'com.google.dagger:dagger:2.11'
annotationProcessor 'com.google.dagger:dagger-compiler:2.11'// Dagger 2 with Kotlin
kapt 'com.google.dagger:dagger-compiler:2.11'implementation 'com.github.bumptech.glide:glide:3.8.0'implementation 'com.android.support:recyclerview-v7:26.0.2'
implementation 'com.android.support:cardview-v7:26.0.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.3-2"implementation 'com.google.guava:guava:23.3-android'
// Testing
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.10.0'}
repositories {
mavenCentral()
}project.afterEvaluate {
// Grab all build types and product flavors
def buildTypes = android.buildTypes.collect { type ->
type.name
}def productFlavors = android.productFlavors.collect { flavor ->
flavor.name
}
// When no product flavors defined, use empty
if (!productFlavors) productFlavors.add('')//iterate over the flavorsproductFlavors.each {productFlavorName ->
//iterate over build types like debug,release,prod etc.
buildTypes.each {buildTypeName ->
//sourceName — e.g. freeDebug ,sourcePath — e.g. free/debug
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
// testTaskName —  e.g. testFreeDebugtest task that the coverage task depends on,
def testTaskName = "test${sourceName.capitalize()}UnitTest"
// Create coverage task of form 'testFlavorTypeCoverage' depending on 'testFlavorTypeUnitTest'
task "${testTaskName}Coverage"(type: JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: [
'**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/Manifest*.*',
'android/**/*.*',
'**/Lambda$*.class', //Retrolambda
'**/Lambda.class',
'**/ * Lambda.class',
'**/*Lambda*.class',
'**/*Lambda*.*',
'**/*Builder.*',
'**/*_MembersInjector.class', //Dagger2 generated code
'**/*_MembersInjector*.*', //Dagger2 generated code
'**/ *_ * Factory*. * ', //Dagger2 generated code
'**/*Component*.*', //Dagger2 generated code
'**/*Module*.*', //Dagger2 generated code
'io/**/*.*'
],
// include: []
) + fileTree(
dir:"${project.buildDir}/tmp/kotlin-classes/${sourcePath}",
excludes: [
'**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/Manifest*.*',
'android/**/*.*',
'**/Lambda$*.class', //Retrolambda
'**/Lambda.class',
'**/ * Lambda.class',
'**/*Lambda*.class',
'**/*Lambda*.*',
'**/*Builder.*',
'**/*_MembersInjector.class', //Dagger2 generated code
'**/*_MembersInjector*.*', //Dagger2 generated code
'**/ *_ * Factory*. * ', //Dagger2 generated code
'**/*Component*.*', //Dagger2 generated code
'**/*Module*.*', //Dagger2 generated code
'io/**/*.*'
],
// include: []
)def coverageSourceDirs = [
"src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
reports {
//enables and disable the type of file you need
xml.enabled = false
html.enabled = true
}
}
}
}
}
Once above changes are completed. You will get a new gradle command "testDebugUnitTestCoverage". As you will run this command into a terminal of your project like ./gradlew testDebugUnitTestCoverage.
This command will run first your unit tests and then that will generate a Code Coverage Report of Unit Tests. So from now, we can remove testDebugUnitTests command because new command will take care of that also. It's time to show you pictorially.
Once command successfully complete. We will get our Uni Tests and Unit Test Code Coverage Report on path. app/build/reports as shown below:
Above image is a Code Coverage Report of our Unit Tests. HURRAY.
We can see there are two colors. Red and Green. Red color means that code never tested by Unit Tests and Green code means that code pass through from Unit Test. So in this report, I can see how much code coverage we have for our code base. Now maybe for some of you that make some sense. What is 100% code coverage? Means all number of lines of code should pass through from Unit Tests and we will get all green.
Here, some companies think we should have 100% code coverage and some thinks we should do test our Presenters and ViewModels only 100% in case of Unit Tests. But I am not going into this discussion :).
Note: I am going to replace our testDebugUnitTest with testDebugUnitTestCoverage in Jenkin build section as shown below:
As we know that is same an HTML file so we can add it's path on our Jenkins as we did for Unit Tests report.
After save if web-hook is working then do push new gradle changes to git and build will start automatically and if currently, you disable web-hook then do manually build now.
Once build is completed you can see your code coverage report as shown below:
Congratulations. :)
Lint rules ( Skip ):
Sorry guys I am not going to implement this in this post. I think that is not important as other topics which we have. This is my personal opinion and due to this, I am going to next topic Artifacts.
Artifacts:
I am not going into the definition of Artifact. In simple words or in our case, APK will be our Artifact. Now we want to save that APK on Jenkins as Archive Artifact. For a revision purpose, we already know we are giving assembleDebug command to generate a Debug APK.
So in the first step, we need to find out the path of this APK. For that I am running this command into our Android Studio Terminal as shown below:
Once command successfully complete we can see our Debug APK into app/build/outputs/apk/debug/app-debug.apk
Next, we need to save this APK as an archive artifact on Jenkins. So for that, we need to open Build Configuration and add a new Post Build Action as shown below:
After saving new changes. Run a build and wait for completion.
Once build is successfully complete. You can see your APK as an Archive Artifact as shown below:
HURRAY, we successfully saved our APK on Jenkins.
Now there is one very important point. On the main page as you can see above. We always get the last APK. But if you want to download old APK you can. For example, I want to download second last APK so for that I need to click on Build 8 and go into status. There I can see my APK.
Release notes:
Every time when we want to send a build to QA or we do publish our app we mostly write our release notes. Now by using Jenkins and git hooks, I will show you there is no need to write Release Notes instead you already have your release notes. If you open your Status of any build like we see for build 8. In changes section, you can see all your commit messages. You can use these messages as release notes if you are giving proper commit message. Personally, In my time we are using these commit messages as a release notes for QA. Now, I am going to show you these messages in our Jenkins project as shown below:
Conclusion:
I hope you guys enjoyed this part also. In this part, I try to share with you knowledge about Web-hooks, Unit Test Report, Unit Test Code Coverage Report, Archive Artifacts and Release notes. OK, guys BYE BYE. Have a Nice Weekend.