# Collaborative Research Software Engineering in Python - solutions to tasks
<span style="color:green">**! TASK 1 !** </span> **Write a new test case that tests the ```daily_max()``` function**</span>, adding it to ```test/test_models.py```. Also regenerate your ```requirements.txt``` file, commit your changes, and merge the ```test-suite``` branch with the ```develop``` branch. (5-10 min)
- You could choose to write your functions very similarly to ```daily_mean()```, defining input and expected output variables followed by the equality assertion.
- Use test cases that are suitably different.
- Once added, run all the tests again with ```python -m pytest tests/test_models.py```, and have a look at your new tests pass.
**Solution** (examples)
```python=
def test_daily_max_integers():
"""Test that max function works for an array of positive integers."""
from inflammation.models import daily_max
test_input = np.array([[4, 2, 5],
[1, 6, 2],
[4, 1, 9]])
test_result = np.array([4, 6, 9])
npt.assert_array_equal(daily_max(test_input), test_result)
```
One can also check cases where seeing an error is the correct behaviour (we need to import ```pytest``` to be able to execute ```pytest.raises()```):
```python=
import pytest
...
def test_daily_max_string():
"""Test for TypeError when passing strings"""
from inflammation.models import daily_max
with pytest.raises(TypeError):
error_expected = daily_max([['Hi', 'there'], ['Alfe', 'folks']])
```
We regenerate our ```requirements.txt``` file, commit our changes, and merge the ```test-suite``` branch with the ```develop``` branch.
```
$ pip3 freeze > requirements.txt
$ git add requirements.txt tests/test_models.py # alternative: git add ./
$ git commit -m "Add initial test cases for daily_max() and daily_min()"
$ git checkout develop
$ git merge test-suite
```
<span style="color:green">**! TASK 2 !**</span> **Rewrite your test functions for ```daily_max()``` using test parameterisation.** (5-10 min)
- Make sure you're back in the ```test-suite``` branch.
- Once added, run all the tests again with ```python -m pytest tests/test_models.py```, and have a look at your new tests pass.
- Commit your changes, and merge the ```test-suite``` branch with the ```develop``` branch.
**Solution** (examples)
```python=
@pytest.mark.parametrize(
"test, expected",
[
([ [0, 0], [0, 0], [0, 0] ], [0, 0]),
([ [1, 2], [3, 4], [5, 6] ], [5, 6]),
])
def test_daily_max(test, expected):
"""Test mean function works for array of zeroes and positive integers."""
from inflammation.models import daily_max
npt.assert_array_equal(daily_max(np.array(test)), np.array(expected))
```
We can also incorporate the case where seeing an error is the correct behaviour by including a third argument name indicating the expected error raises (e.g., ```expect_raises```), as well as the expected error raises (```None``` for where we don't expect to see any):
```python=
@pytest.mark.parametrize(
"test, expected, expect_raises",
[
([ [0, 0], [0, 0], [0, 0] ], [0, 0], None),
([ [1, 2], [3, 4], [5, 6] ], [5, 6], None),
([['Hi', 'there'], ['ALife', 'folks']], ['Whats', 'up'] , TypeError)
])
def test_daily_max(test, expected, expect_raises):
"""Test mean function works for array of zeroes and positive integers."""
from inflammation.models import daily_max
if expect_raises is not None:
with pytest.raises(TypeError):
daily_max([['Hi', 'there'], ['ALife', 'folks']])
else:
npt.assert_array_equal(daily_max(np.array(test)), np.array(expected))
```
<span style="color:green">**! QUESTION 1 !</span> What outputs do you expect here?**
```python=
Alice = Patient('Alice')
print(Alice)
obs = Alice.add_observation(3)
print(obs)
Bob = Person('Bob')
print(Bob)
obs = Bob.add_observation(4)
print(obs)
```
Output:
```python
Alice
3
Bob
AttributeError: 'Person' object has no attribute 'add_observation'
```
An error is thrown because we cannot add an observation to bob, who is a Person but not a Patient.
<span style="color:green">**! TASK 3 !</span> Write a Doctor class to hold the data representing a single doctor**:
- It should have a name attribute, as well as a list of patients that this doctor is responsible for.
- If you have the time, write corresponding tests in ```test_patient.py```.
```python=
class Doctor(Person):
"""A doctor in an inflammation study."""
def __init__(self, name):
super().__init__(name)
self.patients = []
def add_patient(self, new_patient):
# A crude check by name if this patient is already looked after
# by this doctor before adding them
for patient in self.patients:
if patient.name == new_patient.name:
return
self.patients.append(new_patient)
def __str__(self):
return {'name': self.name, 'patients': self.patients}
```
- Doctor class inherits from Person class, and instantiates ```self.patients``` as another attribute (an empty list at the start).
- We add patients to doctors in the same way as observations had been added to patients.
Possible tests:
```python=
def test_add_patient_to_doctor():
"""Check patients are being added correctly by a doctor. """
from inflammation.models import Doctor, Patient
doc = Doctor("Sheila Wheels")
new_patient = Patient("Bob")
doc.add_patient(new_patient)
assert doc.patients is not None
assert len(doc.patients) == 1
```
- We instantiate both Doctor and Patient objects, add the patient to the doctor, and check whether ```doc.patients``` is neither ```None```, nor empty.
```python=
def test_doctor_is_person():
"""Check if a doctor is a person."""
from inflammation.models import Doctor, Person
doc = Doctor("Sheila Wheels")
assert isinstance(doc, Person)
```
- We instantiate a Doctor object, and check whether it is a Person object using ```isinstance()```.
<span style="color:green">**! QUESTION 2 !**</span> Which of these functions are pure?
```python=
def add_one(x):
return x + 1
def say_hello(name):
print('Hello', name)
def append_item_1(a_list, item):
a_list += [item]
return a_list
def append_item_2(a_list, item):
result = a_list + [item]
return result
```
- ```add_one``` is pure - it has no effects other than to return a value and this value will always be the same when given the same inputs.
- ```say_hello``` is not pure - printing text is a side effect.
- ```append_item_1``` is not pure - the argument ```a_list``` gets modified as a side effect.
- ```append_item_2``` is pure - ```result``` is a new variable, so this time ```a_list``` does not get modified.
<span style="color:green">**! TASK 4 !</span> Write a decorator that measures the time time taken to execute a particular function** using the time.process_time_ns() function.
- You need to import ```time```.
- To get a time stamp, you can simply write ```start = time.process_time_ns()```, and get another time stamp once the calculation in question is done using ```end = time.process_time_ns()```.
- Use this function to measure its execution time:
```python=
def measure_me(n):
total = 0
for i in range(n):
total += i * i
return total
```
Solution (example):
```python=
import time
def profile(func):
def inner(*args, **kwargs):
start = time.process_time_ns()
result = func(*args, **kwargs)
stop = time.process_time_ns()
print("Took {0} seconds".format((stop - start) / 1e9))
return result
return inner
@profile
def measure_me(n):
total = 0
for i in range(n):
total += i * i
return total
print(measure_me(1000000))
```
Output
```python=
Took 0.124199753 seconds
333332833333500000
```