From 8cda759e3c2dcb798bd90a6d99f48e3e9ddcd01c Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 16:35:22 +0100 Subject: [PATCH 1/6] Step 2: Add type assertions --- diffusion2d.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/diffusion2d.py b/diffusion2d.py index 51a07f2d..8d1c2b99 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -38,6 +38,11 @@ def __init__(self): self.dt = None def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): + # You could argue individual assertions are better, since it allows to understand which values have the wrong type. + # However, for this exercise I keep it like this for initialize_domain and add individual assertions in initialize_physical_parameters + # In some cases individual assertions might not even allow for better understanding, if the assertion is deep in a library and the user might + # not understand how the values end up there. It probably is a case-by-case decision. + assert all(map(lambda v: type(v) == float, [w, h, dx, dy])), "All inputs must be floats" self.w = w self.h = h self.dx = dx @@ -45,7 +50,10 @@ def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): self.nx = int(w / dx) self.ny = int(h / dy) - def initialize_physical_parameters(self, d=4., T_cold=300, T_hot=700): + def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.): + assert type(d) == float, "d must be a float" + assert type(T_cold) == float, "T_cold must be a float" + assert type(T_hot) == float, "T_cold must be a float" self.D = d self.T_cold = T_cold self.T_hot = T_hot From a82e2eec569947c32185cbc91bf84536c19392f5 Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 16:37:11 +0100 Subject: [PATCH 2/6] Step 3: pytest Unit-Tests --- README.md | 240 +++++++++++++++++++++++ tests/unit/test_diffusion2d_functions.py | 86 ++++++++ 2 files changed, 326 insertions(+) diff --git a/README.md b/README.md index da66993c..864e069f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,246 @@ Please follow the instructions in [python_testing_exercise.md](https://github.co ## Test logs (for submission) ### pytest log +(After swapping `w` for `h` in the calculation of `nx`) +``` +============================= test session starts ============================== +collecting ... collected 6 items + +integration/test_diffusion2d.py::test_initialize_physical_parameters +integration/test_diffusion2d.py::test_set_initial_condition +unit/test_diffusion2d_functions.py::test_initialize_domain PASSED [ 16%]PASSED [ 33%] +unit/test_diffusion2d_functions.py::test_initialize_domain_testcase_2 +unit/test_diffusion2d_functions.py::test_initialize_physical_parameters +unit/test_diffusion2d_functions.py::test_set_initial_condition + +========================= 2 failed, 4 passed in 0.22s ========================== +FAILED [ 50%] +unit/test_diffusion2d_functions.py:9 (test_initialize_domain) +5 != 10 + +Expected :10 +Actual :5 + + +def test_initialize_domain(): + """ + Check function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange test values + w = 20. + h = 10. + dx = 2. + dy = 0.2 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values +> assert solver.nx == 10 +E assert 5 == 10 +E + where 5 = .nx + +unit/test_diffusion2d_functions.py:31: AssertionError +FAILED [ 66%] +unit/test_diffusion2d_functions.py:34 (test_initialize_domain_testcase_2) +6 != 5 + +Expected :5 +Actual :6 + + +def test_initialize_domain_testcase_2(): + """ + Second testcase for function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange test values + # "unschöne" values + w = 15.2 + h = 17.1 + dx = 2.7 + dy = 0.29 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values +> assert solver.nx == 5 +E assert 6 == 5 +E + where 6 = .nx + +unit/test_diffusion2d_functions.py:57: AssertionError +PASSED [ 83%]dt = 0.0011428571428571432 +PASSED [100%] +Process finished with exit code 1 +``` + +(After changing the formula for `dt`: `dx2, dy2 = self.dx * self.dy, self.dx * self.dy`) +``` +============================= test session starts ============================== +collecting ... collected 6 items + +integration/test_diffusion2d.py::test_initialize_physical_parameters PASSED [ 16%] +integration/test_diffusion2d.py::test_set_initial_condition PASSED [ 33%] +unit/test_diffusion2d_functions.py::test_initialize_domain PASSED [ 50%] +unit/test_diffusion2d_functions.py::test_initialize_domain_testcase_2 PASSED [ 66%] +unit/test_diffusion2d_functions.py::test_initialize_physical_parameters FAILED [ 83%]dt = 0.001428571428571429 + +unit/test_diffusion2d_functions.py:60 (test_initialize_physical_parameters) +0.001428571428571429 != 0.001142 ± 1.0e-06 + +Expected :0.001142 ± 1.0e-06 +Actual :0.001428571428571429 + + +def test_initialize_physical_parameters(): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange + d = 3.5 + T_cold = 342.4 + T_hot = 723.15 + solver.dx = 0.1 + solver.dy = 0.2 + + # Act + solver.initialize_physical_parameters(d, T_cold, T_hot) + + # Assert + assert solver.D == d + assert solver.T_cold == T_cold + assert solver.T_hot == T_hot +> assert solver.dt == pytest.approx(0.001142, abs=1e-6) +E assert 0.001428571428571429 == 0.001142 ± 1.0e-06 +E +E comparison failed +E Obtained: 0.001428571428571429 +E Expected: 0.001142 ± 1.0e-06 + +unit/test_diffusion2d_functions.py:80: AssertionError + +unit/test_diffusion2d_functions.py::test_set_initial_condition PASSED [100%] + +========================= 1 failed, 5 passed in 0.22s ========================== + +Process finished with exit code 1 +``` + +(After introducing an error into `set_initial_condition` (wrong shape): `u = self.T_cold * np.ones((self.nx, self.nx))`) +``` +============================= test session starts ============================== +collecting ... collected 6 items + +integration/test_diffusion2d.py::test_initialize_physical_parameters +integration/test_diffusion2d.py::test_set_initial_condition +unit/test_diffusion2d_functions.py::test_initialize_domain PASSED [ 16%]PASSED [ 33%] +unit/test_diffusion2d_functions.py::test_initialize_domain_testcase_2 +unit/test_diffusion2d_functions.py::test_initialize_physical_parameters +unit/test_diffusion2d_functions.py::test_set_initial_condition + +========================= 1 failed, 5 passed in 0.22s ========================== +PASSED [ 50%]PASSED [ 66%]PASSED [ 83%]dt = 0.0007142857142857145 +FAILED [100%] +unit/test_diffusion2d_functions.py:82 (test_set_initial_condition) +100 != 50 + +Expected :50 +Actual :100 + + +def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + # Arrange + solver.nx = 100 + solver.ny = 50 + solver.dx = 0.1 + solver.dy = 0.2 + solver.T_cold = 100. + solver.T_hot = 600. + + # Act + u: np.ndarray = solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + assert u.shape[0] == solver.nx +> assert u.shape[1] == solver.ny +E assert 100 == 50 +E + where 50 = .ny + +unit/test_diffusion2d_functions.py:102: AssertionError + +Process finished with exit code 1 +``` + +(After introducing an error into `set_initial_condition` (inverse values): `if not p2 < r2:`) +``` +============================= test session starts ============================== +collecting ... collected 6 items + +integration/test_diffusion2d.py::test_initialize_physical_parameters +integration/test_diffusion2d.py::test_set_initial_condition +unit/test_diffusion2d_functions.py::test_initialize_domain PASSED [ 16%]PASSED [ 33%] +unit/test_diffusion2d_functions.py::test_initialize_domain_testcase_2 +unit/test_diffusion2d_functions.py::test_initialize_physical_parameters +unit/test_diffusion2d_functions.py::test_set_initial_condition + +========================= 1 failed, 5 passed in 0.21s ========================== +PASSED [ 50%]PASSED [ 66%]PASSED [ 83%]dt = 0.0007142857142857145 +FAILED [100%] +unit/test_diffusion2d_functions.py:82 (test_set_initial_condition) +np.float64(600.0) != 100.0 + +Expected :100.0 +Actual :np.float64(600.0) + + +def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + # Arrange + solver.nx = 100 + solver.ny = 50 + solver.dx = 0.1 + solver.dy = 0.2 + solver.T_cold = 100. + solver.T_hot = 600. + + # Act + u: np.ndarray = solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + assert u.shape[0] == solver.nx + assert u.shape[1] == solver.ny + # Border is cold, center is hot +> assert u[0][0] == 100. +E assert np.float64(600.0) == 100.0 + +unit/test_diffusion2d_functions.py:104: AssertionError + +Process finished with exit code 1 +``` ### unittest log diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index c4277ffd..3855e1fd 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -1,6 +1,8 @@ """ Tests for functions in class SolveDiffusion2D """ +import numpy as np +import pytest from diffusion2d import SolveDiffusion2D @@ -10,6 +12,50 @@ def test_initialize_domain(): Check function SolveDiffusion2D.initialize_domain """ solver = SolveDiffusion2D() + # Arrange test values + w = 20. + h = 10. + dx = 2. + dy = 0.2 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values + assert solver.nx == 10 + assert solver.ny == 50 + + +def test_initialize_domain_testcase_2(): + """ + Second testcase for function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange test values + # "unschöne" values + w = 15.2 + h = 17.1 + dx = 2.7 + dy = 0.29 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values + assert solver.nx == 5 + assert solver.ny == 58 def test_initialize_physical_parameters(): @@ -17,6 +63,21 @@ def test_initialize_physical_parameters(): Checks function SolveDiffusion2D.initialize_domain """ solver = SolveDiffusion2D() + # Arrange + d = 3.5 + T_cold = 342.4 + T_hot = 723.15 + solver.dx = 0.1 + solver.dy = 0.2 + + # Act + solver.initialize_physical_parameters(d, T_cold, T_hot) + + # Assert + assert solver.D == d + assert solver.T_cold == T_cold + assert solver.T_hot == T_hot + assert solver.dt == pytest.approx(0.001142, abs=1e-6) def test_set_initial_condition(): @@ -24,3 +85,28 @@ def test_set_initial_condition(): Checks function SolveDiffusion2D.get_initial_function """ solver = SolveDiffusion2D() + # Arrange + solver.nx = 100 + solver.ny = 50 + solver.dx = 0.1 + solver.dy = 0.2 + solver.T_cold = 100. + solver.T_hot = 600. + + # Act + u: np.ndarray = solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + assert u.shape[0] == solver.nx + assert u.shape[1] == solver.ny + # Border is cold, center is hot + assert u[0][0] == 100. + assert u[99][0] == 100. + assert u[0][49] == 100. + assert u[99][49] == 100. + + assert u[49][24] == 600. + assert u[50][24] == 600. + assert u[49][25] == 600. + assert u[50][25] == 600. From b5e4701ac89b3e27b8c394c2d9051f52c0a22072 Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 16:55:24 +0100 Subject: [PATCH 3/6] Step 4: unittest Unit-Tests --- README.md | 174 ++++++++++++++ .../pytest_test_diffusion2d_functions.py.bak | 112 +++++++++ tests/unit/test_diffusion2d_functions.py | 212 +++++++++--------- 3 files changed, 394 insertions(+), 104 deletions(-) create mode 100644 tests/unit/pytest_test_diffusion2d_functions.py.bak diff --git a/README.md b/README.md index 864e069f..ce6c7ce0 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,180 @@ Process finished with exit code 1 ``` ### unittest log +(After swapping `w` for `h` in the calculation of `nx`) +``` +============================= test session starts ============================== +collecting ... collected 4 items + +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain FAILED [ 25%] +test_diffusion2d_functions.py:16 (TestDiffusion2D.test_initialize_domain) +10 != 5 + +Expected :5 +Actual :10 + + +self = + + def test_initialize_domain(self): + """ + Check function SolveDiffusion2D.initialize_domain + """ + # Arrange test values + w = 20. + h = 10. + dx = 2. + dy = 0.2 + + # Act (= Perform test action) + self.solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + self.assertEqual(self.solver.w, w) + self.assertEqual(self.solver.h, h) + self.assertEqual(self.solver.dx, dx) + self.assertEqual(self.solver.dy, dy) + # Calculated values +> self.assertEqual(self.solver.nx, 10) + +test_diffusion2d_functions.py:37: AssertionError + +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain_testcase_2 FAILED [ 50%] +test_diffusion2d_functions.py:39 (TestDiffusion2D.test_initialize_domain_testcase_2) +5 != 6 + +Expected :6 +Actual :5 + + +self = + + def test_initialize_domain_testcase_2(self): + """ + Second testcase for function SolveDiffusion2D.initialize_domain + """ + # Arrange test values + # "unschöne" values + w = 15.2 + h = 17.1 + dx = 2.7 + dy = 0.29 + + # Act (= Perform test action) + self.solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + self.assertEqual(self.solver.w, w) + self.assertEqual(self.solver.h, h) + self.assertEqual(self.solver.dx, dx) + self.assertEqual(self.solver.dy, dy) + # Calculated values +> self.assertEqual(self.solver.nx, 5) + +test_diffusion2d_functions.py:61: AssertionError + +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_physical_parameters PASSED [ 75%]dt = 0.0011428571428571432 + +test_diffusion2d_functions.py::TestDiffusion2D::test_set_initial_condition PASSED [100%] + +========================= 2 failed, 2 passed in 0.21s ========================== + +Process finished with exit code 1 +``` + +(After changing the formula for `dt`: `dx2, dy2 = self.dx * self.dy, self.dx * self.dy`) +``` +============================= test session starts ============================== +collecting ... collected 4 items + +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain PASSED [ 25%] +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain_testcase_2 PASSED [ 50%] +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_physical_parameters FAILED [ 75%]dt = 0.001428571428571429 + +test_diffusion2d_functions.py:63 (TestDiffusion2D.test_initialize_physical_parameters) +self = + + def test_initialize_physical_parameters(self): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + # Arrange + d = 3.5 + T_cold = 342.4 + T_hot = 723.15 + self.solver.dx = 0.1 + self.solver.dy = 0.2 + + # Act + self.solver.initialize_physical_parameters(d, T_cold, T_hot) + + # Assert + self.assertEqual(self.solver.D, d) + self.assertEqual(self.solver.T_cold, T_cold) + self.assertEqual(self.solver.T_hot, T_hot) + # unittest assumes rounding here, so the last decimal differs from pytest. +> self.assertAlmostEqual(self.solver.dt, 0.001143, 6) +E AssertionError: 0.001428571428571429 != 0.001143 within 6 places (0.0002855714285714291 difference) + +test_diffusion2d_functions.py:83: AssertionError + +test_diffusion2d_functions.py::TestDiffusion2D::test_set_initial_condition PASSED [100%] + +========================= 1 failed, 3 passed in 0.22s ========================== + +Process finished with exit code 1 +``` + +(After introducing an error into `set_initial_condition` (inverse values): `if not p2 < r2:`) +``` +============================= test session starts ============================== +collecting ... collected 4 items + +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_domain_testcase_2 +test_diffusion2d_functions.py::TestDiffusion2D::test_initialize_physical_parameters +test_diffusion2d_functions.py::TestDiffusion2D::test_set_initial_condition + +========================= 1 failed, 3 passed in 0.22s ========================== +PASSED [ 25%]PASSED [ 50%]PASSED [ 75%]dt = 0.0011428571428571432 +FAILED [100%] +test_diffusion2d_functions.py:84 (TestDiffusion2D.test_set_initial_condition) +100.0 != np.float64(600.0) + +Expected :np.float64(600.0) +Actual :100.0 + + +self = + + def test_set_initial_condition(self): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + # Arrange + self.solver.nx = 100 + self.solver.ny = 50 + self.solver.dx = 0.1 + self.solver.dy = 0.2 + self.solver.T_cold = 100. + self.solver.T_hot = 600. + + # Act + u: np.ndarray = self.solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + self.assertEqual(u.shape[0], self.solver.nx) + self.assertEqual(u.shape[1], self.solver.ny) + # Border is cold, center is hot +> self.assertEqual(u[0][0], 100.) + +test_diffusion2d_functions.py:105: AssertionError + +Process finished with exit code 1 +``` ## Citing diff --git a/tests/unit/pytest_test_diffusion2d_functions.py.bak b/tests/unit/pytest_test_diffusion2d_functions.py.bak new file mode 100644 index 00000000..3855e1fd --- /dev/null +++ b/tests/unit/pytest_test_diffusion2d_functions.py.bak @@ -0,0 +1,112 @@ +""" +Tests for functions in class SolveDiffusion2D +""" +import numpy as np +import pytest + +from diffusion2d import SolveDiffusion2D + + +def test_initialize_domain(): + """ + Check function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange test values + w = 20. + h = 10. + dx = 2. + dy = 0.2 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values + assert solver.nx == 10 + assert solver.ny == 50 + + +def test_initialize_domain_testcase_2(): + """ + Second testcase for function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange test values + # "unschöne" values + w = 15.2 + h = 17.1 + dx = 2.7 + dy = 0.29 + + # Act (= Perform test action) + solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + assert solver.w == w + assert solver.h == h + assert solver.dx == dx + assert solver.dy == dy + # Calculated values + assert solver.nx == 5 + assert solver.ny == 58 + + +def test_initialize_physical_parameters(): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + # Arrange + d = 3.5 + T_cold = 342.4 + T_hot = 723.15 + solver.dx = 0.1 + solver.dy = 0.2 + + # Act + solver.initialize_physical_parameters(d, T_cold, T_hot) + + # Assert + assert solver.D == d + assert solver.T_cold == T_cold + assert solver.T_hot == T_hot + assert solver.dt == pytest.approx(0.001142, abs=1e-6) + + +def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + # Arrange + solver.nx = 100 + solver.ny = 50 + solver.dx = 0.1 + solver.dy = 0.2 + solver.T_cold = 100. + solver.T_hot = 600. + + # Act + u: np.ndarray = solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + assert u.shape[0] == solver.nx + assert u.shape[1] == solver.ny + # Border is cold, center is hot + assert u[0][0] == 100. + assert u[99][0] == 100. + assert u[0][49] == 100. + assert u[99][49] == 100. + + assert u[49][24] == 600. + assert u[50][24] == 600. + assert u[49][25] == 600. + assert u[50][25] == 600. diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index 3855e1fd..87251d76 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -1,112 +1,116 @@ """ Tests for functions in class SolveDiffusion2D """ +from unittest import TestCase + import numpy as np -import pytest +import unittest from diffusion2d import SolveDiffusion2D -def test_initialize_domain(): - """ - Check function SolveDiffusion2D.initialize_domain - """ - solver = SolveDiffusion2D() - # Arrange test values - w = 20. - h = 10. - dx = 2. - dy = 0.2 - - # Act (= Perform test action) - solver.initialize_domain(w, h, dx, dy) - - # Assert - # "cheap" assertions - assert solver.w == w - assert solver.h == h - assert solver.dx == dx - assert solver.dy == dy - # Calculated values - assert solver.nx == 10 - assert solver.ny == 50 - - -def test_initialize_domain_testcase_2(): - """ - Second testcase for function SolveDiffusion2D.initialize_domain - """ - solver = SolveDiffusion2D() - # Arrange test values - # "unschöne" values - w = 15.2 - h = 17.1 - dx = 2.7 - dy = 0.29 - - # Act (= Perform test action) - solver.initialize_domain(w, h, dx, dy) - - # Assert - # "cheap" assertions - assert solver.w == w - assert solver.h == h - assert solver.dx == dx - assert solver.dy == dy - # Calculated values - assert solver.nx == 5 - assert solver.ny == 58 - - -def test_initialize_physical_parameters(): - """ - Checks function SolveDiffusion2D.initialize_domain - """ - solver = SolveDiffusion2D() - # Arrange - d = 3.5 - T_cold = 342.4 - T_hot = 723.15 - solver.dx = 0.1 - solver.dy = 0.2 - - # Act - solver.initialize_physical_parameters(d, T_cold, T_hot) - - # Assert - assert solver.D == d - assert solver.T_cold == T_cold - assert solver.T_hot == T_hot - assert solver.dt == pytest.approx(0.001142, abs=1e-6) - - -def test_set_initial_condition(): - """ - Checks function SolveDiffusion2D.get_initial_function - """ - solver = SolveDiffusion2D() - # Arrange - solver.nx = 100 - solver.ny = 50 - solver.dx = 0.1 - solver.dy = 0.2 - solver.T_cold = 100. - solver.T_hot = 600. - - # Act - u: np.ndarray = solver.set_initial_condition() - - # Assert - # Domain has expected dimensions - assert u.shape[0] == solver.nx - assert u.shape[1] == solver.ny - # Border is cold, center is hot - assert u[0][0] == 100. - assert u[99][0] == 100. - assert u[0][49] == 100. - assert u[99][49] == 100. - - assert u[49][24] == 600. - assert u[50][24] == 600. - assert u[49][25] == 600. - assert u[50][25] == 600. +class TestDiffusion2D(unittest.TestCase): + + def setUp(self): + self.solver = SolveDiffusion2D() + + def test_initialize_domain(self): + """ + Check function SolveDiffusion2D.initialize_domain + """ + # Arrange test values + w = 20. + h = 10. + dx = 2. + dy = 0.2 + + # Act (= Perform test action) + self.solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + self.assertEqual(self.solver.w, w) + self.assertEqual(self.solver.h, h) + self.assertEqual(self.solver.dx, dx) + self.assertEqual(self.solver.dy, dy) + # Calculated values + self.assertEqual(self.solver.nx, 10) + self.assertEqual(self.solver.ny, 50) + + def test_initialize_domain_testcase_2(self): + """ + Second testcase for function SolveDiffusion2D.initialize_domain + """ + # Arrange test values + # "unschöne" values + w = 15.2 + h = 17.1 + dx = 2.7 + dy = 0.29 + + # Act (= Perform test action) + self.solver.initialize_domain(w, h, dx, dy) + + # Assert + # "cheap" assertions + self.assertEqual(self.solver.w, w) + self.assertEqual(self.solver.h, h) + self.assertEqual(self.solver.dx, dx) + self.assertEqual(self.solver.dy, dy) + # Calculated values + self.assertEqual(self.solver.nx, 5) + self.assertEqual(self.solver.ny, 58) + + def test_initialize_physical_parameters(self): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + # Arrange + d = 3.5 + T_cold = 342.4 + T_hot = 723.15 + self.solver.dx = 0.1 + self.solver.dy = 0.2 + + # Act + self.solver.initialize_physical_parameters(d, T_cold, T_hot) + + # Assert + self.assertEqual(self.solver.D, d) + self.assertEqual(self.solver.T_cold, T_cold) + self.assertEqual(self.solver.T_hot, T_hot) + # unittest assumes rounding here, so the last decimal differs from pytest. + self.assertAlmostEqual(self.solver.dt, 0.001143, 6) + + def test_set_initial_condition(self): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + # Arrange + self.solver.nx = 100 + self.solver.ny = 50 + self.solver.dx = 0.1 + self.solver.dy = 0.2 + self.solver.T_cold = 100. + self.solver.T_hot = 600. + + # Act + u: np.ndarray = self.solver.set_initial_condition() + + # Assert + # Domain has expected dimensions + self.assertEqual(u.shape[0], self.solver.nx) + self.assertEqual(u.shape[1], self.solver.ny) + # Border is cold, center is hot + self.assertEqual(u[0][0], 100.) + self.assertEqual(u[99][0], 100.) + self.assertEqual(u[0][49], 100.) + self.assertEqual(u[99][49], 100.) + + self.assertEqual(u[49][24], 600.) + self.assertEqual(u[50][24], 600.) + self.assertEqual(u[49][25], 600.) + self.assertEqual(u[50][25], 600.) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 90e0893fb6c7284df478814e852f26a6a2b1b047 Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 17:12:59 +0100 Subject: [PATCH 4/6] Step 5: pytest Integration-Tests --- README.md | 91 +++++++++++++++++++++++++++ tests/integration/test_diffusion2d.py | 27 ++++++++ 2 files changed, 118 insertions(+) diff --git a/README.md b/README.md index ce6c7ce0..56eec4c7 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,97 @@ test_diffusion2d_functions.py:105: AssertionError Process finished with exit code 1 ``` +### pytest-log for Integration-Tests + +(breaking `test_initialize_physical_parameters` by taking `dx2*dx2`) +``` +============================= test session starts ============================== +collecting ... collected 1 item + +test_diffusion2d.py::test_initialize_physical_parameters FAILED [100%]dt = 0.006666666666666669 + +test_diffusion2d.py:8 (test_initialize_physical_parameters) +0.006666666666666669 != 0.001666 ± 1.0e-06 + +Expected :0.001666 ± 1.0e-06 +Actual :0.006666666666666669 + + +def test_initialize_physical_parameters(): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + solver = SolveDiffusion2D() + solver.initialize_domain(10., 20., 0.2, 0.1) + solver.initialize_physical_parameters(2.4, 0., 1.) + # dx2 = 0.04 + # dy2 = 0.01 + # dt = 0.0004 / (2*2.4*0.05) = 1/600 = 0.0016666 +> assert solver.dt == pytest.approx(0.001666, abs=1e-6) +E assert 0.006666666666666669 == 0.001666 ± 1.0e-06 +E +E comparison failed +E Obtained: 0.006666666666666669 +E Expected: 0.001666 ± 1.0e-06 + +test_diffusion2d.py:19: AssertionError + + +============================== 1 failed in 0.21s =============================== + +Process finished with exit code 1 +``` + +(breaking `test_set_initial_condition` by cubing instead of squaring) +``` +============================= test session starts ============================== +collecting ... collected 1 item + +test_diffusion2d.py::test_set_initial_condition FAILED [100%]dt = 0.09259259259259259 + +test_diffusion2d.py:21 (test_set_initial_condition) +0.0 != np.float64(1.0) + +Expected :np.float64(1.0) +Actual :0.0 + + +def test_set_initial_condition(): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver = SolveDiffusion2D() + solver.initialize_domain(10., 10., 1., 1.) + solver.initialize_physical_parameters(2.7, 0., 1.) + u = solver.set_initial_condition() + + expected_array= [ + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + ] + + for x in range(u.shape[0]): + for y in range(u.shape[1]): +> assert expected_array[x][y] == u[x, y], f"Values at {x}, {y} do not match" +E AssertionError: Values at 0, 0 do not match +E assert 0.0 == np.float64(1.0) + +test_diffusion2d.py:46: AssertionError + + +============================== 1 failed in 0.21s =============================== + +Process finished with exit code 1 +``` + ## Citing The code used in this exercise is based on [Chapter 7 of the book "Learning Scientific Programming with Python"](https://scipython.com/book/chapter-7-matplotlib/examples/the-two-dimensional-diffusion-equation/). diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index fd026b40..a5bd1174 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -1,6 +1,7 @@ """ Tests for functionality checks in class SolveDiffusion2D """ +import pytest from diffusion2d import SolveDiffusion2D @@ -10,6 +11,12 @@ def test_initialize_physical_parameters(): Checks function SolveDiffusion2D.initialize_domain """ solver = SolveDiffusion2D() + solver.initialize_domain(10., 20., 0.2, 0.1) + solver.initialize_physical_parameters(2.4, 0., 1.) + # dx2 = 0.04 + # dy2 = 0.01 + # dt = 0.0004 / (2*2.4*0.05) = 1/600 = 0.0016666 + assert solver.dt == pytest.approx(0.001666, abs=1e-6) def test_set_initial_condition(): @@ -17,3 +24,23 @@ def test_set_initial_condition(): Checks function SolveDiffusion2D.get_initial_function """ solver = SolveDiffusion2D() + solver.initialize_domain(10., 10., 1., 1.) + solver.initialize_physical_parameters(2.7, 0., 1.) + u = solver.set_initial_condition() + + expected_array= [ + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 1., 1., 1., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + ] + + for x in range(u.shape[0]): + for y in range(u.shape[1]): + assert expected_array[x][y] == u[x, y], f"Values at {x}, {y} do not match" From a1b39a741ddba8c537ac0d98345e00defb9ad422 Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 17:16:58 +0100 Subject: [PATCH 5/6] Step 6: Measure coverage --- coverage-report.pdf | Bin 0 -> 37464 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 coverage-report.pdf diff --git a/coverage-report.pdf b/coverage-report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..56e2b88509e0529fa3c185f8cc9f14919a6eba7b GIT binary patch literal 37464 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 literal 0 HcmV?d00001 From 8b4ee2e7fcc5b0c9196d6bbccaf3ad73b01e27f1 Mon Sep 17 00:00:00 2001 From: Kilian Krampf Date: Wed, 15 Jan 2025 23:32:32 +0100 Subject: [PATCH 6/6] Step 7: Add tox.toml --- pyproject.toml | 5 +++++ requirements.txt | 2 ++ tox.toml | 12 ++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 tox.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0a9352f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +# I had to add this file so pytest would actually find diffusion2d.py +[ pyproject.toml] + +[tool.pytest.ini_options] +pythonpath = "." diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ac3f2889 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +numpy>=1.15.0 +matplotlib>=3.0.0 \ No newline at end of file diff --git a/tox.toml b/tox.toml new file mode 100644 index 00000000..874a35bd --- /dev/null +++ b/tox.toml @@ -0,0 +1,12 @@ +requires = ["tox>=4"] +env_list = ["pytest", "unittest"] + +[env.pytest] +description = "Run pytest" +deps = ["pytest>=8", "-r requirements.txt"] +commands = [["pytest", "tests/integration/test_diffusion2d.py"]] + +[env.unittest] +description = "Run unittest" +deps = ["-r requirements.txt"] +commands = [["python3", "-m", "unittest", "tests/unit/test_diffusion2d_functions.py"]] \ No newline at end of file