Skip to content

Commit 6559119

Browse files
committed
Revamp functions used in the Python test demo, and their corresponding tests
1 parent d644f95 commit 6559119

File tree

6 files changed

+159
-139
lines changed

6 files changed

+159
-139
lines changed

05_testing_and_ci/examples/python_testing/mean_data.csv

Lines changed: 0 additions & 2 deletions
This file was deleted.

05_testing_and_ci/examples/python_testing/operations.py

Lines changed: 97 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,109 @@
22
A set of mathematical operations.
33
"""
44

5-
6-
def find_max(data):
7-
"""
8-
Find maximum of all elements of a given list
9-
10-
Parameters
11-
----------
12-
data : list
13-
List of data. Elements are numbers
14-
15-
Returns
16-
-------
17-
find_max : float
18-
Maximum of list
19-
"""
20-
# Check that the input list has numbers
21-
for n in data:
22-
assert type(n) == int or type(n) == float
23-
24-
max_num = data[0] # Assume the first number is the maximum
25-
for n in data:
26-
if n > max_num:
27-
max_num = n
28-
29-
return max_num
30-
31-
32-
def find_mean(data):
33-
"""
34-
Find mean of all elements of a given list
35-
36-
Parameters
37-
----------
38-
data : list
39-
List of data. Elements are numbers
40-
41-
Returns
42-
-------
43-
float : float
44-
Mean of list
45-
"""
46-
# Check that the input list has numbers
47-
for n in data:
48-
assert type(n) == int or type(n) == float
49-
50-
return sum(data) / len(data)
5+
class MathOperations:
6+
7+
def __init__(self, data):
8+
self._data = data
9+
10+
def reorder_data(self):
11+
"""
12+
Reorder data in ascending order
13+
"""
14+
self._data.sort()
15+
16+
def find_max(self):
17+
"""
18+
Find maximum of all elements of a given list
19+
Parameters
20+
----------
21+
data : list
22+
List of data. Elements are numbers
23+
24+
Returns
25+
-------
26+
find_max : float
27+
Maximum of list
28+
"""
29+
# Check that the input list has numbers
30+
for n in self._data:
31+
assert type(n) == int or type(n) == float
32+
33+
max_num = self._data[0] # Assume the first number is the maximum
34+
for n in self._data:
35+
if n > max_num:
36+
max_num = n
37+
38+
return max_num
39+
40+
def find_median(self):
41+
"""
42+
Find median of all elements of a given list
43+
44+
Parameters
45+
----------
46+
data : list
47+
List of data. Elements are numbers
48+
49+
Returns
50+
-------
51+
float : float
52+
Median of list
53+
"""
54+
# Check that the input list has numbers
55+
for n in self._data:
56+
assert type(n) == int or type(n) == float
57+
58+
# Sort the data to find the median
59+
sorted_data = sorted(self._data)
60+
n = len(sorted_data)
61+
62+
# If odd number of elements, return the middle one
63+
if n % 2 == 1:
64+
return sorted_data[n // 2]
65+
# If even number of elements, return the average of the two middle ones
66+
else:
67+
mid1 = sorted_data[n // 2 - 1]
68+
mid2 = sorted_data[n // 2]
69+
return (mid1 + mid2) / 2
70+
71+
def find_mean(self):
72+
"""
73+
Find mean of all elements of a given list
74+
75+
Parameters
76+
----------
77+
data : list
78+
List of data. Elements are numbers
79+
80+
Returns
81+
-------
82+
float : float
83+
Mean of list
84+
"""
85+
# Check that the input list has numbers
86+
for n in self._data:
87+
assert type(n) == int or type(n) == float
88+
89+
total = sum(self._data)
90+
count = len(self._data)
91+
mean = total / count
92+
return mean
5193

5294

5395
def main():
54-
data = [5, 3, 14, 27, 4, 9]
96+
data = [5, 3, 14, 27, 4, 9, 53]
97+
98+
math_ops = MathOperations(data)
5599

56-
maximum = find_max(data)
100+
maximum = math_ops.find_max()
57101
print("Maximum of {} is {}".format(data, maximum))
58102

59-
mean = find_mean(data)
60-
print("Average of {} is {}".format(data, mean))
103+
median = math_ops.find_median()
104+
print("Median of {} is {}".format(data, median))
105+
106+
mean = math_ops.find_mean()
107+
print("Mean of {} is {}".format(data, mean))
61108

62109

63110
if __name__ == "__main__":
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1,17,18,32,43,167,209

05_testing_and_ci/examples/python_testing/test_operations.py

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,91 @@
22
Tests for mathematical operations functions.
33
"""
44

5-
from operations import find_max, find_mean
5+
from operations import MathOperations
66
import pytest
77
import csv
88

99

10+
@pytest.fixture
11+
def math_operations():
12+
"""
13+
Fixture for MathOperations class
14+
"""
15+
data = [43, 32, 167, 18, 1, 209, 17]
16+
return MathOperations(data)
17+
1018
# Unit test
11-
def test_find_max():
19+
def test_find_max(math_operations):
1220
"""
1321
Test operations.find_max
1422
"""
15-
# Fixture
16-
data = [43, 32, 167, 18, 1, 209]
17-
1823
# Expected result
1924
expected_max = 209
2025

2126
# Actual result
22-
actual_max = find_max(data)
27+
actual_max = math_operations.find_max()
2328

2429
# Test
2530
assert actual_max == expected_max
2631

32+
# Unit test
33+
def test_reorder_data(math_operations):
34+
"""
35+
Test operations.reorder_data
36+
"""
37+
# Expected result
38+
expected_data = [1, 17, 18, 32, 43, 167, 209]
39+
40+
# Actual result
41+
math_operations.reorder_data()
42+
actual_data = math_operations._data
43+
44+
# Test
45+
assert actual_data == expected_data
2746

2847
# Unit test
29-
def test_find_mean():
48+
def test_find_mean(math_operations):
3049
"""
3150
Test operations.find_mean
3251
"""
33-
# Fixture
34-
data = [43, 32, 167, 18, 1, 209]
35-
3652
# Expected result
37-
expected_mean = 78.33
38-
# expected_mean = pytest.approx(78.33, abs=0.01)
39-
53+
expected_mean = 69.57
54+
4055
# Actual result
41-
actual_mean = find_mean(data)
42-
56+
actual_mean = math_operations.find_mean()
57+
4358
# Test
4459
assert actual_mean == expected_mean
4560

46-
4761
# Integration test
48-
def test_mean_of_max():
49-
"""
50-
Test operations.find_max and operations.find_mean
62+
def test_find_median(math_operations):
5163
"""
52-
# Fixture
53-
data1 = [43, 32, 167, 18, 1, 209]
54-
data2 = [3, 13, 33, 23, 498]
55-
64+
Test operations.find_median
65+
"""
5666
# Expected result
57-
expected_mean_of_max = 353.5
58-
59-
maximum1 = find_max(data1)
60-
maximum2 = find_max(data2)
61-
67+
expected_median = 32
68+
6269
# Actual result
63-
actual_mean_of_max = find_mean([maximum1, maximum2])
70+
actual_median = math_operations.find_median()
6471

6572
# Test
66-
assert actual_mean_of_max == expected_mean_of_max
73+
assert actual_median == expected_median
6774

6875

6976
# Regression test
70-
def test_regression_mean():
77+
def test_reg_reorder_data(math_operations):
7178
"""
72-
Test operations.find_mean on a previously generated dataset
79+
Test operations.reorder_data with data from CSV file
7380
"""
74-
with open("mean_data.csv") as f:
81+
with open("reordered_data.csv") as f:
7582
rows = csv.reader(f, quoting=csv.QUOTE_NONNUMERIC)
76-
# Fixture
77-
data = next(rows)
78-
79-
# Expected result
80-
reference_mean = next(rows)
83+
84+
for row in rows:
85+
expected_reordered_data = row
8186

8287
# Actual result
83-
actual_mean = find_mean(data)
84-
85-
expected_mean = pytest.approx(reference_mean[0], abs=0.01)
86-
88+
math_operations.reorder_data()
89+
actual_reordered_data = math_operations._data
90+
8791
# Test
88-
assert actual_mean == expected_mean
92+
assert actual_reordered_data == expected_reordered_data

05_testing_and_ci/python_testing_demo.md

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,57 +4,26 @@ Example code is in [05_testing_and_ci/examples/python_testing](https://github.co
44

55
## Software Code Used
66

7-
- The file `operations.py` consists of two functions `find_max` and `find_mean` which calculate the maximum and mean of all elements of a list. The `main()` routine in the file applies the functions to a list and prints the output.
8-
- `main()` function in `operations.py` has assertion statements to check if the correct data type is passed to specific functions.
9-
- Assertion statements are the most basic way of testing code and are also used in unit and integration testing.
10-
- Tests are written in the file `test_operations.py`. The `test_*` prefix in the name is required so that pytest detects the file as a testing file. Suffix form `*_test.py` also works.
11-
- In all there are two unit tests, one integration test and one regression test.
12-
- The unit tests test the individual functions `find_max` and `find_mean`.
13-
- The integration test triggers both the functions `find_max` and `find_mean` and checks that the mean is less than the maximum, something that should always be true for a set of numbers.
14-
- The regression test first reads an old data set and a mean value from a CSV file. Then the function `find_mean` is run with the old data set and the new mean value is compared to the old one.
7+
The file [operations.py](examples/python_testing/operations.py) consists of a class `MathOperations` that has the following functions: `reorder_data`, `find_max`, `find_median`, and `find_mean`. The `main()` routine in the file applies the functions to a list and prints the output.
158

169
## pytest
1710

1811
- pytest is installed using pip: `pip install pytest`.
1912
- All tests can be run using the command-line tool called `pytest`. Just type `pytest` in the working directory and hit ENTER.
2013
- If pytest is installed in some other way, you might need to run it like `python -m pytest`.
21-
- One test is expected to fail. Reading the error message we understand that the failure occurs because floating-point variable comparison is not handled correctly.
22-
- We need to tell pytest that while comparing two floating-point variables the value needs to be correct only up to a certain tolerance limit. To do this, the expected mean value needs to be changed by uncommenting the line in the following part of the code:
14+
- Tests are written in the file `test_operations.py`. The `test_*` prefix in the name is required so that pytest detects the file as a testing file. Suffix form `*_test.py` also works.
15+
- There are unit tests for the functions `reorder_data`, `find_max`, and `find_mean`.
16+
- There is an integration test for the function `find_median`, and a regression test for `reorder_data`. The regression test reads in a list from a CSV file.
17+
- The test fixture is defined under `@pytest.fixture`. pytest runs this once at the start and stores the returned output while running all other tests.
18+
- One test fails. Error message states that the failure occurs because floating-point variable comparison is not handled correctly.
19+
- While comparing two floating-point variables the value needs to be correct only up to a certain tolerance limit. To do this, the expected mean value needs to be changed in the following way:
2320

2421
```python
2522
# Expected result
26-
expected_mean = 78.33
27-
# expected_result = pytest.approx(78.3, abs=0.01)
28-
```
29-
30-
- **Comparing floating point variables** needs to be handled in functions like `find_mean` and is done using `pytest.approx(value, abs)`. The `abs` value is the tolerance up to which the floating-point value will be checked, that is `78.33 +/- 0.01`.
31-
- Even if one test fails, pytest runs all the tests and gives a report on the failing test. The assertion failure report generated my pytest is also more detailed than the usual Python assertion report. When the test fails, the following is observed:
32-
33-
```bash
34-
========================================== FAILURES ===============================================
35-
_______________________________________ test_find_mean ____________________________________________
36-
37-
def test_find_mean():
38-
"""
39-
Test operations.find_mean
40-
"""
41-
# Fixture
42-
data = [43, 32, 167, 18, 1, 209]
43-
44-
# Expected result
45-
expected_mean = 78.33
46-
# expected_result = pytest.approx(78.33, abs=0.01)
47-
48-
# Actual result
49-
actual_mean = find_mean(data)
50-
51-
# Test
52-
> assert actual_mean == expected_mean
53-
E assert 78.33333333333333 == 78.33
54-
55-
test_operations.py:44: AssertionError
23+
expected_mean = pytest.approx(69.57, rel=1e-2)
5624
```
5725

26+
- Even if one test fails, pytest runs the rest and gives a report on the failing test.
5827
- pytest not only points to the assertion but also prints out the test which has failed.
5928
- It is worth noting that pytest is also able to detect tests from other files and run them even if they are not in the conventional test formats.
6029
- pytest is able to detect tests in several forms of folder structures, and the folder structures have advantages and disadvantages. More information on this is in the [documentation](https://docs.pytest.org/en/6.2.x/goodpractices.html#choosing-a-test-layout-import-rules). In this demo we use the simplest folder structure where the source file and the test files are at the same directory level. Very often this is not the case. A more organized folder structure can be generated:

05_testing_and_ci/python_testing_slides.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ slideOptions:
2929

3030
## pytest
3131

32-
- Library to write and manage tests.
33-
- Command-line tool also called `pytest`.
34-
- Install using pip: `pip install -U pytest`.
32+
- Package to write and manage tests.
33+
- Includes command-line interface called `pytest`.
34+
- Install using pip: `pip install pytest`.
3535
- All tests need to be in files named `test_*.py`.
3636
- Each test function needs to be named as `test_*`.
37-
- pytest gives a detailed description of assertion checks.
37+
- pytest gives a detailed description for failing tests.
3838

3939
---
4040

@@ -48,6 +48,7 @@ slideOptions:
4848
- Many features like test automation, sharing of setup and shutdown of tests, etc.
4949
- Use the base class `unittest.TestCase` to create a test suite.
5050
- Command-line interface: `python -m unittest test_module1 test_module2 ...`.
51+
- Part of the Python standard library.
5152

5253
---
5354

0 commit comments

Comments
 (0)