Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.11
python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.3
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:
db:
image: 'zcube/bitnami-compat-mongodb'
image: 'bitnami/mongodb:latest'
ports:
- 27017:27017
restart: on-failure
Expand Down
1,232 changes: 908 additions & 324 deletions poetry.lock

Large diffs are not rendered by default.

30 changes: 10 additions & 20 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,20 @@
name = "tdd project"
version = "0.0.1"
description = ""
authors = ["Nayanna Nara <nayanna501@gmail.com>"]
authors = ["juvenalculino <b831381@gmail.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.104.1"
uvicorn = "^0.24.0.post1"
pydantic = "^2.5.1"
pydantic-settings = "^2.1.0"
motor = "^3.3.1"
pytest = "^7.4.3"
pytest-asyncio = "^0.21.1"
pre-commit = "^3.5.0"
httpx = "^0.25.1"
python = "^3.12.3"
fastapi = "^0.111.0"
uvicorn = "^0.30.0"
pydantic = "^2.7.2"
pydantic-settings = "^2.2.1"
motor = "^3.4.0"
pytest = "^8.2.1"
pytest-asyncio = "^0.23.7"
pre-commit = "^3.7.1"

[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = [
"--strict-config",
"--strict-markers",
"--ignore=docs_src",
]
xfail_strict = true
junit_family = "xunit2"

[build-system]
requires = ["poetry-core"]
Expand Down
189 changes: 189 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# TDD Project

## O que é TDD?
TDD é uma sigla para `Test Driven Development`, ou Desenvolvimento Orientado a Testes. A ideia do TDD é que você trabalhe em ciclos.

### Ciclo do TDD
![C4](/docs/img/img-tdd.png)

### Vantagens do TDD
- entregar software de qualidade;
- testar procurando possíveis falhas;
- criar testes de integração, testes isolados (unitários);
- evitar escrever códigos complexos ou que não sigam os pré-requisitos necessários;

A proposta do TDD é que você codifique antes mesmo do código existir, isso nos garante mais qualidade no nosso projeto. Além de que, provavelmente se você deixar pra fazer os testes no final, pode acabar não fazendo. Com isso, sua aplicação perde qualidade e está muito mais propensa a erros.

# Store API
## Resumo do projeto
Este documento traz informações do desenvolvimento de uma API em FastAPI a partir do TDD.

## Objetivo
Essa aplicação tem como objetivo principal trazer conhecimentos sobre o TDD, na prática, desenvolvendo uma API com o Framework Python, FastAPI. Utilizando o banco de dados MongoDB, para validações o Pydantic, para os testes Pytest e entre outras bibliotecas.

## O que é?
Uma aplicação que:
- tem fins educativos;
- permite o aprendizado prático sobre TDD com FastAPI + Pytest;

## O que não é?
Uma aplicação que:
- se comunica com apps externas;


## Solução Proposta
Desenvolvimento de uma aplicação simples a partir do TDD, que permite entender como criar tests com o `pytest`. Construindo testes de Schemas, Usecases e Controllers (teste de integração).

### Arquitetura
|![C4](/docs/img/store.drawio.png)|
|:--:|
| Diagrama de C4 da Store API |

### Banco de dados - MongoDB
|![C4](/docs/img/product.drawio.png)|
|:--:|
| Database - Store API |


## StoreAPI
### Diagramas de sequência para o módulo de Produtos
#### Diagrama de criação de produto

```mermaid
sequenceDiagram
title Create Product
Client->>+API: Request product creation
Note right of Client: POST /products

API->>API: Validate body

alt Invalid body
API->Client: Error Response
Note right of Client: Status Code: 422 - Unprocessable Entity
end

API->>+Database: Request product creation
alt Error on insertion
API->Client: Error Response
note right of Client: Status Code: 500 - Internal Server Error
end
Database->>-API: Successfully created

API->>-Client: Successful Response
Note right of Client: Status Code: 201 - Created

```
#### Diagrama de listagem de produtos

```mermaid
sequenceDiagram
title List Products
Client->>+API: Request products list
Note right of Client: GET /products

API->>+Database: Request products list

Database->>-API: Successfully queried

API->>-Client: Successful Response
Note right of Client: Status Code: 200 - Ok
```

#### Diagrama de detalhamento de um produto

```mermaid
sequenceDiagram
title Get Product
Client->>+API: Request product
Note right of Client: GET /products/{id}<br/> Path Params:<br/> - id: <id>

API->>+Database: Request product
alt Error on query
API->Client: Error Response
Note right of Client: Status Code: 500 - Internal Server Error
else Product not found
API->Client: Error Response
Note right of Client: Status Code: 404 - Not Found
end

Database->>-API: Successfully queried

API->>-Client: Successful Response
Note right of Client: Status Code: 200 - Ok
```
#### Diagrama de atualização de produto

```mermaid
sequenceDiagram
title PUT Product
Client->>+API: Request product update
Note right of Client: PUT /products/{id}<br/> Path Params:<br/> - id: <id>

API->>API: Validate body

alt Invalid body
API->Client: Error Response
Note right of Client: Status Code: 422 - Unprocessable Entity
end

API->>+Database: Request product
alt Product not found
API->Client: Error Response
Note right of Client: Status Code: 404 - Not Found
end

Database->>-API: Successfully updated

API->>-Client: Successful Response
Note right of Client: Status Code: 200 - Ok
```

#### Diagrama de exclusão de produto

```mermaid
sequenceDiagram
title Delete Product
Client->>+API: Request product delete
Note right of Client: DELETE /products/{id}<br/> Path Params:<br/> - id: <id>

API->>+Database: Request product
alt Product not found
API->Client: Error Response
Note right of Client: Status Code: 404 - Not Found
end

Database->>-API: Successfully deleted

API->>-Client: Successful Response
Note right of Client: Status Code: 204 - No content
```

## Desafio Final
- Create
- Mapear uma exceção, caso dê algum erro de inserção e capturar na controller
- Update
- Modifique o método de patch para retornar uma exceção de Not Found, quando o dado não for encontrado
- a exceção deve ser tratada na controller, pra ser retornada uma mensagem amigável pro usuário
- ao alterar um dado, a data de updated_at deve corresponder ao time atual, permitir modificar updated_at também
- Filtros
- cadastre produtos com preços diferentes
- aplique um filtro de preço, assim: (price > 5000 and price < 8000)

## Preparar ambiente

Vamos utilizar Pyenv + Poetry, link de como preparar o ambiente abaixo:

[poetry-documentation](https://github.com/nayannanara/poetry-documentation/blob/master/poetry-documentation.md)

## Links uteis de documentação
[mermaid](https://mermaid.js.org/)

[pydantic](https://docs.pydantic.dev/dev/)

[validatores-pydantic](https://docs.pydantic.dev/latest/concepts/validators/)

[model-serializer](https://docs.pydantic.dev/dev/api/functional_serializers/#pydantic.functional_serializers.model_serializer)

[mongo-motor](https://motor.readthedocs.io/en/stable/)

[pytest](https://docs.pytest.org/en/7.4.x/)
19 changes: 17 additions & 2 deletions store/controllers/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from fastapi import APIRouter, Body, Depends, HTTPException, Path, status
from pydantic import UUID4
from store.core.exceptions import NotFoundException
from datetime import datetime

from store.schemas.product import ProductIn, ProductOut, ProductUpdate, ProductUpdateOut
from store.usecases.product import ProductUsecase
Expand All @@ -13,7 +14,12 @@
async def post(
body: ProductIn = Body(...), usecase: ProductUsecase = Depends()
) -> ProductOut:
return await usecase.create(body=body)
try:
return await usecase.create(body=body)
except BaseException as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=exc.message
)


@router.get(path="/{id}", status_code=status.HTTP_200_OK)
Expand All @@ -30,14 +36,23 @@ async def get(
async def query(usecase: ProductUsecase = Depends()) -> List[ProductOut]:
return await usecase.query()

@router.get(path="/price_range", status_code=status.HTTP_200_OK)
async def get_by_price_range(min_price: float, max_price: float ,usecase: ProductUsecase = Depends()) -> List[ProductOut]:
return await usecase.get_product_by_price_range(min_price, max_price)

@router.patch(path="/{id}", status_code=status.HTTP_200_OK)
async def patch(
id: UUID4 = Path(alias="id"),
body: ProductUpdate = Body(...),
usecase: ProductUsecase = Depends(),
) -> ProductUpdateOut:
return await usecase.update(id=id, body=body)
try:
body.updated_at = datetime.now()
return await usecase.update(id=id, body=body)
except NotFoundException:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Product not found. Try again!"
)


@router.delete(path="/{id}", status_code=status.HTTP_204_NO_CONTENT)
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ async def clear_collections(mongo_client):
@pytest.fixture
async def client() -> AsyncClient:
from store.main import app

async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac




@pytest.fixture
def products_url() -> str:
return "/products/"
Expand Down