Jenkins Project: Building CI/CD Pipeline for Scalable Web Applications
Jenkins
Unit testing with pytest
In this guide, we demonstrate how to set up a Jenkins Pipeline to run automated tests on a Flask application using Pytest. Follow the step-by-step instructions below to integrate your Flask project with Jenkins and ensure your code remains robust with continuous testing.
Setting Up the Jenkins Pipeline
Begin by creating a new pipeline in Jenkins for your Flask project:
In Jenkins, create a new item, name it "Flask Pipeline," and select Pipeline as the project type.
Configure the build trigger to use the GitHub hook trigger for Git SCM polling.
For the pipeline configuration, use a Jenkinsfile stored in the application's Git repository instead of defining the script directly in Jenkins. This way, Jenkins fetches its instructions from your GitHub repo.
Select Pipeline script from SCM, choose Git, and enter the URL of your Git repository. If your repository is private, be sure to provide the appropriate credentials.
Specify the branch to build as "main" (or your designated branch) if your repository contains only that branch.
By default, Jenkins will look for a file named Jenkinsfile in the repository's root directory. If your file is located elsewhere or under a different name, provide the full path accordingly. In our case, the Jenkinsfile is in the root directory.
Exploring the Application Files
Our project is a straightforward Python Flask application. Below is an overview of the key files:
Main Application Code: app.py
from flask import Flask, render_template, request
app = Flask(__name__)
# A dictionary to store tasks with an ID
tasks = {}
task_id_counter = 1
@app.route('/', methods=['GET', 'POST'])
def index():
global task_id_counter
response_text = ""
if request.method == 'POST':
if 'add_task' in request.form:
task_content = request.form.get('task_content')
if task_content:
tasks[task_id_counter] = task_content
task_id_counter += 1
elif 'delete_task' in request.form:
task_id_to_delete = int(request.form.get('task_id_to_delete'))
tasks.pop(task_id_to_delete, None)
return render_template('index.html', tasks=tasks)
Unit Tests: test_app.py
This file sets up the automated unit tests using Pytest.
import pytest
from app import app as flask_app, tasks
@pytest.fixture
def app():
yield flask_app
@pytest.fixture
def client(app):
return app.test_client()
def test_index(app, client):
response = client.get('/')
assert response.status_code == 200
def test_add_task(client):
# Adjust 'task_content' as needed based on your form data
response = client.post('/', data={'task_content': 'New Task', 'add_task': True})
assert response.status_code == 200
assert 'New Task' in tasks.values()
def test_delete_task(client):
# First, add a task
client.post('/', data={'task_content': 'Task to Delete', 'add_task': True})
task_id_to_delete = list(tasks.keys())[0]
response = client.post('/', data={'task_id_to_delete': task_id_to_delete, 'delete_task': True})
assert response.status_code == 200
assert task_id_to_delete not in tasks
Dependency File: requirements.txt
This file lists the external dependencies needed to run the Flask application:
blinker==1.7.0
cachetools==5.3.2
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
exceptiongroup==1.2.0
Flask==3.0.1
google-ai-generativelanguage==0.4.0
google-api-core==2.15.0
google-auth==2.26.2
google-generativeai==0.3.2
googleapis-common-protos==1.62.0
grpcio==1.60.0
grpcio-status==1.60.0
idna==3.6
iniconfig==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.3
MarkupSafe==2.1.4
packaging==23.2
pluggy==1.3.0
proto-plus==1.23.0
protobuf==4.25.1
pysn1==0.5.1
pysn1-modules==0.3.0
pytest==7.4.4
Use the command below to install the required packages:
pip install -r requirements.txt
HTML Template: index.html
The user interface for the Flask application is defined in the templates/index.html
file. Here is an excerpt including the relevant styling:
<html>
<head>
<style>
input[type="submit"]:hover {
background-color: #45a049;
}
ul {
list-style-type: none;
padding: 0;
}
li {
background-color: #fff;
margin-bottom: 8px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-text {
margin: 0;
}
</style>
</head>
<body>
<!-- The rest of the HTML content -->
</body>
</html>
Note
Ensure that all files are correctly committed to your repository before triggering the Jenkins pipeline.
Configuring the Jenkins Pipeline with a Jenkinsfile
Below is the initial version of the Jenkinsfile that sets up the stages of the pipeline:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git url: 'https://github.com/kodekloudhub/jenkins-project.git', branch: 'main'
sh "ls -ltr"
}
}
stage('Setup') {
steps {
sh "pip install -r requirements.txt"
}
}
stage('Test') {
steps {
sh "pytest"
}
}
}
}
This configuration consists of:
- Checkout Stage: Retrieves the source code from Git and lists the files.
- Setup Stage: Installs dependencies as specified in
requirements.txt
. - Test Stage: Executes the tests using Pytest.
After verifying that everything works locally with the tests in test_app.py
, commit your changes and push them to your repository using:
git add .
git commit -m "initial commit"
git push origin main
Jenkins will pull the code, read the Jenkinsfile, and execute the defined steps. Click on a running build in Jenkins to view the console output, which shows details such as the checkout process, dependency installation, and test results.
The console output will confirm the following sequence:
- The source code is checked out (twice if the default checkout is not skipped).
- The file list is printed using
ls -ltr
. - Dependencies are installed from
requirements.txt
. - Pytest runs and confirms that all tests pass.
Optimizing the Pipeline: Skipping the Default Checkout
To avoid the redundant default checkout, update your Jenkinsfile with the skipDefaultCheckout()
option:
pipeline {
agent any
options { skipDefaultCheckout() }
stages {
stage('Checkout') {
steps {
git url: 'https://github.com/kodekloudhub/jenkins-project.git', branch: 'main'
sh 'ls -ltr'
}
}
stage('Setup') {
steps {
sh 'pip install -r requirements.txt'
}
}
stage('Test') {
steps {
sh 'pytest'
}
}
}
}
This adjustment ensures that Jenkins performs only a single checkout, executing the steps as defined in the "Checkout" stage.
After committing and pushing these changes, your pipeline will run with the optimized configuration. Below is an excerpt of the test output:
+ pytest
============================= test session starts ==============================
platform linux -- Python 3.9.16, pytest-7.4.4, pluggy-1.3.0
rootdir: /var/lib/jenkins/workspace/flaskpipeline
collected 3 items
test_app.py ... [100%]
============================= 3 passed in 0.14s ==============================
This output confirms that the tests have passed and the pipeline is executing as expected.
Quick Tip
Integrating continuous testing within your CI/CD pipeline ensures that issues are identified early, making maintenance and scaling more efficient.
Conclusion
By following this approach, you can seamlessly integrate a Flask application with Jenkins to perform automated unit tests using Pytest. This pipeline configuration supports continuous integration and assists in maintaining code quality throughout the development lifecycle.
For more details on continuous integration with Jenkins, visit the Jenkins Documentation.
Further reading and additional materials can be found in links below:
Happy coding and testing!
Watch Video
Watch video content
Practice Lab
Practice lab