Skip to content

Commit 3f1d018

Browse files
authored
Merge pull request #26 from pythonhealthdatascience/dev
refactor(tests): rearrange tests into unit, back and functional
2 parents 5a49671 + d85f115 commit 3f1d018

File tree

4 files changed

+130
-125
lines changed

4 files changed

+130
-125
lines changed

tests/test_backtest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
"""Back testing for the Discrete-Event Simulation (DES) Model.
1+
"""Back Testing
22
3-
These check that the model code produces results consistent with prior code.
3+
Back tests check that the model code produces results consistent with those
4+
generated historically/from prior code.
45
56
Licence:
67
This project is licensed under the MIT Licence. See the LICENSE file for

tests/test_unittest_model.py renamed to tests/test_functionaltest.py

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""Unit testing for the Discrete-Event Simulation (DES) Model.
1+
"""Functional Testing
22
3-
These check specific parts of the simulation and code, ensuring they work
4-
correctly and as expected.
3+
Functional tests verify that the system or components perform their intended
4+
functionality.
55
66
Licence:
77
This project is licensed under the MIT Licence. See the LICENSE file for
@@ -17,21 +17,7 @@
1717
import pandas as pd
1818
import pytest
1919
import simpy
20-
from simulation.model import (
21-
Param, Exponential, Model, Runner, MonitoredResource)
22-
23-
24-
def test_new_attribute():
25-
"""
26-
Confirm that it is impossible to add new attributes to the parameter class.
27-
"""
28-
# No need to test when creating class (e.g. Param(new_entry=3)) as it will
29-
# not allow input of variables not inputs for __init__.
30-
# However, do need to check it is preventing additions after creating class
31-
param = Param()
32-
with pytest.raises(AttributeError,
33-
match='only possible to modify existing attributes'):
34-
param.new_entry = 3
20+
from simulation.model import Param, Model, Runner, MonitoredResource
3521

3622

3723
@pytest.mark.parametrize('param_name, value, rule', [
@@ -404,44 +390,6 @@ def test_interval_audit_time():
404390
)
405391

406392

407-
def test_exponentional():
408-
"""
409-
Test that the Exponentional class behaves as expected.
410-
"""
411-
# Initialise distribution
412-
d = Exponential(mean=10, random_seed=42)
413-
414-
# Check that sample is a float
415-
assert isinstance(d.sample(), float), (
416-
f'Expected sample() to return a float - instead: {type(d.sample())}'
417-
)
418-
419-
# Check that correct number of values are returned
420-
count = len(d.sample(size=10))
421-
assert count == 10, (
422-
f'Expected 10 samples - instead got {count} samples.'
423-
)
424-
425-
bigsample = d.sample(size=100000)
426-
assert all(x > 0 for x in bigsample), (
427-
'Sample contains non-positive values.'
428-
)
429-
430-
# Using the big sample, check that mean is close to expected (allowing
431-
# some tolerance)
432-
assert np.isclose(np.mean(bigsample), 10, atol=0.1), (
433-
'Mean of samples differs beyond tolerance - sample mean ' +
434-
f'{np.mean(bigsample)}, expected mean 10.'
435-
)
436-
437-
# Check that different seeds return different samples
438-
sample1 = Exponential(mean=10, random_seed=2).sample(size=5)
439-
sample2 = Exponential(mean=10, random_seed=3).sample(size=5)
440-
assert not np.array_equal(sample1, sample2), (
441-
'Samples with different random seeds should not be equal.'
442-
)
443-
444-
445393
def test_parallel():
446394
"""
447395
Check that sequential and parallel execution produce consistent results.

tests/test_unittest.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Unit Testing
2+
3+
Unit tests are a type of functional testing that focuses on individual
4+
components (e.g. methods, classes) and tests them in isolation to ensure they
5+
work as intended.
6+
7+
Licence:
8+
This project is licensed under the MIT Licence. See the LICENSE file for
9+
more details.
10+
11+
Typical usage example:
12+
13+
pytest
14+
"""
15+
16+
from io import StringIO
17+
import logging
18+
import os
19+
from unittest.mock import patch, MagicMock
20+
21+
import numpy as np
22+
import pytest
23+
24+
from simulation.logging import SimLogger
25+
from simulation.model import Param, Exponential
26+
27+
28+
def test_new_attribute():
29+
"""
30+
Confirm that it is impossible to add new attributes to the parameter class.
31+
"""
32+
# No need to test when creating class (e.g. Param(new_entry=3)) as it will
33+
# not allow input of variables not inputs for __init__.
34+
# However, do need to check it is preventing additions after creating class
35+
param = Param()
36+
with pytest.raises(AttributeError,
37+
match='only possible to modify existing attributes'):
38+
param.new_entry = 3
39+
40+
41+
def test_exponentional():
42+
"""
43+
Test that the Exponentional class behaves as expected.
44+
"""
45+
# Initialise distribution
46+
d = Exponential(mean=10, random_seed=42)
47+
48+
# Check that sample is a float
49+
assert isinstance(d.sample(), float), (
50+
f'Expected sample() to return a float - instead: {type(d.sample())}'
51+
)
52+
53+
# Check that correct number of values are returned
54+
count = len(d.sample(size=10))
55+
assert count == 10, (
56+
f'Expected 10 samples - instead got {count} samples.'
57+
)
58+
59+
bigsample = d.sample(size=100000)
60+
assert all(x > 0 for x in bigsample), (
61+
'Sample contains non-positive values.'
62+
)
63+
64+
# Using the big sample, check that mean is close to expected (allowing
65+
# some tolerance)
66+
assert np.isclose(np.mean(bigsample), 10, atol=0.1), (
67+
'Mean of samples differs beyond tolerance - sample mean ' +
68+
f'{np.mean(bigsample)}, expected mean 10.'
69+
)
70+
71+
# Check that different seeds return different samples
72+
sample1 = Exponential(mean=10, random_seed=2).sample(size=5)
73+
sample2 = Exponential(mean=10, random_seed=3).sample(size=5)
74+
assert not np.array_equal(sample1, sample2), (
75+
'Samples with different random seeds should not be equal.'
76+
)
77+
78+
79+
def test_log_to_console():
80+
"""
81+
Confirm that logger.log() prints the provided message to the console.
82+
"""
83+
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
84+
logger = SimLogger(log_to_console=True)
85+
logger.log(sim_time=None, msg='Test console log')
86+
# Check if console output matches
87+
assert 'Test console log' in mock_stdout.getvalue()
88+
89+
90+
def test_log_to_file():
91+
"""
92+
Confirm that logger.log() would output the message to a .log file at the
93+
provided file path.
94+
"""
95+
# Mock the file open operation
96+
with patch('builtins.open', new_callable=MagicMock) as mock_open:
97+
# Create the logger and log a simple example
98+
logger = SimLogger(log_to_file=True, file_path='test.log')
99+
logger.log(sim_time=None, msg='Log message')
100+
101+
# Check that the file was opened in write mode at the absolute path
102+
mock_open.assert_called_with(
103+
os.path.abspath('test.log'), 'w', encoding='locale', errors=None)
104+
105+
# Verify a FileHandler is attached to the logger
106+
assert (any(isinstance(handler, logging.FileHandler)
107+
for handler in logger.logger.handlers))
108+
109+
110+
def test_invalid_path():
111+
"""
112+
Ensure there is appropriate error handling for an invalid file path.
113+
"""
114+
with pytest.raises(ValueError):
115+
SimLogger(log_to_file=True, file_path='/invalid/path/to/log.log')
116+
117+
118+
def test_invalid_file_extension():
119+
"""
120+
Ensure there is appropriate error handling for an invalid file extension.
121+
"""
122+
with pytest.raises(ValueError):
123+
SimLogger(log_to_file=True, file_path='test.txt')

tests/test_unittest_logger.py

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

0 commit comments

Comments
 (0)