Skip to content

Commit f84d9f9

Browse files
authored
Alpha (#1)
* add test cases for invalid arrange * add invalid tests for act * add tests for missing arrange and act * add cases for assert * add initial checks for arrange * write checks for arrange * add checks for act start * add test for filename * add integration tests * add integration tests for docs pattern argument * add support for custom indent size * add tests against own repository * add hypothesis checks * update changelog * add documentation * add workflows
1 parent 2dccca4 commit f84d9f9

14 files changed

+2869
-1
lines changed

.devcontainer/devcontainer.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "Python 3",
3+
"image": "mcr.microsoft.com/devcontainers/python:0-3.11",
4+
"features": {
5+
"ghcr.io/devcontainers/features/python:1": {},
6+
"ghcr.io/devcontainers-contrib/features/black:1": {},
7+
"ghcr.io/devcontainers-contrib/features/tox:1": {},
8+
"ghcr.io/devcontainers-contrib/features/isort:1": {}
9+
}
10+
}

.flake8

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[flake8]
2+
max-line-length = 99
3+
max-doc-length = 99
4+
extend-ignore = E203,W503
5+
per-file-ignores =
6+
tests/*:D205,D400
7+
flake8_test_docs.py:N802
8+
test-docs-pattern = given/when/then

.github/workflows/ci-cd.yaml

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: CI-CD
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- "v[0-9]+.[0-9]+.[0-9]+"
9+
pull_request:
10+
branches:
11+
- main
12+
13+
jobs:
14+
lint:
15+
name: Lint
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v3
19+
- uses: actions/setup-python@v4
20+
with:
21+
python-version: '3.11'
22+
- name: Install tox
23+
run: python -m pip install tox
24+
- name: Run linting
25+
run: tox -e lint
26+
test:
27+
name: Test
28+
runs-on: ubuntu-latest
29+
strategy:
30+
matrix:
31+
python-version:
32+
- "3.8"
33+
- "3.9"
34+
- "3.10"
35+
- "3.11"
36+
steps:
37+
- uses: actions/checkout@v3
38+
- name: Set up Python ${{ matrix.python-version }}
39+
uses: actions/setup-python@v4
40+
with:
41+
python-version: ${{ matrix.python-version }}
42+
- name: Install tox
43+
run: python -m pip install tox
44+
- name: Run testing
45+
run: tox -e test
46+
build:
47+
runs-on: ubuntu-latest
48+
needs:
49+
- test
50+
- lint
51+
steps:
52+
- uses: actions/checkout@v3
53+
- name: Set up Python
54+
uses: actions/setup-python@v4
55+
with:
56+
python-version: "3.11"
57+
- name: Install poetry
58+
run: pip install poetry
59+
- uses: actions/cache@v3
60+
id: cache-poetry
61+
with:
62+
path: ~/.virtualenvs
63+
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock', 'poetry.toml') }}
64+
- name: Configure poetry for ci
65+
run: |
66+
poetry config virtualenvs.in-project false --local
67+
poetry config virtualenvs.path ~/.virtualenvs --local
68+
- name: Install dependencies
69+
if: steps.cache-poetry.outputs.cache-hit != 'true'
70+
run: |
71+
poetry install
72+
- name: Build packages
73+
run: poetry build
74+
- name: Upload artifacts for release
75+
if: startsWith(github.ref, 'refs/tags/')
76+
uses: actions/upload-artifact@v3
77+
with:
78+
name: wheel
79+
path: dist/
80+
release-test-pypi:
81+
runs-on: ubuntu-latest
82+
if: startsWith(github.ref, 'refs/tags/')
83+
needs:
84+
- build
85+
steps:
86+
- name: Retrieve packages
87+
uses: actions/download-artifact@v3
88+
with:
89+
name: wheel
90+
path: dist/
91+
- name: Publish distribution 📦 to Test PyPI
92+
uses: pypa/gh-action-pypi-publish@v1.6.4
93+
with:
94+
password: ${{ secrets.test_pypi_password }}
95+
repository_url: https://test.pypi.org/legacy/
96+
release-pypi:
97+
runs-on: ubuntu-latest
98+
if: startsWith(github.ref, 'refs/tags/')
99+
needs:
100+
- release-test-pypi
101+
steps:
102+
- name: Retrieve packages
103+
uses: actions/download-artifact@v3
104+
with:
105+
name: wheel
106+
path: dist/
107+
- name: Publish distribution 📦 to PyPI
108+
if: startsWith(github.ref, 'refs/tags')
109+
uses: pypa/gh-action-pypi-publish@v1.6.4
110+
with:
111+
password: ${{ secrets.pypi_password }}
112+
repository_url: https://upload.pypi.org/legacy/
113+
release-github:
114+
runs-on: ubuntu-latest
115+
if: startsWith(github.ref, 'refs/tags/')
116+
needs:
117+
- release-pypi
118+
steps:
119+
- name: Get version from tag
120+
id: tag_name
121+
run: |
122+
echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v}
123+
shell: bash
124+
- uses: actions/checkout@v3
125+
- name: Get latest Changelog Entry
126+
id: changelog_reader
127+
uses: mindsers/changelog-reader-action@v2
128+
with:
129+
version: v${{ steps.tag_name.outputs.current_version }}
130+
path: ./CHANGELOG.md
131+
- name: Create Release
132+
id: create_release
133+
uses: actions/create-release@v1
134+
env:
135+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
136+
with:
137+
tag_name: ${{ steps.changelog_reader.outputs.version }}
138+
release_name: Release ${{ steps.changelog_reader.outputs.version }}
139+
body: ${{ steps.changelog_reader.outputs.changes }}
140+
prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }}
141+
draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }}

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"python.analysis.typeCheckingMode": "basic",
3+
"python.linting.pylintEnabled": true,
4+
"python.linting.enabled": true
5+
}

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelog
2+
3+
## [Unreleased]
4+
5+
## [v1.0.0] - 2022-12-23
6+
7+
### Added
8+
9+
- Lint checks for test docs using the arrange/act/assert pattern
10+
- Lint checks for longer descriptions of each stage
11+
- `--test-docs-pattern` argument to customise the docstring pattern
12+
- `--test-docs-filename-pattern` argument to customise the test file discovery
13+
- `--test-docs-function-pattern` argument to customise the test function
14+
discovery
15+
- support for flake8 `--indent-size` argument
16+
17+
[//]: # "Release links"
18+
[v1.0.0]: https://github.com/jdkandersson/flake8-test-docs/releases/v1.0.0

README.md

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,181 @@
11
# flake8-test-docs
2-
Linter that checks test docstrings for the arrange/act/assert or given/when/then structure
2+
3+
Have you ever needed to understand a new project and started reading the tests
4+
only to find that you have no idea what the tests are doing? Good test
5+
documentation is critical during test definition and when reviewing tests
6+
written in the past or by someone else. This linter checks that the test
7+
function docstring includes a description of the test setup, execution and
8+
checks.
9+
10+
## Getting Started
11+
12+
```shell
13+
python -m venv venv
14+
source ./venv/bin/activate
15+
pip install flake8 flake8-test-docs
16+
flake8 test_source.py
17+
```
18+
19+
On the following code:
20+
21+
```Python
22+
# test_source.py
23+
def test_foo():
24+
value = foo()
25+
assert value == "bar"
26+
```
27+
28+
This will produce warnings such as:
29+
30+
```shell
31+
flake8 test_source.py
32+
test_source.py:2:1: TDO001 Docstring not defined on test function, more information: https://github.com/jdkandersson/flake8-test-docs#fix-tdo001
33+
```
34+
35+
This can be resolved by changing the code to:
36+
37+
```Python
38+
# test_source.py
39+
def test_foo():
40+
"""
41+
arrange: given foo that returns bar
42+
act: when foo is called
43+
assert: then bar is returned
44+
"""
45+
value = foo()
46+
assert value == "bar"
47+
```
48+
49+
## Configuration
50+
51+
The plugin adds the following configurations to `flake8`:
52+
53+
* `--test-docs-patter`: The pattern the test documentation should follow,
54+
e.g., `given/when/then`. Defaults to `arrange/act/assert`.
55+
* `--test-docs-filename-pattern`: The filename pattern for test files. Defaults
56+
to `test_.*.py`.
57+
* `--test-docs-function-pattern`: The function pattern for test functions.
58+
Defaults to `test_.*`.
59+
60+
61+
## Rules
62+
63+
A few rules have been defined to allow for selective suppression:
64+
65+
* `TDO001`: checks that test functions have a docstring.
66+
* `TDO002`: checks that test function docstrings follow the documentation
67+
pattern.
68+
69+
### Fix TDO001
70+
71+
This linting rule is triggered by a test function in a test file without a
72+
docstring. For example:
73+
74+
```Python
75+
# test_source.py
76+
def test_foo():
77+
result = foo()
78+
assert result == "bar"
79+
```
80+
81+
This example can be fixed by:
82+
83+
```Python
84+
# test_source.py
85+
def test_foo():
86+
"""
87+
arrange: given foo that returns bar
88+
act: when foo is called
89+
assert: then bar is returned
90+
"""
91+
result = foo()
92+
assert result == "bar"
93+
```
94+
95+
### Fix TDO002
96+
97+
This linting rule is triggered by a test function in a test file with a
98+
docstring that doesn't follow the documentation pattern. For example:
99+
100+
```Python
101+
# test_source.py
102+
def test_foo():
103+
"""Test foo."""
104+
result = foo()
105+
assert result == "bar"
106+
```
107+
108+
This example can be fixed by:
109+
110+
```Python
111+
# test_source.py
112+
def test_foo():
113+
"""
114+
arrange: given foo that returns bar
115+
act: when foo is called
116+
assert: then bar is returned
117+
"""
118+
result = foo()
119+
assert result == "bar"
120+
```
121+
122+
The message of the linting rule should give you the specific problem with the
123+
documentation. In general, the pattern is:
124+
125+
* start on the second line with the same indentation is the start of the
126+
docstring
127+
* the starting line should begin with `arrange:` (or whatever was set using
128+
`--test-docs-patter`) followed by at least some words describing the test
129+
setup
130+
* long test setup descriptions can be broken over multiple lines by indenting
131+
the lines after the first by one level (e.g., 4 spaces by default)
132+
* this is followed by similar sections starting with `act:` describing the test
133+
execution and `assert:` describing the checks
134+
* the last line should be indented the same as the start of the docstring
135+
136+
Below are some valid examples. Starting with a vanilla example:
137+
138+
```Python
139+
# test_source.py
140+
def test_foo():
141+
"""
142+
arrange: given foo that returns bar
143+
act: when foo is called
144+
assert: then bar is returned
145+
"""
146+
result = foo()
147+
assert result == "bar"
148+
```
149+
150+
Here is an example where the test function is in a nested scope:
151+
152+
```Python
153+
# test_source.py
154+
class TestSuite:
155+
156+
def test_foo():
157+
"""
158+
arrange: given foo that returns bar
159+
act: when foo is called
160+
assert: then bar is returned
161+
"""
162+
result = foo()
163+
assert result == "bar"
164+
```
165+
166+
Here is an example where each of the descriptions go over multiple lines:
167+
168+
```Python
169+
# test_source.py
170+
def test_foo():
171+
"""
172+
arrange: given foo
173+
that returns bar
174+
act: when foo
175+
is called
176+
assert: then bar
177+
is returned
178+
"""
179+
result = foo()
180+
assert result == "bar"
181+
```

0 commit comments

Comments
 (0)