Skip to content

Commit

Permalink
test(type-checking): enforce type checking on tests, user setup.cfg (#12
Browse files Browse the repository at this point in the history
)
  • Loading branch information
zhavir authored Aug 27, 2022
1 parent aba71d7 commit cf391cf
Show file tree
Hide file tree
Showing 13 changed files with 75 additions and 20 deletions.
2 changes: 0 additions & 2 deletions .coveragerc

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
echo "PYTHONPATH=src/" >> $GITHUB_ENV
- name: Test with pytest
run: |
pytest --cov=src/app --cov=src/tests --cov-report xml:coverage.xml src
pytest
- name: Upload coverage
if: ${{ github.ref == 'refs/heads/main' }}
Expand Down
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# NOTE: The actions below will be executed in the order of definition
repos:
# Remove unused imports
- repo: https://github.com/PyCQA/autoflake
rev: v1.4
hooks:
- id: autoflake
args: [ "--in-place", "--remove-all-unused-imports" ]
# Format imports
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
# Format code
- repo: https://github.com/google/yapf
rev: v0.32.0
hooks:
- id: yapf
args: ["-pri", "src"]
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ Openapi specifications available at: http://localhost:9001/docs/
#### Registry
Docker registry available at: https://hub.docker.com/r/zhavir/python-assessment

#### 4. Pre-commit hooks

I used pre-commit hooks to format the code with `yapf` and optimize the imports using `isort`.

To activate the hooks you have to run the following command:

```bash
pre-commit install
```

## Best practices followed
In the first step, I used a multi-layer pattern approach just because the domain was
very tiny and does not justify a domain driven approach ( for example by following the hexagonal architecture ).
Expand All @@ -99,6 +109,8 @@ I followed those principles while modelling the classes:
* Open–closed principle
* Single-responsibility principle
Due to the fact python is not a typed language, I've adopted MyPy in order to enforce the type checking on tests run

For the testing, I used to cover everything with the unit tests. Meanwhile, I've tested only some happy path with the
integration tests. This because I've decided to follow the testing pyramid principle.
Moreover, I tried to follow the 100% test coverage objective
Expand Down
4 changes: 3 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ httpx
pytest
pytest-asyncio
pytest-cov
pytest-mock
pytest-mock
pytest-mypy
pre-commit
21 changes: 21 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[tool:pytest]
addopts =
--mypy
--cov=src/app
--cov=src/tests
--cov-report xml:coverage.xml
src

[isort]
include_trailing_comma=true
use_parentheses=true
line_length=120
multi_line_output=3

[mypy]
python_version = 3.10
ignore_missing_imports = True
plugins = pydantic.mypy

[coverage:run]
omit = */main.py
2 changes: 1 addition & 1 deletion src/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def custom_openapi():
return app.openapi_schema


app.openapi = custom_openapi
app.openapi = custom_openapi # type: ignore

if __name__ == "__main__":
parser = argparse.ArgumentParser()
Expand Down
2 changes: 1 addition & 1 deletion src/app/routers/models/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


class GeneratePasswordRequest(BaseModel):
password_length: conint(gt=0, le=200) = settings.default_password_length
password_length: conint(gt=0, le=200) = settings.default_password_length # type: ignore
has_numbers: bool = settings.default_has_numbers
has_lowercase_chars: bool = settings.default_has_lowercase_chars
has_uppercase_chars: bool = settings.default_has_uppercase_chars
Expand Down
2 changes: 1 addition & 1 deletion src/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


class Settings(BaseSettings):
default_password_length: conint(gt=0, le=200) = 10
default_password_length: conint(gt=0, le=200) = 10 # type: ignore
default_has_numbers: bool = True
default_has_lowercase_chars: bool = True
default_has_uppercase_chars: bool = True
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/get_string_chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async def get_string_chars(
uppercase_letters: bool,
special_chars: bool,
) -> List[str]:
allowed_chars = []
allowed_chars: List[str] = []

if numbers:
allowed_chars.extend(string.digits)
Expand Down
21 changes: 12 additions & 9 deletions src/tests/integration/test_e2e_password_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'has_lowercase_chars',
'has_uppercase_chars',
'has_special_chars',
'expected_match'
'expected_match',
],
[
(20, True, False, False, False, r'^\d+$'),
Expand All @@ -29,17 +29,20 @@ async def test_e2e_generate_password(
has_lowercase_chars: bool,
has_uppercase_chars: bool,
has_special_chars: bool,
expected_match: str
expected_match: str,
):

async with mocked_client as client:
response = await client.post("/api/v1/passwords/generate/", json={
"password_length": password_length,
"has_numbers": has_numbers,
"has_lowercase_chars": has_lowercase_chars,
"has_uppercase_chars": has_uppercase_chars,
"has_special_chars": has_special_chars,
})
response = await client.post(
"/api/v1/passwords/generate/",
json={
"password_length": password_length,
"has_numbers": has_numbers,
"has_lowercase_chars": has_lowercase_chars,
"has_uppercase_chars": has_uppercase_chars,
"has_special_chars": has_special_chars,
}
)

assert response.status_code == 200
body = response.json()
Expand Down
2 changes: 1 addition & 1 deletion src/tests/unit/test_providers/test_randomizer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
(["test", "test2"], 1),
(list(string.digits), 5),
(list(string.digits), 15),
]
],
)
@pytest.mark.asyncio
async def test_generate_random_sample(values: List[str], length: int):
Expand Down
4 changes: 2 additions & 2 deletions src/tests/unit/test_routers/test_password_generate_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
async def test_generate_password(
mocked_client: AsyncClient,
mocked_password_generator_service: AsyncMock,
mocked_randomizer_provider,
mocked_randomizer_provider,
):
mocked_password_generator_service.return_value.generate_password = AsyncMock(return_value="something")

Expand Down Expand Up @@ -41,7 +41,7 @@ async def test_generate_password(
async def test_generate_password_but_input_is_not_valid(
mocked_client: AsyncClient,
mocked_password_generator_service: AsyncMock,
mocked_randomizer_provider,
mocked_randomizer_provider,
body: dict,
):
async with mocked_client as client:
Expand Down

0 comments on commit cf391cf

Please sign in to comment.