DEV Community

Cover image for Developing, Testing, and Integrating CI/CD for a Flask Store App with MongoDB and GitHub Actions
V
V

Posted on • Edited on

Developing, Testing, and Integrating CI/CD for a Flask Store App with MongoDB and GitHub Actions

My first application using flask, Mongodb and Github Actions


Introduction

What is the purpose of MongoDB?

MongoDB is a popular NoSQL database that stores data in a flexible, JSON-like format called BSON (Binary JSON). Unlike traditional relational databases, MongoDB is designed to handle large amounts of unstructured or semi-structured data. It allows developers to:

  • Scale Horizontally: MongoDB can handle vast amounts of data and traffic by distributing data across many servers, making it ideal for large applications.
  • Flexible Schema: It does not require a fixed schema, meaning that each document in a collection can have a different structure. This flexibility is useful for handling dynamic data.
  • Speed and Performance: MongoDB is optimized for fast read and write operations, which is especially beneficial for real-time applications.

In the simple store application, MongoDB is well-suited to store product information, where the data structure might evolve over time.

What is the purpose of Flask

Flask is a lightweight web framework for Python that allows developers to build web applications. It is classified as a micro-framework because it provides only the essential tools, allowing developers to add additional functionality as needed.

When building the simple store application, Flask provides the foundation for creating routes, handling requests, and rendering views, while MongoDB stores the back-end data.

The Role of CI/CD Pipelines

Modern software development thrives on speed and stability. Continuous Integration and Continuous Deployment (CI/CD) pipelines streamline the development process by automating testing, integration, and deployment. With GitHub Actions, developers can:

  1. Ensure Code Quality: Automated tests catch bugs early, reducing manual intervention.
  2. Accelerate Deployment: Push changes to production with confidence, knowing that all tests have passed.

In this project, we’ll use a GitHub Actions CI/CD pipeline to ensure that our Flask application and its integration with the Gemini API works as expected. This pipeline will automate testing the API’s functionality.


In the sections to follow, we’ll dive deeper into building the Flask application, integrating it with the MongoDB, and setting up the CI/CD pipeline to test and deploy the project.


The Project Setup

Setting up the MongoDB Atlas

  1. Login to MongoDB Atlas or Create an account
  2. Create a new Cluster & select M2 for the initial free tier
  3. Once the database has been created click on connect to get the connection link.

Setting and activating the virtual environment

  1. Creating the virtual environment > Make sure virtualenv is installed
$ pip install virtualenv
$ python -m venv <environment_name>
$ source <environment_name>/bin/activate
Enter fullscreen mode Exit fullscreen mode

Note: You can deactivate the virtualenv by typing deactivate

Setting up the project

  1. Install the MongoDB Python Driver

    • On your local machine, make sure you have Python installed.
    • Install the pymongo library by running:
     pip install pymongo
    
  2. Test the Connection

    • In your Python script, use the following code to test the connection:
     from pymongo import MongoClient
    
     # Replace with your connection string
     client = MongoClient("<Connection String>")
     db = client.test  # Access the test database
     collection = db.test_collection  # Access the test collection
    
     # Insert a test document
     collection.insert_one({"name": "Test Document", "value": 1})
    
     print("Connected to MongoDB Atlas and inserted a document!")
    
  • Replace <Connection String> with the connection string you copied earlier.
  • Run the script to ensure that the connection to MongoDB Atlas is working.
  1. The Routes Create the necessary routes

# Routing to / to render home.html
@app.route("/")
def home():
    return render_template('home.html')

# Routing to /products to render Products and display the products from the mongodb shop_db database
@app.route("/products")
def products():
    products = products_collection.find()
    return render_template("products.html", products = products)
Enter fullscreen mode Exit fullscreen mode
  1. The Templates Create the templates that has to be rendered.
    • The Base Template:
   <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Store</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
    <link rel="stylesheet" href="{{url_for('static', filename='css/styles.css')}}"
</head>
<body>

    <nav data-bs-theme="dark" class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
          <a class="navbar-brand" href="#">Store</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
              <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="{{url_for('home')}}">Home</a>
              </li>
              <li class="nav-item">
                <a class="nav-link active" aria-current="page" href="{{url_for('products')}}">Products</a>
              </li>
            </ul>
          </div>
        </div>
      </nav>

    <div class="container">
      {% block pageContent %} {% endblock %}
    </div>

    <footer>
         Store
    </footer>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
  • The Home Template:
{% extends "base.html" %}
{% block pageContent %}
    <div class="homepage_body">
        <div class="card card-homepage">
            <img src="static/images/sales.jpg" class="card-img-top" alt="...">
            <div class="card-body">
              <h5 class="card-title">STORE</h5>
              <p class="card-text">View our cheapest products.</p>
              <a href="{{url_for('products')}}" class="btn btn-primary button-link">View Our Products</a>
            </div>
          </div>
    </div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode
  • The Products Template:
{% extends "base.html" %}
{% block pageContent %}
    <h1 class="title"> Our Products </h1>
    <br/>

    <div class="table_format">
        <table class="table table-dark table-hover">
            <thead>
                <th>Product Image</th>
                <th>Product Name</th>
                <th>Product Tag</th>
                <th>Product Price</th>
            </thead>
            <tbody>
                {% for product in products %}
                    <tr>
                        <td><img src="static/{{product.image_path}}" alt="Picture of {{product.name}}" /></td>
                        <td>{{product.name}}</td>
                        <td>{{product.tag}}</td>
                        <td>$ {{product.price}}</td>
                    </tr>
                {% endfor %}
            </tbody>
          </table>
    </div>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Running the Application

Note: Make sure the virtualenv is activate

  1. Running the app.py in development mode:
python app.py
Enter fullscreen mode Exit fullscreen mode
  1. Open localhost: http://127.0.0.1:5000

Testing the Application

  1. Create a test directory
    • Route testing:
import unittest
from app import app

class FlaskRouteTest(unittest.TestCase):
    # Set up the Flask test client
    def setUp(self):
        self.app = app.test_client()
        self.app.testing = True

    # Test for invalid method (POST instead of GET)
    def test_home_route_invalid_method(self):
        # Send a POST request to the home route
        response = self.app.post('/')

        # Expect 405 Method Not Allowed when trying to send post request for '/'
        self.assertEqual(response.status_code, 405)  

    # Test if the /products route works with GET
    def test_products_route_get(self):
        # Send a GET request to the /products route
        response = self.app.get('/products')

        # Expect 200 OK
        self.assertEqual(response.status_code, 200)  

        # Check if the page contains the word "Our Products" - The title
        self.assertIn(b"Our Products", response.data)  

if __name__ == "__main__":
    unittest.main()
Enter fullscreen mode Exit fullscreen mode
  • Testing the Database: Pinging the database:
import unittest
from pymongo import MongoClient
from app import MONGO_USERNAME, MONGO_PASSWORD

# Test 2: Database Read Operation - 
# Write a unit test to check the correct connection of a MongoDB read operation.
class MongoDBConnectionTest(unittest.TestCase):

    def test_mongo_connection(self):
        # Constructing the MongoDB URI, getting the MONGO_USERNAME AND PASSWORD FROM THE APP
        URI = "<MONGODB_URI>"

        # Creating MongoClient, timeout in 5 seconds for testing
        client = MongoClient(URI, serverSelectionTimeoutMS=5000)

        try:
            # Try to ping the database to verify the connection
            client.admin.command('ping')

            # If no exception is raised, connection is successful
            self.assertTrue(True)

        except Exception as e:
            # If Error is thrown Fail with Error
            self.fail(f"MongoDB connection failed: {e}")

        finally:

            # Ensure MongoClient is closed to avoid resource warnings
            client.close()

if __name__ == "__main__":
    unittest.main()
Enter fullscreen mode Exit fullscreen mode
  • Writing to the database:
import unittest
from pymongo import MongoClient
from app import MONGO_USERNAME, MONGO_PASSWORD

# Test 3: Database Write Operation
# Unit test for a MongoDB write operation
class MongoDBWriteOperationTest(unittest.TestCase):

    def setUp(self):
        # Constructing the MongoDB URI, getting the MONGO_USERNAME AND PASSWORD FROM THE APP
        URI = "<MONGODB_URI>"

        # Creating MongoClient, timeout in 5 seconds for testing
        self.client = MongoClient(URI, serverSelectionTimeoutMS=5000)

        # Setting DB and Collection
        self.db = self.client.shop_db
        self.collection = self.db.products

    def test_insert_document(self):

        # Creating a product to insert
        product = {
            "name": "House",
            "tag": "Test",
            "price": 999.99,
            "image_path": "images/house.jpg"
        }

        # Insert the product into the database
        result = self.collection.insert_one(product)

        # Verify the insertion
        # Query the database to check if the document is present
        inserted_product = self.collection.find_one({"_id": result.inserted_id})

        # Assertions to verify the document
        self.assertIsNotNone(inserted_product, "The document was not inserted.");
        self.assertEqual(inserted_product["name"], product["name"]);
        self.assertEqual(inserted_product["tag"], product["tag"]);
        self.assertEqual(inserted_product["price"], product["price"]);
        self.assertEqual(inserted_product["image_path"], product["image_path"]);

        # Cleanup: Remove the inserted document after the test
        self.collection.delete_one({"_id": result.inserted_id})

    # Clean up
    def tearDown(self):
        # Close the client connection
        self.client.close()

if __name__ == "__main__":
    unittest.main()
Enter fullscreen mode Exit fullscreen mode
  • Running the test: The following command will run all the test under the test directory
python -m unittest discover test
Enter fullscreen mode Exit fullscreen mode

Integrating with Github Actions

  1. Create the following directory to include Github Action's configuration:
.github/workflows/main.yml
Enter fullscreen mode Exit fullscreen mode
  1. Configuring the yml file:

on:
  push:
    branches:
      - main  # Trigger on push to the main branch
  pull_request:
    branches:
      - main  # Trigger on pull requests targeting the main branch

jobs:
  test:
    runs-on: ubuntu-latest  # Use the latest Ubuntu runner

    steps:
      # Step 1: Checkout the repository code
      - name: Checkout code
        uses: actions/checkout@v3

      # Step 2: Set up Python 3.8
      - name: Set up Python 3.8
        uses: actions/setup-python@v4
        with:
          python-version: '3.8'

      # Step 3: Install dependencies from requirements.txt inside the 'flask_app' directory
      - name: Install dependencies
        run: |
          cd flask_app
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      # Step 4: Set up MongoDB credentials as environment variables
      - name: Set up MongoDB environment variables
        run: |
          echo "MONGO_USERNAME=${{ secrets.MONGO_USERNAME }}" >> $GITHUB_ENV
          echo "MONGO_PASSWORD=${{ secrets.MONGO_PASSWORD }}" >> $GITHUB_ENV

      # Step 5: Run the tests located in 'flask_app/tests'
      - name: Run Tests in 'flask_app/tests'
        run: |
          cd flask_app
          python -m unittest discover tests
Enter fullscreen mode Exit fullscreen mode
  1. Testing the workflow:
  2. Once a change has been pushed, GitHub Action will trigger the automated testing:

Image description

Image description


Conclusion

Integrating GitHub Actions for Continuous Integration and Continuous Deployment (CI/CD) with MongoDB Atlas and Flask takes your development workflow to the next level by automating testing, building, and deployment processes. This setup allows you to easily deploy your Flask application to any environment while ensuring that your code is tested and validated every time a change is made.

By setting up MongoDB Atlas, you’ve taken care of the backend database, providing a scalable and secure cloud-based solution. With GitHub Actions, you can now automate your deployments, ensuring your code is always up-to-date and reliable across all stages of development, from testing to production.

This CI/CD pipeline reduces manual intervention, eliminates errors in deployments, and boosts efficiency by integrating the entire workflow from code commit to live production. Whether you're building a simple store application or a more complex platform, combining Flask, MongoDB Atlas, and GitHub Actions offers a streamlined, modern solution for both development and deployment.

References

  1. How to Set Up a Virtual Environment in Python – And Why It's Useful by Stephen Sanwo: Setting up Virtualenv

  2. MongoDB Atlas Documentation: MongoDB Atlas Documentation

  3. Flask Documentation: Flask Documentation

  4. MongoDB Atlas – Free Tier Overview: MongoDB Atlas Free Tier

  5. PyMongo Documentation: PyMongo Documentation

  6. GitHub Actions CI/CD Guide: GitHub Actions CI/CD Guide

Top comments (0)