Skip to content

Python, Flask, MongoDB, Docker ile RestAPI Geliştirme

Bu eğitim serisinde python, flask, mongodb ve docker ile bir rest api geliştireceğiz.

Part 1

Kurulum ve Proje Yapısı

Uygulamayı çalıştırma ve ilk view fonksiyonu ve endpoint(uç noktayı) ekleme Requirements(Gereksinimler)

  • python3.11
  • Docker version 20.10.21
  • Flask==2.2.2
  • flask-mongoengine==1.0.0
  • flask-marshmallow==0.14.0
  • apifairy==1.3.0

Flask_MongoDB_Docker adında bir proje klasörü oluşturalım.

Yeni bir python projesine başlarken sanal bir ortam oluşturmak iyi bir pratiktir.

Proje klasörüne aşağıdaki komutu yazarak sanal ortamı oluşturalım.

$cd Flask_MongoDB_Docker/
# Sanal ortamımı env311 olarak adlandırdım
$ python3.11 -m venv env311
# Sanal ortamı etkinleştirin (macos/linux kullanıcıları)
$ source env311/bin/activate
# Windows kullanıcısıysanız bu şekilde etkinleştirebilirsiniz.
# $ env311\Scripts\activate
- Flask, Flask-MongoEngine ve diğer bağımlılıkları kurun

$ pip install flask
$ pip install flask-mongoengine
$ pip install python-dotenv
$ pip install flask-marshmallow
$ pip install apifairy 
- Proje Dosyaları ve Klasör Yapısı

$ tree -I env311 Flask_MongoDB_Docker 

Flask_MongoDB_Docker
├── Dockerfile
├── README.md
├── api
   ├── __init__.py
   ├── app.py
   ├── models.py
   ├── schemas.py
   └── views.py
├── config.py
├── docker-compose.yml
├── main.py
└── requirements.txt

1 directory, 11 files
- config.py üzerinde kodlamaya başlayalım

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

BASE_DIR = os.path.abspath(os.path.dirname(__file__))

class Config:
    # security options
    SECRET_KEY = os.environ.get('SECRET_KEY', 'top-secret!')

    # mongo db options eklenecek
- Config sınıfını tanımladıktan sonra api/app.py üzerinde app instance oluşturmak için bir fonksiyon ekleyeceğiz.

# api/app.py
from flask import Flask

# internals
from config import Config

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    return app
- create_app fonksiyonunu çağırabilmek için api/init.py içine aktaralım(import)

# api/__init__.py
from api.app import create_app
- Son olarak main.py üzerinde create_app fonksiyonunu kullanarak bir app oluşturacağız.
# main.py
from api import create_app

app = create_app()
- Projeyi çalıştırmak için proje klasörü içinde .env dosyası oluşturun. FLASK_APP değişkenini aşağıdaki gibi tanımlayın ve ayrıca debugger(hata ayıklayıcıyı) etkinleştirmek için FLASK_DEBUG değişkenini tanımlayın.
# Flask_MongoDB_Docker/.env
FLASK_APP=main.py
FLASK_DEBUG=1
- Artık projemizi terminalde çalıştırabiliriz.

# Flask_MongoDB_Docker/
$ flask run
- Aşağıdaki gibi bir çıktı göreceksiniz
# output of "flask run" command

 * Serving Flask app 'main.py'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 854-344-911
- http://127.0.0.1:5000 adresine GET isteği yaparsanız HTTP 404 Hatası alırsınız. Çünkü herhangi bir endpoint (uç nokta, kaynak, yol) oluşturmadık.
<!doctype html>
<html lang=en>
  <title>404 Not Found</title>
  <h1>Not Found</h1>
  <p>The requested URL was not found on the server. If you entered the URL
    manually please check your spelling and try again.</p>
- api/views.py üzerine ilk endpoint ve view fonksiyonunu yazalım

# api/views.py
from flask import Blueprint

books_bp = Blueprint("books", __name__)


@books_bp.route("/books/", methods=["GET"])
def books():
    book_list = [
        {"id":1, "name":"Python Flask & MongoDB", "author":"Adnan Kaya", "published_year": 2023},
        {"id":2, "name":"Python for Absolute Beginners", "author":"Adnan Kaya", "published_year": 2025},
        {"id":3, "name":"Learn Django by Developing Projects", "author":"Adnan Kaya", "published_year": 2024},
    ]

    return book_list
- İlk önce Blueprint'i import ediyoruz - Ve parametre olarak "books" ve name vererek bir Blueprint nesnesi oluşturun. - Bu nesne books_bp değişkenine atandı. - Endpointleri tanımlarken books_bp nesnesini kullanacağız. - Yalnızca "/books/" uç noktasında GET isteğine izin veriyoruz - books adında bir fonksiyon oluşturuyoruz ve bu 3 adet dict nesnesi içeren bir liste yani book_list döndürüyor. - Ve şimdi create_app fonksiyonunda (api/app.py'de) books blueprint instance(örneği) tanımlayın.
# api/app.py
from flask import Flask

# internals
from config import Config

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    # blueprints
    API_URL_PREFIX = "/api/v1"
    from api.views import books_bp

    app.register_blueprint(books_bp, url_prefix=API_URL_PREFIX)

    return app
- http://localhost:5000/api/v1/books/ için bir GET isteği yaparsanız, aşağıdaki yanıtı alırsınız

[
  {
    "author": "Adnan Kaya",
    "id": 1,
    "name": "Python Flask & MongoDB",
    "published_year": 2023
  },
  {
    "author": "Adnan Kaya",
    "id": 2,
    "name": "Python for Absolute Beginners",
    "published_year": 2025
  },
  {
    "author": "Adnan Kaya",
    "id": 3,
    "name": "Learn Django by Developing Projects",
    "published_year": 2024
  }
]

Part 2

Mongoengine'i uygulamadan önce, mongodb'yi bir docker container(konteyner) olarak çalıştıralım. Terminalde aşağıdaki gibi çalıştırabilirsiniz.

docker run --name mongodb -d -p 27017:27017 -e  MONGO_INITDB_ROOT_USERNAME=developer -e MONGO_INITDB_ROOT_PASSWORD=developer mongo
- Bu komut, aşağıdaki gibi bir container id yazdırır:
ec1fc7b8439dfeb57cf06f93a0271935123cd1d1467be9e02bc4ece5b9046e77

  • Docker konteyneri kontrol edelim ve mongo kabuğunu/shell (mongosh) açalım:
    $ docker ps -a
    # output
    CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                      NAMES
    ec1fc7b8439d   mongo     "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes   0.0.0.0:27017->27017/tcp   mongodb
    
  • Mongo shell

    $ docker exec -it mongodb mongosh -u developer -p developer
    
    # eğer başarılı ise aşağıdaki gibi görmelisiniz
    test>
    # databaseleri görmek için `show dbs` komutunu yazabilirsiniz.
    test> show dbs
    admin   100.00 KiB
    config   60.00 KiB
    local    72.00 KiB
    

  • Şimdi config.py üzerinde flask uygulaması için mongodb konfigürasyonları ekleyelim

    # config.py
    import os
    from dotenv import load_dotenv
    
    # .env yi yükler
    load_dotenv()
    
    BASE_DIR = os.path.abspath(os.path.dirname(__file__))
    
    class Config:
        # security options
        SECRET_KEY = os.environ.get('SECRET_KEY', 'top-secret!')
    
        # mongo db options
        MONGODB_SETTINGS = [
            {
                "db": os.environ.get("MONGODB_DBNAME","mydb"),
                "host": os.environ.get("MONGODB_HOST","localhost"),
                "port": int(os.environ.get("MONGODB_PORT")) or 27017,
                "alias": "default",
                "username": os.environ.get("MONGODB_USERNAME","developer"),
                "password": os.environ.get("MONGODB_PASSWORD","developer")
            }
        ]
    

  • Ayrıca bu ortam değişkenlerini(environment variables) .env dosyasına ekleyebiliriz.

    FLASK_APP=main.py
    FLASK_DEBUG=1
    MONGODB_DBNAME=mydb
    MONGODB_HOST=localhost
    MONGODB_PORT=27017
    MONGODB_USERNAME=developer
    MONGODB_PASSWORD=developer
    

  • Şimdi api/app.py'de MongoEngine örneği oluşturuluyor(instantiating)

    # api/app.py
    from flask import Flask
    from flask_mongoengine import MongoEngine # yeni
    
    # internals
    from config import Config
    
    db = MongoEngine() # yeni
    
    def create_app(config_class=Config):
        app = Flask(__name__)
        app.config.from_object(config_class)
    
        # database mongodb
        db.init_app(app) # yeni
    
        # blueprints
        API_URL_PREFIX = "/api/v1"
        from api.views import books_bp
    
        app.register_blueprint(books_bp, url_prefix=API_URL_PREFIX)
    
        return app
    

  • api/init.py içine db'yi de import etmeliyiz.

    # api/__init__.py
    from api.app import create_app, db
    

  • Şimdi modellerimizi oluşturma zamanı. models.py'yi açın

    # internals
    from api import db
    
    class Book(db.Document):
        name = db.StringField(max_length=64)
        author = db.StringField(max_length=32)
        published_year = db.IntField(min=610)
    
        def __str__(self) -> str:
            return self.name
    

  • Modelimizi oluşturduktan sonra flask shell üzerinde oynayalım, terminali açabilirsiniz

$ flask shell
# Python 3.11.0 (v3.11.0:deaf509e8f, Oct 24 2022, 14:43:23) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
# App: api.app
# Instance: /<my-path>/Flask_MongoDB_Docker/instance
>>> from api.models import *
>>> book = Book(name="Python Flask", author="Adnan Kaya", published_year=2023)
>>> book.save()
<Book: Python Flask>
  • İşte bu! Veritabanında ilk kaydımızı oluşturduk. Mongo kabuğunda/shell kontrol edelim
test> show dbs
admin   100.00 KiB
config   92.00 KiB
local    72.00 KiB
mydb      8.00 KiB # our database is created

# Bizim veritabanı mydb olduğu icin onu seciyoruz
test> use mydb
switched to db mydb

# show collections(tablolari goster)
mydb> show collections
book

# book kayitlarini getir
mydb> db.book.find()
[
  {
    _id: ObjectId("638fb5df4653579e2ea4367c"),
    name: 'Python Flask',
    author: 'Adnan Kaya',
    published_year: 2023
  }
]
  • 2 kayıt daha ekledik. Şimdi api'miz aracılığıyla veritabanından veri almak için api/views.py dosyasını düzenleyelim.

    # api/views.py
    from flask import Blueprint
    # internals
    from .models import Book
    
    books_bp = Blueprint("books", __name__)
    
    
    @books_bp.route("/books/", methods=["GET"])
    def books():
        book_list = Book.objects.all()
    
        return book_list
    

  • Şimdi http://localhost:5000/api/v1/books/ adresine bir GET isteği yapın. Yanıt durumu 200, ancak herhangi bir yanıt verimiz yok ve ayrıca terminalde hatamız var:

    127.0.0.1 - - [07/Dec/2022 00:53:38] "GET /api/v1/books/ HTTP/1.1" 200 -
    Error on request:
    Traceback (most recent call last):
      File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 335, in run_wsgi
        execute(self.server.app)
      File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 325, in execute
        write(data)
      File "/<mypath>/Flask_MongoDB_Docker/env311/lib/python3.11/site-packages/werkzeug/serving.py", line 293, in write
        assert isinstance(data, bytes), "applications must write bytes"
    AssertionError: applications must write bytes
    

  • Hata mesajı özeti

    AssertionError: applications must write bytes
    

  • Yani dönülecek veri byte türünde olmalıdır. views.py'deki book_list türü şu şekildedir:

    <class 'flask_mongoengine.BaseQuerySet'>
    

  • book_list ise BaseQuerySet türündedir.

Bu sorunun üstesinden gelmek için flask.jsonify() kullanmalıyız. api/views.py dosyasını açın ve jsonify'ı içe aktarın ve jsonify'a ileterek book_list'i döndürün

from flask import Blueprint, jsonify # yeni
# internals
from .models import Book

books_bp = Blueprint("books", __name__)


@books_bp.route("/books/", methods=["GET"])
def books():
    book_list = Book.objects.all()

    return jsonify(book_list) # yeni

  • Şimdi http://localhost:5000/api/v1/books/ adresine bir GET isteği yapın ve aşağıdaki sonucu alacaksınız.

    [
      {
        "_id": {
          "$oid": "638fb5df4653579e2ea4367c"
        },
        "author": "Adnan Kaya",
        "name": "Python Flask",
        "published_year": 2023
      },
      {
        "_id": {
          "$oid": "638fb80a4653579e2ea4367d"
        },
        "author": "Adnan Kaya",
        "name": "Python Django",
        "published_year": 2025
      },
      {
        "_id": {
          "$oid": "638fb83b4653579e2ea4367e"
        },
        "author": "Adnan Kaya",
        "name": "Python Tkinter",
        "published_year": 2026
      }
    ]
    

  • _id ve $oid (object id) otomatik olarak oluşturulur.

Part 3

  • Projemizi dockerize edip yeni modeller ekleyeceğiz. Başlayalım!

docker-compose.yml dosyamızı yazmaya başlayacağız.

# Flask_MongoDB_Docker/docker-compose.yml

version: '3.7'
services:

  db:
    image: mongo:latest
    container_name: mymongodb
    ports:
      - 27017:27017
    volumes:
      - mongo_data:/data/db

volumes:
  mongo_data:

  • Yalnızca mongodb'nin en son imajını kullanan ve konteynerin adı mymongodb olan db servisini ekledik (bu container adına dikkat edin çünkü flask(web) servisini eklediğimizde bu container adını flask konteynerinden mongo konteynerine bağlanmak için kullanacağız).

Son olarak volumes mongo_data olarak adlandırdık. Aşağıdaki komutu yazarak mongodb container başlatalım:

# docker-compose.yml dosyasında db servisi bizim mongodb servisimizdir.
$ docker compose up -d --build db

# output
[+] Running 2/2
  Network flask_mongodb_docker_default  Created                                                                           0.0s
  Container mymongodb                   Started

  • Konteyner statusunu kontrol edelim

    $ docker ps
    CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS            PORTS                      NAMES
    dfa10167aa02   mongo:latest   "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes      0.0.0.0:27017->27017/tcp   mymongodb
    

  • .env dosyamıza bir göz atalım

    FLASK_APP=main.py
    FLASK_DEBUG=1
    MONGODB_DBNAME=mydb
    MONGODB_HOST=localhost
    MONGODB_PORT=27017
    MONGODB_USERNAME=developer
    MONGODB_PASSWORD=developer
    

  • Şu anda sadece mongodb olan 1 konteynerimiz var. Terminalimizden flaski çalıştıracağız. Mongodb'ye doğrudan flask'tan bağlandığımız için (herhangi bir konteynerden değil) , MONGODB_HOST'u localhost olarak kullanacağız. Flask'i docker compose ile başlattığımızda MONGODB_HOST değeri mongodb servisinin container adı olacaktır. Flask'i çalıştıralım:

    $ source env311/bin/activate
    (env311)$ flask run
    

  • http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapın

  • HTTP Status 200 ile boş liste [ ] yanıtı almalısınız. Ancak part2'de 3 kayıt eklemiştik. Verilerimiz nerede?
  • Part2'de container verilerini bağlamadık. Bu nedenle önceki konteyner kaldırıldığında 3 adet kayıt da kaybedildi.
  • docker-compose.yml dosyasında, container kaldırılsa bile verilerimizi kalıcı olarak saklamak için birimimizi/volume belirledik.

Yeniden veri eklemeden şimdi flask'ı dockerize edelim ve mongodb konteynerine bağlanalım. docker-compose-yml dosyasını açın ve servislere web ekleyin.

# Flask_MongoDB_Docker/docker-compose.yml
version: '3.7'
services:
  web:
    container_name: flask_mongodb_docker_tutorial
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - .:/usr/src/app/
    ports:
      - 5000:5000
    env_file:
      - ./.env
    depends_on:
      - db
    stdin_open: true
    tty: true

  db:
    image: mongo:latest
    container_name: mymongodb
    ports:
      - 27017:27017
    volumes:
      - mongo_data:/data/db

volumes:
  mongo_data:

  • Şimdi .env dosyasını aşağıdaki gibi düzenleyin
FLASK_APP=main.py
FLASK_DEBUG=1
MONGODB_DBNAME=mydb
# MONGODB_HOST=localhost
# docker compose kullandigimizda MONGODB_HOST=container_name olmali ki
# web ile db servisleri haberlesebilsin
MONGODB_HOST=mymongodb
MONGODB_PORT=27017
MONGODB_USERNAME=developer
MONGODB_PASSWORD=developer
  • Şimdi Dockerfile dosyamızı düzenleyeceğiz.

    # Flask_MongoDB_Docker/Dockerfile
    
    # python3.11 resmi imajı indir
    FROM python:3.11.0-slim-buster
    
    # work directory belirle
    WORKDIR /usr/src/app
    
    # environment variables / ortam değişkenleri belirle
    ENV PYTHONDONTWRITEBYTECODE 1
    ENV PYTHONUNBUFFERED 1
    
    # sistem bagimliliklarini kur
    RUN apt-get update && apt-get install -y netcat
    
    RUN pip install --upgrade pip
    # requirements.txt yi container WORKDIR dizinine kopyala
    COPY ./requirements.txt /usr/src/app/requirements.txt
    # proje paketlerini kur
    RUN pip install -r requirements.txt
    
    # projeyi WORKDIR dizinine kopyala
    COPY . /usr/src/app/
    
    # development modda çalıştır
    CMD ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=5000"]
    

  • Docker komutları için yorum satırları ekledim. Kolayca anlayabilirsiniz.

Şimdi terminalde çalışan flask uygulamasını durdurun. Ve flask(web) servisi oluşturun.

$ docker compose up -d --build web

# output
[+] Building 2.6s (12/12) FINISHED                                                                                                    
 # ...
 # .....
[+] Running 2/2
  Container mymongodb                      Running                                                                              0.0s
  Container flask_mongodb_docker_tutorial  Started  

  • Container status kontrol edelim

    $ docker ps
    CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                      NAMES
    ccd7f1c0de54   flask_mongodb_docker-web   "python -m flask run…"   4 minutes ago    Up 3 minutes    0.0.0.0:5000->5000/tcp     flask_mongodb_docker_tutorial
    dfa10167aa02   mongo:latest               "docker-entrypoint.s…"   37 minutes ago   Up 37 minutes   0.0.0.0:27017->27017/tcp   mymongodb
    

  • Artık bir GET isteğini http://127.0.0.1:5000/api/v1/books/ adresine yapabilir ve yanıt olarak boş listeyi ve durumunu 200 olarak alabilirsiniz.

Yeni kayıtlar eklemeden önce, models.py'yi düzenleyelim ve modeller arasında ilişki kurmak için yeni model ekleyelim.

# Flask_MongoDB_Docker/api/models.py
# internals
from api import db


class Author(db.Document):
    firstname = db.StringField(max_length=64)
    lastname = db.StringField(max_length=64)

    def __str__(self):
        return f"{self.firstname} {self.lastname}"


class Book(db.Document):
    name = db.StringField(max_length=64)
    author = db.ReferenceField(document_type=Author)
    published_year = db.IntField(min=610)

    def __str__(self) -> str:
        return self.name

  • Her kitabın bir yazarı vardır. Basit ilişki (Birden Çoğa / One-To-Many)

Flask kabuğunda author ve book kayıtları oluşturalım. Öncelikle flask konteynerine girmemiz gerekiyor.

$ docker compose exec web sh
# ls
Dockerfile  README.md  __pycache__  api  config.py  docker-compose.yml env311 main.py  requirements.txt
# pwd
/usr/src/app

  • Şimdi flask shell

    # flask shell
    Python 3.11.0 (main, Dec  6 2022, 08:43:25) [GCC 8.3.0] on linux
    App: api.app
    Instance: /usr/src/app/instance
    >>>
    

  • Kayıtlar ekleyelim

    >>> from api.models import Author, Book
    >>> a1 = Author.objects.create(firstname="Adnan", lastname="Kaya")
    >>> a2 = Author.objects.create(firstname="Recep", lastname="Alkurt")
    >>> a1
    <Author: Adnan Kaya>
    >>> a2
    <Author: Recep Alkurt>
    >>> b1 = Book.objects.create(author=a1, name="Flask 101", published_year=2023)
    >>> b2 = Book.objects.create(author=a2, name="Angular 101", published_year=2023)
    >>> b3 = Book.objects.create(author=a1, name="Django 101", published_year=2025)
    >>> b4 = Book.objects.create(author=a1, name="Scrapy 101", published_year=2024)
    >>> b1
    <Book: Flask 101>
    >>> b1.author
    <Author: Adnan Kaya>
    >>> b2
    <Book: Angular 101>
    >>> b2.author
    <Author: Recep Alkurt>
    

  • HARİKA! Veritabanına 2 yazar ve 4 kitap ekledik.

views.py dosyasına bir göz atalım (Şimdiye kadar hiçbir şeyi değiştirmedik)

from flask import Blueprint, jsonify
# internals
from .models import Book

books_bp = Blueprint("books", __name__)


@books_bp.route("/books/", methods=["GET"])
def books():
    book_list = Book.objects.all()

    return jsonify(book_list)

  • http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapın, yanıt şu olacaktır:

    [
      {
        "_id": {
          "$oid": "63b9d62bd1627fb9544b8833"
        },
        "author": {
          "$oid": "63b9d5ded1627fb9544b8831"
        },
        "name": "Flask 101",
        "published_year": 2023
      },
      {
        "_id": {
          "$oid": "63b9d63dd1627fb9544b8834"
        },
        "author": {
          "$oid": "63b9d601d1627fb9544b8832"
        },
        "name": "Angular 101",
        "published_year": 2023
      },
      {
        "_id": {
          "$oid": "63b9d64dd1627fb9544b8835"
        },
        "author": {
          "$oid": "63b9d5ded1627fb9544b8831"
        },
        "name": "Django 101",
        "published_year": 2025
      },
      {
        "_id": {
          "$oid": "63b9d65cd1627fb9544b8836"
        },
        "author": {
          "$oid": "63b9d5ded1627fb9544b8831"
        },
        "name": "Scrapy 101",
        "published_year": 2024
      }
    ]
    

  • Sonraki partta serialization ve CRUD operasyonları yapacağız.

Part 4

Bu partta flask_marshmallow(bir nesne serileştirme kütüphanesi) extension/uzantısını uygulayacağız.

Ayrıca CRUD işlemleri de yapacağız.

Proje dosya yapısını tekrar hatırlayalım

Flask_MongoDB_Docker $ tree . -I env311
.
├── Dockerfile
├── README.md
├── __pycache__
├── api
   ├── __init__.py
   ├── __pycache__
   ├── app.py
   ├── models.py
   ├── schemas.py
   └── views.py
├── config.py
├── docker-compose.yml
├── main.py
└── requirements.txt

3 directories, 11 files
  • flask_marshmallow'u uygulayalım. api/app.py dosyasını açın ve flask_marshmallow'u import edin

    from flask import Flask
    from flask_mongoengine import MongoEngine
    from flask_marshmallow import Marshmallow # yeni
    
    # internals
    from config import Config
    
    
    db = MongoEngine()
    marsh = Marshmallow() # yeni
    
    def create_app(config_class=Config):
        # existing code ...
    
        # serialization/deserialization Marshmallow
        marsh.init_app(app) # yeni
    
        # existing code ...
    
        return app
    

  • Ve api/init.py dosyasını aşağıdaki gibi düzenleyin

    # api/__init__.py
    from api.app import create_app, db , marsh
    

  • Şimdi api/schemas.py dosyasını açın ve AuthorSchema ve BookSchema sınıflarını oluşturun ve serialize/deserialize etmek için alanları ekleyin (bu alanları model sınıflarında da tanımladığımızı unutmayın). Bu yüzden tüm model alanlarını serialize etmek istiyoruz ve şema sınıflarında da tanımladık. Yalnızca istediğiniz alanları serialize etmek istiyorsanız, yalnızca istediğinizi tanımlayabilirsiniz.

    # api/schemas.py
    from marshmallow import validate
    #  internals
    from api import marsh as ma
    
    
    class AuthorSchema(ma.Schema):
    
        id = ma.String(unique=True, dump_only=True)
        firstname = ma.String(required=True)
        lastname = ma.String(required=True)
    
        class Meta:
            ordered = True
    
    
    class BookSchema(ma.Schema):
    
        id = ma.String(unique=True, dump_only=True)
        name = ma.String(required=True)
        author = ma.Nested(AuthorSchema, dump_only=True)
        published_year = ma.Integer(strict=True, required=True,
                                    validate=[validate.Range(
                                        min=610, error="Year must be min 610")]
                                    )
    
        class Meta:
            ordered = True
    

  • AuthorSchema sınıfı, tümü string olan id, firstname ve lastname olmak üzere üç alan tanımlar. id alanı benzersiz olarak işaretlenir ve yalnızca serileştirilmiş çıktıya dahil edilir (dump_only=True).

  • BookSchema sınıfı, id, name, author ve published_year olmak üzere dört alanı tanımlar. id alanı benzersiz olarak işaretlenir ve yalnızca serileştirilmiş çıktıya dahil edilir.
  • Ad alanı zorunludur. author alanı nested(iç içedir), yani AuthorSchema sınıfının bir örneğidir(instance) ve ayrıca yalnızca serileştirilmiş çıktıya dahil edilir.
  • published_year alanı bir tamsayıdır, zorunludur ve yalnızca tamsayıları kabul eder ve tamsayı olmayan bir değer iletilirse hata verir. Ayrıca, yılın en az 610 olması gereken bir aralık doğrulamasıyla doğrulanır.
  • Her iki sınıfın da ordered(sıralı) olarak True, ayarlanan bir Meta sınıfı vardır. Bu, alanların bildirildiği sıraya göre sıralı serileştirme çıktısı anlamına gelir.
  • Şimdi bu şemaları api/views.py içinde kullanmanın zamanı geldi.

    from flask import Blueprint, jsonify
    from apifairy import response # yeni
    # internals
    from .models import Book
    from .schemas import AuthorSchema, BookSchema # yeni
    
    books_bp = Blueprint("books", __name__)
    
    books_schema = BookSchema(many=True) # yeni
    
    @books_bp.route("/books/", methods=["GET"])
    @response(books_schema) # yeni
    def books():
        book_list = Book.objects.all()
    
        return book_list
    

  • Özel bir dekoratör olan apifairy'den bir response decorator import ediyoruz.

  • Ayrıca daha önce tanımladığımız AuthorSchema ve BookSchema'yı import ediyoruz.
  • Ve sonra, marshmallow'a bir nesne listesini serileştireceğini söyleyen many parametresi True olarak ayarlanan BookSchema sınıfının bir örneğini oluşturuyoruz.
  • Yanıt dekoratörü, books_schema'yı bağımsız değişken olarak alan özel bir dekoratördür ve döndürülen verileri yanıt olarak göndermeden önce otomatik olarak serialize eder.
  • http://127.0.0.1:5000/api/v1/books/ adresine bir GET isteği yapalım ve yanıtı görelim. Yanıtı böyle görmelisiniz:

    [
      {
        "author": {
          "firstname": "Adnan",
          "id": "63b9d5ded1627fb9544b8831",
          "lastname": "Kaya"
        },
        "id": "63b9d62bd1627fb9544b8833",
        "name": "Flask 101",
        "published_year": 2023
      },
      {
        "author": {
          "firstname": "Recep",
          "id": "63b9d601d1627fb9544b8832",
          "lastname": "Alkurt"
        },
        "id": "63b9d63dd1627fb9544b8834",
        "name": "Angular 101",
        "published_year": 2023
      },
      {
        "author": {
          "firstname": "Adnan",
          "id": "63b9d5ded1627fb9544b8831",
          "lastname": "Kaya"
        },
        "id": "63b9d64dd1627fb9544b8835",
        "name": "Django 101",
        "published_year": 2025
      },
      {
        "author": {
          "firstname": "Adnan",
          "id": "63b9d5ded1627fb9544b8831",
          "lastname": "Kaya"
        },
        "id": "63b9d65cd1627fb9544b8836",
        "name": "Scrapy 101",
        "published_year": 2024
      }
    ]
    

  • Dikkat ettiyseniz $oid yok ve ilişkili nesne alanları serialize edilerek döndürülür.

Yazar Oluşturmak Şimdi POST isteği yapmak için /authors/ endpoint ve onun view fonksiyonunu create_author olarak ekleyelim. api/views.py'yi açın:

from flask import Blueprint, jsonify
from apifairy import response, body # yeni
# internals
from .models import Book, Author # yeni
from .schemas import AuthorSchema, BookSchema

books_bp = Blueprint("books", __name__)

books_schema = BookSchema(many=True)
author_schema = AuthorSchema() # yeni


@books_bp.route("/authors/", methods=["POST"])
@body(author_schema)
@response(author_schema, status_code=201)
def create_author(payload):
    author = Author(**payload)
    author.save()
    return author

  • create_author fonksiyonu, üç dekoratörle dekore edilmiştir.

Birincisi @books_bp.route("/authors/", method=["POST"]) olup, /authors/ endpointini bu fonksiyona eşler ve bu endpointe yalnızca POST isteklerine izin verir. İkinci dekoratör, istek gövdesini author_schema'ya göre doğrulayan @body(author_schema) dekoratörüdür ve request body verisini ayrıştırıp fonksiyona bir payload argümanı olarak iletir. Üçüncü dekoratör @response(author_schema, status_code=201) dekoratöre author_schema kullanarak döndürülen verileri serialize edilmesi ve kaydın başarıyla oluşturulduğunu gösteren yanıtın durum kodunu 201 olarak ayarlamasını söyler. Şimdi request body ile http://127.0.0.1:5000/api/v1/authors/ için bir POST isteği yapalım. Request body:

{
  "firstname":"Adnan2",
  "lastname":"Kaya2"
}

  • Aşağıdaki gibi yanıt alacağız
    {
      "firstname": "Adnan2",
      "id": "63c31e4110bf2407b4c2eb89",
      "lastname": "Kaya2"
    }
    

Authors Listesini Çekmek Yazar listesini almak için bir endpoint ve onun view fonksiyonunu oluşturalım. api/views.py açalım:

# öncekiler ...
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True) # yeni

@books_bp.route("/authors/", methods=["GET"])
@response(authors_schema)
def authors():
    author_list = Author.objects.all()

    return author_list

  • http://127.0.0.1:5000/api/v1/authors/ adresine bir GET isteği yapın ve yanıt şu şekilde olacaktır:
    [
      {
        "firstname": "Adnan",
        "id": "63b9d5ded1627fb9544b8831",
        "lastname": "Kaya"
      },
      {
        "firstname": "Recep",
        "id": "63b9d601d1627fb9544b8832",
        "lastname": "Alkurt"
      },
      {
        "firstname": "Adnan2",
        "id": "63c31e4110bf2407b4c2eb89",
        "lastname": "Kaya2"
      },
      {
        "firstname": "Adnan3",
        "id": "63c31e9c33f15afb371e3144",
        "lastname": "Kaya3"
      },
      {
        "firstname": "Adnan4",
        "id": "63c31f8dafd3ee10e7cc48da",
        "lastname": "Kaya4"
      }
    ]
    

Book Kaydı Oluşturmak Şimdi yeni kitap eklemek için POST isteği yapmak üzere /books/ endpointi oluşturalım.

Yeni fonksiyon oluşturmadan önce BookSchema'yı düzenlememiz gerekiyor. api/schemas.py açalım:

class BookSchema(ma.Schema):

    # öncekiler ...
    author_id = ma.String(load_only=True, required=True) # yeni
    # öncekiler ...

  • Bir string olan yeni bir author_id alanı tanımlar ve load_only=True ve required=True olarak işaretlenir, bu, bu alanın yalnızca seri hale getirilmiş girdide bulunacağı ve gerekli olduğu anlamına gelir. Bu, sunucuya bir JSON payload gönderildiğinde, bu alanın bulunmasının beklendiği ve kitapla ilişkili yazarı tanımlamak için kullanılacağı anlamına gelir. Bu, senaryomuzda kullanışlıdır, çünkü giriş verileri üzerinde doğrulamayı zorunlu kılarak, müşterinin yeni bir kitap oluşturmadan önce geçerli bir author_id göndermesini sağlar. Ayrıca, bu alanın serileştirilmiş çıktıda bulunmadığını ve bu nedenle müşteriye verilen yanıta dahil edilmeyeceğini belirtmekte fayda var. api/views.py dosyasını açın

    from flask import Blueprint, jsonify, abort # yeni
    # öncekiler ...
    
    books_schema = BookSchema(many=True)
    book_schema = BookSchema() # yeni
    # öncekiler ...
    
    
    @books_bp.route("/books/", methods=["POST"])
    @body(book_schema)
    @response(book_schema, status_code=201)
    def create_book(payload):
        author_id = payload.pop("author_id")
        try:
            author = Author.objects.get(id=author_id)
        except Author.DoesNotExist as exc:
            return abort(code=404,
                         description="Author does not exist, check your author_id again")
        except:
            return abort(code=400)
        else:
            book = Book(**payload, author=author)
            book.save()
            return book
    

  • create_book fonksiyonu, request body'den author_id ayıklayarak başlar ve ardından id numarasına göre ilgili Author nesnesini veritabanından almaya çalışır. Author mevcut değilse, 400 durum kodu ve "Yazar mevcut değil, yazar_kimliğinizi tekrar kontrol edin" şeklinde bir "açıklama" içeren bir yanıt döndürür. Author mevcutsa, fonksiyon kalan request body/payload verileriyle yeni bir Book nesnesi oluşturur ve author attribute alınan Author nesnesine atanır, ardından bunu veritabanına kaydeder ve yeni oluşturulan book nesnesini döndürür. Şimdi http://127.0.0.1:5000/api/v1/authors/ için bir GET isteği yapın ve author id'lerden birini alın. Adnan2'nin id'sini aldım.

Şimdi request body ile http://127.0.0.1:5000/api/v1/books/ adresine bir POST isteği yapın. Request body:

{
  "name":"Rest API development with Flask, MongoDB, Docker",
  "published_year":2023,
  "author_id": "63c31e4110bf2407b4c2eb89"
}

  • Kayıt başarılı bir şekilde oluşturulmalıdır.

ID Göre Author Silmek api/views.py dosyasını açın ve aşağıdakileri ekleyin

@books_bp.route("/authors/<string:id>", methods=["DELETE"])
def delete_author(id):
    try:
        author = Author.objects.get(id=id)
        author.delete()
        return jsonify(message=f"Deleted {author}!")
    except Author.DoesNotExist as ex:
        return abort(404, description=ex)
    except:
        return abort(400)

ID Göre Author Güncelle (PUT , PATCH isteği) api/views.py'yi açın ve aşağıdakileri ekleyin

# öncekiler ..
author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True)
update_author_schema = AuthorSchema(partial=True) # yeni
# öncekiler ...

@books_bp.route("/authors/<string:id>", methods=["PUT", "PATCH"])
@body(update_author_schema)
@response(author_schema, status_code=200)
def update_author(payload, id):
    try:
        author = Author.objects.get(id=id)
        author.update(**payload)
        author.save()
        author.reload()
        return author
    except Author.DoesNotExist as ex:
        return abort(404, description=ex)
    except:
        return abort(400)

  • update_author fonksiyonu, karşılık gelen Author nesnesini id ile veritabanından almaya çalışarak başlar. Author yoksa, 404 durum kodu ve exception mesajının "açıklaması" ile bir yanıt döndürür. Author varsa, payload verilerini kullanarak author nesnesinin alanlarını günceller ve bunu veritabanına kaydeder ve author kaydını veritabanından yeniden yükler ve güncellenen author nesnesini döndürür. Başka bir exception meydana gelirse, 400 durum koduyla bir cevap döndürür. İstek gövdesi(request body) ile http://127.0.0.1:5000/api/v1/authors/63c31e4110bf2407b4c2eb89 adresine PUT isteği yapalım.

    {
        "firstname": "Adnan22",
        "lastname": "Kaya22"
    }
    

  • Cevap aşağıdaki gibidir

    {
      "firstname": "Adnan22",
      "id": "63c31e4110bf2407b4c2eb89",
      "lastname": "Kaya22"
    }
    

  • İstek gövdesi ile PATCH isteğinde bulunun

    {
        "lastname": "Kaya22 | only updated last name"
    }
    

  • Cevap
    {
      "firstname": "Adnan22",
      "id": "63c31e4110bf2407b4c2eb89",
      "lastname": "Kaya22 | only updated last name"
    }
    

Part 5

Unit testler bu yazımızda projeye eklenecektir.

Docker dışında testler yapacaksanız, pytest'i manuel olarak kurabilirsiniz.

$ pip install pytest
  • Testleri çalıştırırken docker kullanacağız bu yüzden requirements.txt dosyasına pytest ekleyeceğim.

    #... önceki paketler
    pytest==7.2.1
    

  • Şimdi api/ klasörü altında tests klasörü oluşturmaya başlıyoruz ve api/tests/ klasörü altında 3 adet test dosyası ve 1 adet base dosyası oluşturuyoruz. Klasör yapısı aşağıdaki gibi olacak

    # Flask_MongoDB_Docker $ tree api
    api
    ├── __init__.py
    ├── app.py
    ├── models.py
    ├── schemas.py
    ├── tests
       ├── __init__.py
       ├── base.py
       ├── test_models.py
       ├── test_schemas.py
       └── test_views.py
    └── views.py
    
    3 directories, 10 files
    

  • Temel sınıflara ortak ve hazırlık çalışmaları koymak iyi bir uygulamadır. api/tests/base.py dosyasını dolduralım

    from config import Config
    
    
    class TestConfig(Config):
        SERVER_NAME = '127.0.0.1:5000'
        TESTING = True
        MONGODB_SETTINGS = [
            {
                "db": "test_db",
                "host": "mymongodb", # docker db host ismi
                "port": 27017,
                "alias": "default",
                "username": "developer",
                "password": "developer",
                "connect": False,
            }
        ]
    

  • Bu kod, Config adlı başka bir sınıftan miras alan TestConfig adlı bir sınıfı tanımlar. TestConfig sınıfı, üst sınıfın çeşitli niteliklerini override eder. SERVER_NAME özniteliği(attribute), yerel ana bilgisayarın IP adresi ve bağlantı noktası numarası olan '127.0.0.1:5000' olarak ayarlanmıştır. TESTING özniteliği, kodun bir test ortamında çalıştığını gösteren True olarak ayarlanır. MONGODB_SETTINGS özniteliği, bir MongoDB veritabanına bağlanmak için bir ayarlar sözlüğü(dict) içeren bir dizidir. Bu TestConfig sınıfı, test ortamı için belirli yapılandırmalar ayarlayacaktır. api/tests/base.py'de temel test sınıfını yazalım

    import unittest
    #  internals
    from api import db, create_app
    from config import Config
    
    class TestConfig(Config):
        SERVER_NAME = '127.0.0.1:5000'
        TESTING = True
        MONGODB_SETTINGS = [
            {
                "db": "test_db",
                "host": "mymongodb", # docker db host adi
                "port": 27017,
                "alias": "default",
                "username": "developer",
                "password": "developer",
                "connect": False,
            }
        ]
    
    class BaseTestCase(unittest.TestCase):
        '''
        Run: docker compose exec web sh -d  "pytest -s --disable-warnings"
        '''
        config = TestConfig
    
        def setUp(self):
            db.disconnect("default")
            #  create flask app
            self.app = create_app(self.config)
            self.app_context = self.app.app_context()
            self.app_context.push()
            #  drop previous existing data
            db.connection["default"].drop_database("test_db")
            self.client = self.app.test_client()
    
        def tearDown(self):
            db.connection["default"].drop_database("test_db")
            db.disconnect()
            self.app_context.pop()
    

  • Bu sınıf, unittest.TestCase'den miras alınan BaseTestCase olarak adlandırılır. Bu, bir birim test paketindeki diğer test durumları için üst sınıf olarak kullanılması amaçlanan bir temel test durumu sınıfıdır. Sınıfın, önceki kod parçacığında tanımlanan TestConfig sınıfına ayarlanmış bir yapılandırma özniteliği vardır. setUp() fonksiyonu, test senaryosundaki her test yönteminden önce çalıştırılır ve test ortamını ayarlamak için kullanılır. Fonksiyon, db.disconnect() fonksiyonunu kullanarak MongoDB veritabanıyla bağlantıyı keserek başlar. Ardından, create_app() işlevini kullanarak, config özniteliğini bir argüman olarak ileten yeni bir Flask uygulaması oluşturur. Fonksiyon daha sonra uygulama bağlamını zorlar, böylece test durumu uygulamanın kaynaklarına erişebilir ve mevcut veritabanını kaldırır. tearDown() fonksiyonu, test senaryosundaki her test yönteminden sonra çalıştırılır ve testten sonra ortamı temizlemek için kullanılır. Mevcut test veritabanını kaldırarak, veritabanı bağlantısını keserek ve uygulama içeriğini açarak başlar. Bu kodun MongoDB veritabanını kullanan flask uygulamamızda entegrasyon testi için temel sınıf olarak kullanılması amaçlanmıştır, setUp fonksiyonu test ortamını kuracak, uygulamayı oluşturacak ve test veritabanına bağlanacak ve tearDown fonksiyonu test bittikten sonra temizlemek için çalışır. Modelleri Test Etme Şimdi api/tests/ klasörü altında test_models.py dosyasını oluşturalım ve 2 sınıfı dolduralım.

    from api.models import Author, Book
    from api.tests.base import BaseTestCase
    
    
    class TestAuthorModel(BaseTestCase):
        '''
        docker compose exec web sh -c "pytest -v api/tests/test_models.py -s --disable-warnings"
        '''
    
        def setUp(self):
            super().setUp()
            self.author = Author(firstname='John', lastname='Doe')
    
        def test_author_str_representation(self):
            self.assertEqual(str(self.author), 'John Doe')
    
        def test_author_save(self):
            self.author.save()
            result = Author.objects(firstname='John', lastname='Doe')
            self.assertEqual(len(result), 1)
            self.assertEqual(result[0].firstname, 'John')
            self.assertEqual(result[0].lastname, 'Doe')
    
    
    class TestBookModel(BaseTestCase):
    
        def setUp(self):
            super().setUp()
            self.author = Author(firstname='John', lastname='Doe').save()
            self.book = Book(name='Test Book', author=self.author,
                             published_year=2000)
    
        def test_book_str_representation(self):
            self.assertEqual(str(self.book), 'Test Book')
    
        def test_book_save(self):
            self.book.save()
            result = Book.objects(name='Test Book')
            self.assertEqual(len(result), 1)
            self.assertEqual(result[0].name, 'Test Book')
            self.assertEqual(str(result[0].author), 'John Doe')
            self.assertEqual(result[0].published_year, 2000)
    

Bu sınıfların her ikisi de, sırasıyla Author ve Book modellerinin işlevselliğini test etmeyi amaçlayan test senaryolarıdır.

TestAuthorModel sınıfının 3 fonksiyonu vardır:

super() kullanarak üst sınıfın setUp() yöntemini çağıran ve firstname='John' ve lastname='Doe' ile Author modelinin yeni bir örneğini oluşturan setUp() fonksiyonu vardır. Author modelinin string olarak temsilinin 'firstname lastname' biçiminde olduğunu test eden test_author_str_representation() fonksiyonu vardır. Author modelinin save() fonksiyonunun modeli veritabanına doğru bir şekilde kaydettiğini ve Author modelinin objects() fonksiyonunun kaydedilen modeli veritabanından alabildiğini test eden test_author_save() fonksiyonu. TestBookModel sınıfının 3 fonksiyonu vardır:

super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur ve bunu veritabanına kaydeder. daha sonra, name='Test Book', author=self.author ve published_year=2000 ile Book modelinin yeni bir örneğini oluşturur. Bir Book modelinin string temsilinin/representation kitap adı olduğunu test eden test_book_str_representation() fonksiyonu. Book modelinin save() fonksiyonunun modeli veritabanına doğru bir şekilde kaydettiğini ve Book modelinin objects() fonksiyonunun kaydedilen modeli veritabanından alabildiğini test eden test_book_save() fonksiyonu. Ayrıca, kitabın yazarının doğru bir şekilde kaydedilip kaydedilmediğini ve yazarın adının doğru bir şekilde alınıp alınmadığını kontrol eder. Test Şemaları (Test Schema Serializers) Şimdi api/tests/ klasörü altında test_schemas.py dosyası oluşturalım ve 2 sınıfı dolduralım.

from marshmallow import ValidationError
from api.models import Author, Book
from api.schemas import AuthorSchema, BookSchema
from api.tests.base import BaseTestCase

class TestAuthorSchema(BaseTestCase):
    '''
    docker compose exec web sh -c "pytest -v api/tests/test_schemas.py -s --disable-warnings"
    '''
    def setUp(self):
        super().setUp()
        self.author = Author(firstname='John', lastname='Doe')
        self.schema = AuthorSchema()

    def test_author_schema_dump(self):
        result = self.schema.dump(self.author)
        result.pop("id")
        self.assertEqual(result, {"lastname": "Doe", "firstname": "John"})

    def test_author_schema_load(self):
        data = {"lastname": "Doe", "firstname": "John"}
        result = self.schema.load(data)
        self.assertEqual(result, {"lastname": "Doe", "firstname": "John"})

    def test_author_schema_load_missing_field(self):
        data = {"lastname": "Doe"}
        with self.assertRaises(ValidationError):
            self.schema.load(data)


class TestBookSchema(BaseTestCase):
    def setUp(self):
        super().setUp()
        self.author = Author(firstname='John', lastname='Doe').save()
        self.book = Book(name='Test Book', author=self.author, 
                    published_year=2000)
        self.schema = BookSchema()

    def test_book_schema_dump(self):
        result = self.schema.dump(self.book)
        result.pop("id")
        author = result.pop("author")
        author.pop("id")
        self.assertEqual(result, {"name": "Test Book", "published_year":2000})
        self.assertEqual(author, {"lastname": "Doe", "firstname": "John"})

    def test_book_schema_load(self):
        data = {"name": "Test Book", "author_id": str(self.author.id), 
                "published_year": 2000}
        result = self.schema.load(data)
        self.assertEqual(result, {"name": "Test Book", 
                                  "author_id": str(self.author.id), 
                                  "published_year": 2000})

    def test_book_schema_load_missing_field(self):
        data = {"name": "Test Book", "published_year": 2000}
        with self.assertRaises(ValidationError):
            self.schema.load(data)

    def test_book_schema_load_invalid_year(self):
        data = {"name": "Test Book", "author_id": str(self.author.id), 
                "published_year": 600}
        with self.assertRaises(ValidationError):
            self.schema.load(data)

TestAuthorSchema sınıfının 3 fonksiyonu vardır:

super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran ve firstname='John' ve lastname='Doe' ile Author modelinin yeni bir örneğinin oluşturulması ve AuthorSchema'nın yeni bir örneğinin oluşturulması. AuthorSchema'nın dump() fonksiyonunun Author örneğini bir sözlüğe doğru şekilde seri hale getirdiğini test eden test_author_schema_dump() fonksiyonu. AuthorSchema'nın load() fonksiyonunun bir sözlüğü doğru bir şekilde bir Author örneğine serialize ettiğini test eden test_author_schema_load() fonksiyonu ve load() fonksiyonunun sözlükte gerekli bir değer eksik olduğunda bir ValidationError raise ettiğini test eden test_author_schema_load_missing_field() fonksiyonu alan. TestBookSchema sınıfının 5 fonksiyonu vardır:

super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur ve bunu veritabanına kaydeder, name='Test Book', author=self.author ve published_year=2000 ile ve BookSchema'nın yeni bir örneğini oluşturur. BookSchema'nın dump() fonksiyonunun Book örneğini doğru bir şekilde bir sözlüğe serialize ettiğini test eden test_book_schema_dump() fonksiyonu. BookSchema'nın load() fonksiyonunun bir sözlüğü bir Book örneğine doğru bir şekilde deserialize ettiğini test eden test_book_schema_load() fonksiyonu. Sözlükte gerekli bir alan eksik olduğunda load() yönteminin bir ValidationError oluşturup oluşturmadığını test eden test_book_schema_load_missing_field() fonksiyonu. load() fonksiyonunun yayınlanan yıl geçersiz olduğunda bir ValidationError oluşturup oluşturmadığını test eden test_book_schema_load_invalid_year() fonksiyonu. View Fonksiyonları Test Etme Şimdi api/tests/ klasörü altında test_views.py dosyası oluşturalım ve bir sınıf ve daha fazla test fonksiyonu dolduralım.

from flask import url_for
from api.models import Author, Book
from api.tests.base import BaseTestCase

class ApiViewsTestCase(BaseTestCase):
    '''
    docker compose exec web sh -c "pytest -v api/tests/test_views.py -s --disable-warnings"
    '''
    def setUp(self):
        super().setUp()
        self.author = Author(firstname='John', lastname='Doe').save()
        self.book = Book(name='Test Book', author=self.author, published_year=2000).save()


    def test_create_author(self):
        data = {"firstname": "Jane", "lastname": "Doe"}
        response = self.client.post(url_for("books.create_author"), json=data)
        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.get_json()["firstname"], data["firstname"])
        self.assertEqual(response.get_json()["lastname"], data["lastname"])

    def test_create_book(self):
        data = {"name": "Test Book 2", "author_id": str(self.author.id), "published_year": 2000}
        response = self.client.post(url_for("books.create_book"), json=data)
        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.get_json()["author"]["id"], data["author_id"])
        self.assertEqual(response.get_json()["name"], data["name"])
        self.assertEqual(response.get_json()["published_year"], data["published_year"])

    def test_create_book_invalid_author(self):
        data = {"name": "Test Book 2", "author_id": "invalid_id", "published_year": 2000}
        response = self.client.post(url_for("books.create_book"), json=data)
        self.assertEqual(response.status_code, 400)

    def test_books(self):
        response = self.client.get(url_for("books.books"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.get_json()), 1)
        self.assertEqual(response.get_json()[0]["name"], self.book.name)
        self.assertEqual(response.get_json()[0]["published_year"], self.book.published_year)
        self.assertEqual(response.get_json()[0]["author"]["firstname"], self.book.author.firstname)
        self.assertEqual(response.get_json()[0]["author"]["lastname"], self.book.author.lastname)

    def test_authors(self):
        response = self.client.get(url_for("books.authors"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.get_json()), 1)
        self.assertEqual(response.get_json()[0]["firstname"], self.author.firstname)
        self.assertEqual(response.get_json()[0]["lastname"], self.author.lastname)

    def test_update_author(self):
        data = {"firstname": "Jane"}
        response = self.client.put(url_for("books.update_author", id=self.author.id), json=data)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.get_json()["firstname"], data["firstname"])

    def test_update_author_invalid_id(self):
        data = {"firstname": "Jane"}
        # 'invalid_id' is not a valid ObjectId, it must be a 12-byte input or a 24-character hex string
        response = self.client.put(url_for("books.update_author", id="invalid_id"), json=data)
        self.assertEqual(response.status_code, 400)

    def test_delete_author(self):
        response = self.client.delete(url_for("books.delete_author", id=self.author.id))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.get_json(), {"message": "Deleted John Doe!"})

    def test_delete_author_invalid_id(self):
        # 'invalid_id' is not a valid ObjectId, it must be a 12-byte input or a 24-character hex string
        response = self.client.delete('/api/v1/authors/invalid_id')
        self.assertEqual(response.status_code, 400)

ApiViewsTestCase sınıfının 10 fonksiyonu vardır:

super() kullanarak üst sınıfın setUp() fonksiyonunu çağıran setUp() fonksiyonu, Author modelinin yeni bir örneğini oluşturur, onu veritabanına kaydeder, Book modelinin yeni bir örneğini oluşturur, onu veritabanına kaydeder. create_author endpointe yapılan bir POST isteğinin doğru bir şekilde yeni bir Author modeli oluşturduğunu ve yanıtın doğru verileri içerdiğini test eden test_create_author() create_book uç noktasına yapılan bir POST isteğinin doğru bir şekilde yeni bir Book modeli oluşturduğunu ve yanıtın doğru verileri içerdiğini test eden test_create_book() Geçersiz bir author_id ile create_book uç noktasına yapılan bir POST isteğinin başarısız olduğunu test eden ve 400 durum kodu döndüren test_create_book_invalid_author() /books/ uç noktasına yapılan bir GET isteğinin veritabanındaki tüm books listesini doğru bir şekilde döndürdüğünü ve yanıtın doğru verileri içerdiğini test eden test_books() Author uç noktasına yapılan bir GET isteğinin veritabanındaki tüm authors listesini doğru bir şekilde döndürdüğünü ve yanıtın doğru verileri içerdiğini test eden test_authors() update_author uç noktasına yapılan bir PUT isteğinin belirtilen Author modelini doğru bir şekilde güncelleyip güncellemediğini ve yanıtın doğru verileri içerdiğini test eden test_update_author() Geçersiz bir id'ye sahip update_author uç noktasına yapılan bir PUT isteğinin başarısız olduğunu test eden ve bir 400 durum kodu döndüren test_update_author_invalid_id() delete_author uç noktasına yapılan bir DELETE isteğinin belirtilen Author modelini doğru bir şekilde sildiğini ve yanıtın doğru verileri içerdiğini test eden test_delete_author() Geçersiz bir id sahip delete_author uç noktasına yapılan bir DELETE isteğinin başarısız olduğunu test eden ve 400 durum kodu döndüren test_delete_author_invalid_id() Tüm Testleri Çalıştırma Aşağıdaki komutu yazarak tüm testleri çalıştırabiliriz.

docker compose exec web sh -c "pytest -s --disable-warnings"

pytest komutu, test paketini çalıştırır ve varsayılan olarak geçerli dizinde ve alt dizinlerinde test_.py veya *_test.py kalıbıyla eşleşen tüm dosyaları keşfeder. -s seçeneği, Pytest'e testleri çalıştırmasını ve çıktıyı konsola yazdırmasını söyler. Bu seçenek olmadan, Pytest çıktıyı yazdırmayacak ve yalnızca test çalışmasının sonucunu gösterecektir. --disable-warnings seçeneği, Pytest'e testleri çalıştırmasını ve tüm uyarıları devre dışı bırakmasını söyler. **Test Komutunun Çıktısı*

$ docker compose exec web sh -c "pytest -s --disable-warnings"
========================================================= test session starts =========================================================
platform linux -- Python 3.11.0, pytest-7.2.1, pluggy-1.0.0
rootdir: /usr/src/app
collected 20 items                                                                                                                    

api/tests/test_models.py ....
api/tests/test_schemas.py .......
api/tests/test_views.py .........

================================================== 20 passed, 112 warnings in 50.99s ==================================================

Kodlar için github repo