Skip to content

Commit d9e404f

Browse files
authored
Improve test coverage (#107)
* OPT: restructured test directory in preparation of extra unit tests, added tests for encryption and decryption logic * OPT: improved test coverage of elekto/core module * NEW: added some tooling to bootstrap testing * OPT: added unit test instructions to the README
1 parent 7283ff3 commit d9e404f

File tree

14 files changed

+223
-21
lines changed

14 files changed

+223
-21
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.vscode
2+
.idea
23
.venv/
4+
venv/
35

46
# Cache files and directories
57
*.pyc

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
PYTHON:=venv/bin/python
2+
PIP:=venv/bin/pip
3+
PYTEST:=venv/bin/py.test
4+
COV:=venv/bin/coverage
5+
6+
.PHONY: clean venv run test cov
7+
clean:
8+
rm -rf venv
9+
10+
venv: clean
11+
python3.11 -m venv venv
12+
$(PIP) install -r requirements.txt
13+
14+
run:
15+
$(PYTHON) console --run
16+
17+
test:
18+
$(PYTEST) test
19+
20+
cov:
21+
$(COV) run -m pytest test || true
22+
$(COV) html
23+
open htmlcov/index.html

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ python console --run
102102
python console --port 8080 --host 0.0.0.0 --run
103103
```
104104

105+
### Running tests
106+
Commands are available to run the test suite. This suite currently only consists of unit tests. A prerequisite for
107+
running the test suite is that you have a virtualenv setup. This can be done by running `make venv`. To run the tests,
108+
do `make test`.
109+
110+
Coverage is available. A report can be generated with `make cov`.
111+
105112
### Contact Us
106113

107114
See the website at [Elekto.dev](https://elekto.dev).

elekto/core/election.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@
1414
#
1515
# Author(s): Manish Sahani <[email protected]>
1616

17-
from typing import List
17+
from typing import TYPE_CHECKING, List
1818
from .types import BallotType
1919
from pandas import DataFrame
2020
from elekto.core import schulze_d, schulze_p, schulze_rank
2121

22+
if TYPE_CHECKING:
23+
from models.sql import Ballot
24+
25+
2226
class Election:
27+
NO_OPINION = 'No opinion'
28+
MAX_RANK = 100_000_000
29+
2330
def __init__(self, candidates: List[str], ballots: BallotType, no_winners=1):
2431
self.candidates = candidates
2532
self.ballots = ballots
@@ -35,14 +42,14 @@ def schulze(self):
3542
return self
3643

3744
@ staticmethod
38-
def build(candidates, ballots):
45+
def build(candidates: list[dict], ballots: list['Ballot']):
3946
candidates = [c['ID'] for c in candidates]
4047
pref = {}
4148

4249
for b in ballots:
4350
if b.voter not in pref.keys():
4451
pref[b.voter] = []
45-
if b.rank == 100000000:
52+
if b.rank == Election.MAX_RANK:
4653
continue
4754
pref[b.voter].append((b.candidate, int(b.rank)))
4855

@@ -56,7 +63,7 @@ def from_csv(df: DataFrame, no_winners: int):
5663
for v, row in df.iterrows():
5764
ballots[v] = []
5865
for c in candidates:
59-
if row[c] == 'No opinion':
66+
if row[c] == Election.NO_OPINION:
6067
continue
6168
ballots[v].append((c, int(row[c])))
6269

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ certifi==2024.12.14
44
cffi==1.17.1
55
charset-normalizer==3.4.1
66
click==8.1.8
7+
coverage==7.8.0
78
cryptography==44.0.0
9+
factory_boy==3.3.3
810
flask==3.1.0
911
flask-wtf==1.2.2
1012
greenlet==3.1.1

test/conftest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
3+
import pytest
4+
5+
from elekto import APP, SESSION
6+
from elekto.models.sql import drop_all, migrate
7+
8+
from test.factories import ElectionFactory, BallotFactory
9+
10+
11+
@pytest.fixture(scope="module", autouse=True)
12+
def reset_db():
13+
with APP.app_context():
14+
drop_all(APP.config.get('DATABASE_URL'))
15+
16+
17+
@pytest.fixture()
18+
def client():
19+
with APP.app_context():
20+
migrate(APP.config.get('DATABASE_URL'))
21+
yield APP.test_client()
22+
SESSION.close()
23+
drop_all(APP.config.get('DATABASE_URL'))
24+
25+
26+
@pytest.fixture()
27+
def salt() -> bytes:
28+
return os.urandom(16)
29+
30+
31+
@pytest.fixture()
32+
def db_session():
33+
return SESSION
34+
35+
36+
@pytest.fixture(autouse=True)
37+
def db_factories(db_session):
38+
ElectionFactory._meta.sqlalchemy_session = db_session
39+
BallotFactory._meta.sqlalchemy_session = db_session

test/controllers/__init__.py

Whitespace-only changes.

test/test_controllers_elections.py renamed to test/controllers/test_elections.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,10 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime
22
from flask.testing import FlaskClient
3-
import pytest
43
from pytest_mock import MockerFixture
54

65
from elekto import APP, SESSION, constants
7-
from elekto.models import meta
8-
from elekto.models.sql import User, drop_all, migrate
6+
from elekto.models.sql import User
97

10-
@pytest.fixture(scope="module", autouse=True)
11-
def reset_db():
12-
with APP.app_context():
13-
drop_all(APP.config.get('DATABASE_URL'))
14-
15-
16-
@pytest.fixture()
17-
def client():
18-
with APP.app_context():
19-
migrate(APP.config.get('DATABASE_URL'))
20-
yield APP.test_client()
21-
SESSION.close()
22-
drop_all(APP.config.get('DATABASE_URL'))
238

249
def test_dashboard(client: FlaskClient):
2510
token="token"

test/core/__init__.py

Whitespace-only changes.
File renamed without changes.

0 commit comments

Comments
 (0)