Skip to content

Commit cf391cf

Browse files
authored
test(type-checking): enforce type checking on tests, user setup.cfg (#12)
1 parent aba71d7 commit cf391cf

13 files changed

+75
-20
lines changed

.coveragerc

-2
This file was deleted.

.github/workflows/run-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
echo "PYTHONPATH=src/" >> $GITHUB_ENV
2626
- name: Test with pytest
2727
run: |
28-
pytest --cov=src/app --cov=src/tests --cov-report xml:coverage.xml src
28+
pytest
2929
3030
- name: Upload coverage
3131
if: ${{ github.ref == 'refs/heads/main' }}

.pre-commit-config.yaml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# NOTE: The actions below will be executed in the order of definition
2+
repos:
3+
# Remove unused imports
4+
- repo: https://github.com/PyCQA/autoflake
5+
rev: v1.4
6+
hooks:
7+
- id: autoflake
8+
args: [ "--in-place", "--remove-all-unused-imports" ]
9+
# Format imports
10+
- repo: https://github.com/pycqa/isort
11+
rev: 5.10.1
12+
hooks:
13+
- id: isort
14+
# Format code
15+
- repo: https://github.com/google/yapf
16+
rev: v0.32.0
17+
hooks:
18+
- id: yapf
19+
args: ["-pri", "src"]

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ Openapi specifications available at: http://localhost:9001/docs/
9090
#### Registry
9191
Docker registry available at: https://hub.docker.com/r/zhavir/python-assessment
9292

93+
#### 4. Pre-commit hooks
94+
95+
I used pre-commit hooks to format the code with `yapf` and optimize the imports using `isort`.
96+
97+
To activate the hooks you have to run the following command:
98+
99+
```bash
100+
pre-commit install
101+
```
102+
93103
## Best practices followed
94104
In the first step, I used a multi-layer pattern approach just because the domain was
95105
very tiny and does not justify a domain driven approach ( for example by following the hexagonal architecture ).
@@ -99,6 +109,8 @@ I followed those principles while modelling the classes:
99109
* Open–closed principle
100110
* Single-responsibility principle
101111
112+
Due to the fact python is not a typed language, I've adopted MyPy in order to enforce the type checking on tests run
113+
102114
For the testing, I used to cover everything with the unit tests. Meanwhile, I've tested only some happy path with the
103115
integration tests. This because I've decided to follow the testing pyramid principle.
104116
Moreover, I tried to follow the 100% test coverage objective

requirements-dev.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ httpx
33
pytest
44
pytest-asyncio
55
pytest-cov
6-
pytest-mock
6+
pytest-mock
7+
pytest-mypy
8+
pre-commit

setup.cfg

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[tool:pytest]
2+
addopts =
3+
--mypy
4+
--cov=src/app
5+
--cov=src/tests
6+
--cov-report xml:coverage.xml
7+
src
8+
9+
[isort]
10+
include_trailing_comma=true
11+
use_parentheses=true
12+
line_length=120
13+
multi_line_output=3
14+
15+
[mypy]
16+
python_version = 3.10
17+
ignore_missing_imports = True
18+
plugins = pydantic.mypy
19+
20+
[coverage:run]
21+
omit = */main.py

src/app/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def custom_openapi():
3838
return app.openapi_schema
3939

4040

41-
app.openapi = custom_openapi
41+
app.openapi = custom_openapi # type: ignore
4242

4343
if __name__ == "__main__":
4444
parser = argparse.ArgumentParser()

src/app/routers/models/requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
class GeneratePasswordRequest(BaseModel):
7-
password_length: conint(gt=0, le=200) = settings.default_password_length
7+
password_length: conint(gt=0, le=200) = settings.default_password_length # type: ignore
88
has_numbers: bool = settings.default_has_numbers
99
has_lowercase_chars: bool = settings.default_has_lowercase_chars
1010
has_uppercase_chars: bool = settings.default_has_uppercase_chars

src/app/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
class Settings(BaseSettings):
7-
default_password_length: conint(gt=0, le=200) = 10
7+
default_password_length: conint(gt=0, le=200) = 10 # type: ignore
88
default_has_numbers: bool = True
99
default_has_lowercase_chars: bool = True
1010
default_has_uppercase_chars: bool = True

src/app/utils/get_string_chars.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ async def get_string_chars(
99
uppercase_letters: bool,
1010
special_chars: bool,
1111
) -> List[str]:
12-
allowed_chars = []
12+
allowed_chars: List[str] = []
1313

1414
if numbers:
1515
allowed_chars.extend(string.digits)

src/tests/integration/test_e2e_password_generate.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
'has_lowercase_chars',
1212
'has_uppercase_chars',
1313
'has_special_chars',
14-
'expected_match'
14+
'expected_match',
1515
],
1616
[
1717
(20, True, False, False, False, r'^\d+$'),
@@ -29,17 +29,20 @@ async def test_e2e_generate_password(
2929
has_lowercase_chars: bool,
3030
has_uppercase_chars: bool,
3131
has_special_chars: bool,
32-
expected_match: str
32+
expected_match: str,
3333
):
3434

3535
async with mocked_client as client:
36-
response = await client.post("/api/v1/passwords/generate/", json={
37-
"password_length": password_length,
38-
"has_numbers": has_numbers,
39-
"has_lowercase_chars": has_lowercase_chars,
40-
"has_uppercase_chars": has_uppercase_chars,
41-
"has_special_chars": has_special_chars,
42-
})
36+
response = await client.post(
37+
"/api/v1/passwords/generate/",
38+
json={
39+
"password_length": password_length,
40+
"has_numbers": has_numbers,
41+
"has_lowercase_chars": has_lowercase_chars,
42+
"has_uppercase_chars": has_uppercase_chars,
43+
"has_special_chars": has_special_chars,
44+
}
45+
)
4346

4447
assert response.status_code == 200
4548
body = response.json()

src/tests/unit/test_providers/test_randomizer_provider.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
(["test", "test2"], 1),
1515
(list(string.digits), 5),
1616
(list(string.digits), 15),
17-
]
17+
],
1818
)
1919
@pytest.mark.asyncio
2020
async def test_generate_random_sample(values: List[str], length: int):

src/tests/unit/test_routers/test_password_generate_router.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
async def test_generate_password(
99
mocked_client: AsyncClient,
1010
mocked_password_generator_service: AsyncMock,
11-
mocked_randomizer_provider,
11+
mocked_randomizer_provider,
1212
):
1313
mocked_password_generator_service.return_value.generate_password = AsyncMock(return_value="something")
1414

@@ -41,7 +41,7 @@ async def test_generate_password(
4141
async def test_generate_password_but_input_is_not_valid(
4242
mocked_client: AsyncClient,
4343
mocked_password_generator_service: AsyncMock,
44-
mocked_randomizer_provider,
44+
mocked_randomizer_provider,
4545
body: dict,
4646
):
4747
async with mocked_client as client:

0 commit comments

Comments
 (0)