Skip to content

Commit

Permalink
Merge pull request #29 from Phoupraw/coverage
Browse files Browse the repository at this point in the history
ci: Coverage, test: More
  • Loading branch information
YDX-2147483647 authored Aug 9, 2023
2 parents 7616c99 + a58277d commit a4a3e80
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 7 deletions.
1 change: 1 addition & 0 deletions contest/contest/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"debug": DEBUG, # for django_coverage_plugin
},
},
]
Expand Down
142 changes: 136 additions & 6 deletions contest/quiz/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,129 @@ def test_finalize_response(self):

class ContestViewTests(TestCase):
def setUp(self):
Question.objects.create(content="Angel Attack")
Question.objects.create(content="The Beast")
Question.objects.create(content="A Transfer")
contents_map = {
"Angel Attack": [
"Emergency in Tokai",
"Angel Attack",
"N2 Mine ~ Enroute",
"The Car Train ~ Tokyo-3",
],
"The Beast": [
"The Welcoming Party",
"Pen2 ~ Laundry of Life",
"The Beast: Part A",
"The Beast: Part B",
'''Eva's True State ~ "Good Night"''',
],
"A Transfer": [
"Training",
"Hedgehog's Dilemma",
"Toji",
"The New Kid ~ Emergency",
],
}
for question, choices in contents_map.items():
q = Question.objects.create(content=question)
Choice.objects.bulk_create(
[Choice(content=c, correct=bool(i), question=q) for i, c in enumerate(choices)]
)

self.user = User.objects.create_user(username="Shinji")
self.student = Student.objects.create(user=self.user)

def test_info_view(self):
"""访问个人中心"""

self.client.force_login(self.user)

response = self.client.get(reverse("quiz:info"))
self.assertEqual(response.status_code, HTTPStatus.OK)

def test_contest_view(self):
"""访问首页,登录,然后开始作答"""
"""访问首页,登录,然后开始作答,再原地刷新"""

response = self.client.get(reverse("quiz:index"))
self.assertEqual(response.status_code, HTTPStatus.OK)

self.client.force_login(self.user)

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.OK)
draft = self.user.student.draft_response

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(response.context["draft_response"], draft)

def test_contest_update_view(self):
"""暂存"""
self.client.force_login(self.user)

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.OK)

answer = self.user.student.draft_response.answer_set.all()[0]
question = answer.question
choice = question.choice_set.all()[0]

# 正常暂存
form = {
f"question-{question.id}": f"choice-{choice.id}",
"csrf_token_etc": "Whatever",
}
response = self.client.post(reverse("quiz:contest_update"), form)
self.assertEqual(response.status_code, HTTPStatus.OK)
answer.refresh_from_db()
self.assertEqual(answer.choice, choice)

# “时光飞逝”
self.user.student.draft_response.deadline -= constants.DEADLINE_DURATION
# -1 s
self.user.student.draft_response.deadline -= timedelta(seconds=1)
self.user.student.draft_response.save()

# 超时后禁止
response = self.client.post(reverse("quiz:contest_update"), form)
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)

def test_bad_contest_update(self):
"""暂存非法数据"""
self.client.force_login(self.user)

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.OK)

def test_auto_submission(self):
"""自动提交"""
answer = self.user.student.draft_response.answer_set.all()[0]
question = answer.question

response = self.client.post(
reverse("quiz:contest_update"),
{f"question-{question.id}": "not a choice"},
)
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)

response = self.client.post(
reverse("quiz:contest_update"),
{"question--3": "choice-0"},
)
self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)

response = self.client.post(
reverse("quiz:contest_update"),
{f"question-{question.id}": "choice--3"},
)
self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)

def test_bad_contest_submit(self):
"""非法提交"""
self.client.force_login(self.user)

# 还没发卷呢
response = self.client.post(reverse("quiz:contest_submit"))
self.assertNotEqual(response.status_code, HTTPStatus.OK)

def test_status(self):
"""自动提交的状态"""
response = self.client.get(reverse("quiz:index"))
self.assertEqual(response.context["status"], "")

Expand Down Expand Up @@ -113,6 +216,33 @@ def test_empty_response(self):
self.assertEqual(response.status_code, HTTPStatus.FOUND)
self.assertEqual(response.url, reverse("quiz:info"))

response = self.client.get(reverse("quiz:info"))
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(self.user.student.final_score(), 0)

def test_too_many_tries(self):
"""答题次数超限"""
template_response = Response(submit_at=timezone.now(), student=self.user.student)
Response.objects.bulk_create(
[template_response for _ in range(constants.MAX_TRIES - 1)]
)

self.client.force_login(self.user)

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertTrue(hasattr(self.user.student, "draft_response"))

response = self.client.post(reverse("quiz:contest_submit"))
self.assertNotEqual(response.status_code, HTTPStatus.FORBIDDEN)
self.user.student.refresh_from_db()
self.assertEqual(self.user.student.response_set.count(), constants.MAX_TRIES)

response = self.client.get(reverse("quiz:contest"))
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
self.user.student.refresh_from_db()
self.assertFalse(hasattr(self.user.student, "draft_response"))

def test_non_student_user(self):
"""如果登录了但不是学生,应当禁止访问"""
user = User.objects.create_user(username="Keel")
Expand Down
29 changes: 29 additions & 0 deletions doc/coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Coverage

利用 [coverage.py](https://coverage.readthedocs.io/) 评估测试的覆盖范围。(包括 python 代码和 Django 模板)

```shell
$ poetry install --with coverage
```

```shell
# 请在项目根目录运行

$ poetry run coverage run
$ poetry run coverage html
# 然后单击 htmlcov/index.html 链接
```

## 为何没加入`justfile`

运行 Django 项目序号`manage.py`所在目录在`sys.path`上。`coverage run`会向`sys.path`加入`manage.py`所在目录,而`python -m coverage run`会加入当前目录。这种区别导致后者无法正常运行。

> **Note**
>
> 这与虚拟环境是否启用无关——`./.venv/Scripts/coverage.exe``./.venv/Scripts/python.exe -m coverage`同样符合上述描述。
如果要加入`justfile`,最好保证未启用虚拟环境也能正常工作。然而无虚拟环境时,`coverage`并不在`$PATH`上,只有`{{ python }} -m coverage`能可靠一致地被调用——`sys.path``$PATH`难以兼顾。

Mypy 提供了`mypy_path`配置 [import discovery](https://mypy.readthedocs.io/en/latest/command_line.html#import-discovery),可解决`sys.path`的问题,从而`{{ python }} -m mypy`即可。很不幸 coverage.py 似乎没有类似机制。

考虑到无需一直关注测试覆盖范围,就不加入`justfile`了。
101 changes: 100 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a4a3e80

Please sign in to comment.