# 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 ```