diff --git a/doc/guide/Unit-Tests-Workshop.ipynb b/doc/guide/Unit-Tests-Workshop.ipynb new file mode 100644 index 0000000000..aad1ba8d64 --- /dev/null +++ b/doc/guide/Unit-Tests-Workshop.ipynb @@ -0,0 +1,2183 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "a57f241f", + "metadata": {}, + "source": [ + "# Testing in CLIMADA " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d8631efc-5e55-4bd6-ac6b-32834495bb52", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## set up notebook" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cbdeb70d", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import climada\n", + "\n", + "logging.getLogger(\"climada\").setLevel(\"WARNING\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a305d3be", + "metadata": {}, + "source": [ + "This code here is just for this workshop so that the tests run in a notebook. No need to understand the details" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8276831c-0cc2-441f-96f2-fadb8345bf2c", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-13T12:45:27.619499Z", + "start_time": "2023-06-13T12:45:27.603169Z" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import unittest\n", + "import warnings\n", + "\n", + "def run_test(test, filterwarnings=None):\n", + "\n", + " tests = unittest.defaultTestLoader.loadTestsFromName(name=f\"{test.__module__}.{test.__qualname__}\")\n", + " with warnings.catch_warnings():\n", + " runner = unittest.TextTestRunner()\n", + " filterwarnings and warnings.simplefilter(filterwarnings)\n", + " result = runner.run(tests)\n", + "\n", + " return result" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ef4014a1", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Execute tests " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d93bf26b", + "metadata": {}, + "source": [ + "Who does not know how too?" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "65863621-e866-4b49-b902-97695a5c163b", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-06-13 18:16:49,504 - climada.hazard.base - INFO - Reading C:\\Users\\me\\climada\\data\\test\\test_tc_florida\\v1\\test_tc_florida.hdf5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.004s\n", + "\n", + "OK\n" + ] + } + ], + "source": [ + "# spoiler\n", + "! python -m unittest climada.engine.test.test_impact.TestImpact.test_pyproj_crs" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6953053d", + "metadata": { + "tags": [] + }, + "source": [ + "# Writing Good Better Unit/Integration Tests" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "48a6587f", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Test species for CLIMADA " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "10e51fc7", + "metadata": {}, + "source": [ + "- Integration tests: located `climada_python/climada/test/` or `climada_petals/climada/test/`\n", + "- Unit tests: all other module tests" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e5538acd", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Unit Test" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ae73c70a-47fd-422e-98d4-b69848ed1587", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "_not to be confused with_: `unittest` the standard test package from Python\n", + "\n", + "Tests whether a single function or method is doing just what it is supposed to do.\n", + "\n", + "It should be possible to know the correct result of the test by calculating with a pen and paper, or ideally just looking at the test code alone.\\\n", + "Do not use real datasets.\\\n", + "Do not use large datasets.\\\n", + "Do in general not use data files. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1de24a9c", + "metadata": {}, + "source": [ + "Try to test some simple edge cases (e.g., negative numbers, zeroes, extra attributes,...). \\\n", + "Try not to explicitly test implicit values (e.g. size of array if array values are explictly checked)." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e90e071b", + "metadata": {}, + "source": [ + "Do NOT write a test that proves only itself. I.e., if it fails in the future, one should be able to tell whether the new result is correct, or the old one. " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e2597437-bc7a-4143-988e-5791b22ce945", + "metadata": {}, + "source": [ + "#### Unit test file location in climada:\n", + "\n", + "climada/**/test/" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3abb27bf", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Example\n", + "\n", + "Test: [TestImpactCalc.test_apply_cover_to_mat](https://github.com/CLIMADA-project/climada_python/blob/main/climada/engine/test/test_impact_calc.py#L92)\n", + "\n", + "Method under test: [ImpactCalc.apply_cover_to_mat](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/engine/impact_calc.py#L451)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7151c421", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-13T12:45:35.748130Z", + "start_time": "2023-06-13T12:45:28.855590Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.002s\n", + "\n", + "OK\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from unittest import TestCase\n", + "\n", + "import numpy as np\n", + "from scipy import sparse\n", + "\n", + "from climada.engine import ImpactCalc\n", + "\n", + "class TestImpactCalc(TestCase):\n", + " def test_apply_cover_to_mat(self):\n", + " \"\"\"Test methods to get insured metrics\"\"\"\n", + " mat = sparse.csr_matrix(np.array(\n", + " [[1, 0, 1],\n", + " [2, 2, 0]]\n", + " ))\n", + " cover = np.array([0, 1, 10])\n", + " imp = ImpactCalc.apply_cover_to_mat(mat, cover)\n", + " np.testing.assert_array_equal(\n", + " imp.todense(), np.array([[0, 0, 1], [0, 1, 0]])\n", + " )\n", + "\n", + "run_test(TestImpactCalc.test_apply_cover_to_mat)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7abd8b5b-9510-4ea4-99b2-582fce5f2b59", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "### Warm Up Exercise\n", + "\n", + "room for improvement?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf3e0e57-823f-4a95-9f02-bccbd8cb52c9", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-13T12:45:51.231227Z", + "start_time": "2023-06-13T12:45:51.220408Z" + }, + "collapsed": true, + "jupyter": { + "outputs_hidden": true, + "source_hidden": true + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.002s\n", + "\n", + "OK\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Spoiler\n", + "class TestImpactCalc(TestCase):\n", + " def test_apply_cover_to_mat(self):\n", + " \"\"\"Test methods to get insured metrics\"\"\"\n", + " mat = sparse.csr_matrix(np.array(\n", + " [[1, -1, 1], # 1) before only clipping \"from above\" was tested\n", + " [2, 2, 0]]\n", + " ))\n", + " cover = np.array([0, 1, 10])\n", + " imp = ImpactCalc.apply_cover_to_mat(mat, cover)\n", + " self.assertEqual(imp.nnz, 2) # check for zero elimination \n", + " np.testing.assert_array_equal(\n", + " imp.todense(), np.array([[0, 0, 1], [0, 1, 0]])\n", + " )\n", + "\n", + "run_test(TestImpactCalc.test_apply_cover_to_mat)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "da27af33", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Integration Test " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6de74771", + "metadata": {}, + "source": [ + "Test that integrate different elements of the CLIMADA code (e.g., an impact computation with an exposure, hazard and impact function). There are two kinds:\n", + "\n", + "- Reference test: using real data, can be larger data sets, can take longer to test, and mostly tests that the code does not fail.\\\n", + " It checks that the result does not change over time, but not whether it is in itself correct (aka regression test).\n", + "- True integration test: looks like a unit test, but integrates different modules." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7713ab7c-c48e-43cf-93ee-03646231d7ae", + "metadata": {}, + "source": [ + "Integration tests are often used for\n", + "\n", + "- I/O test for real data: try reading and writing real data sets.\n", + "- Plotting: mostly tests that plotting passes, but not that it is correct" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c4290ca6-72b7-4c4a-a331-8d796d87efc6", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Ingegration test file location in climada:\n", + "\n", + "climada/test/" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2dbf8b8a-31f6-4f1d-9a26-5714cdcf7585", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Example\n", + "\n", + "Test: [test_cost_benefit.TestCalc.test_calc_change_pass](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/engine/test/test_cost_benefit.py#L720)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "db22ebf9-be54-42a5-b895-7e283f71a488", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 1.232s\n", + "\n", + "OK\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Measure Cost (USD bn) Benefit (USD bn) Benefit/Cost\n", + "----------------- --------------- ------------------ --------------\n", + "Mangroves 1.31177 113.345 86.4063\n", + "Beach nourishment 1.728 89.4449 51.7621\n", + "Seawall 8.87878 347.977 39.192\n", + "Building code 9.2 144.216 15.6757\n", + "\n", + "-------------------- --------- --------\n", + "Total climate risk: 576.866 (USD bn)\n", + "Average annual risk: 59.5067 (USD bn)\n", + "Residual risk: -118.118 (USD bn)\n", + "-------------------- --------- --------\n", + "Net Present Values\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import copy\n", + "\n", + "from climada.hazard import Hazard\n", + "from climada.entity import Entity\n", + "from climada.engine import CostBenefit, risk_aai_agg\n", + "from climada.test import get_test_file\n", + "from climada.util.constants import ENT_DEMO_FUTURE, ENT_DEMO_TODAY\n", + "\n", + "HAZ_TEST_MAT = get_test_file('atl_prob_no_name')\n", + "ENT_TEST_MAT = get_test_file('demo_today', file_format='MAT-file')\n", + "\n", + "class TestCalc(unittest.TestCase):\n", + " \"\"\"Test calc\"\"\"\n", + "\n", + " def test_calc_change_pass(self):\n", + " \"\"\"Test calc with future change\"\"\"\n", + " # present\n", + " hazard = Hazard.from_mat(HAZ_TEST_MAT)\n", + " entity = Entity.from_excel(ENT_DEMO_TODAY)\n", + " entity.exposures.gdf.rename(columns={'impf_': 'impf_TC'}, inplace=True)\n", + " entity.check()\n", + " entity.exposures.ref_year = 2018\n", + "\n", + " # future\n", + " ent_future = Entity.from_excel(ENT_DEMO_FUTURE)\n", + " ent_future.check()\n", + " ent_future.exposures.ref_year = 2040\n", + "\n", + " haz_future = copy.deepcopy(hazard)\n", + " haz_future.intensity.data += 25\n", + "\n", + " cost_ben = CostBenefit()\n", + " cost_ben.calc(hazard, entity, haz_future, ent_future)\n", + "\n", + " self.assertEqual(cost_ben.present_year, 2018)\n", + " self.assertEqual(cost_ben.future_year, 2040)\n", + " self.assertAlmostEqual(cost_ben.tot_climate_risk, 5.768659152882021e+11, places=3)\n", + "\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present['no measure']['risk'],\n", + " 6.51220115756442e+09, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present['Mangroves']['risk'],\n", + " 4.850407096284983e+09, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present['Beach nourishment']['risk'],\n", + " 5.188921355413834e+09, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present['Seawall']['risk'],\n", + " 4.736400526119911e+09, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present['Building code']['risk'],\n", + " 4.884150868173321e+09, places=3)\n", + "\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future['no measure']['risk'],\n", + " 5.9506659786664024e+10, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future['Mangroves']['risk'],\n", + " 4.826231151473135e+10, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future['Beach nourishment']['risk'],\n", + " 5.0647250923231674e+10, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future['Seawall']['risk'],\n", + " 21089567135.7345, places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future['Building code']['risk'],\n", + " 4.462999483999791e+10, places=3)\n", + "\n", + " self.assertAlmostEqual(cost_ben.benefit['Mangroves'], 113345027690.81276, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Beach nourishment'], 89444869971.53653, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Seawall'], 347977469896.1333, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Building code'], 144216478822.05154, places=2)\n", + "\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Mangroves'], 0.011573232523528404)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Beach nourishment'], 0.01931916274851638)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Seawall'], 0.025515385913577368)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Building code'], 0.06379298728650741)\n", + "\n", + " self.assertAlmostEqual(cost_ben.tot_climate_risk, 576865915288.2021, places=3)\n", + "\n", + "\n", + "run_test(TestCalc.test_calc_change_pass, \"ignore\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6c45e017", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "### Improvement exercise\n", + "\n", + "How could the test above be improved?" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bdd01cc0-d681-417b-8e00-b1982f808542", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true, + "source_hidden": true + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.977s\n", + "\n", + "OK\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Measure Cost (USD bn) Benefit (USD bn) Benefit/Cost\n", + "----------------- --------------- ------------------ --------------\n", + "Mangroves 1.31177 113.345 86.4063\n", + "Beach nourishment 1.728 89.4449 51.7621\n", + "Seawall 8.87878 347.977 39.192\n", + "Building code 9.2 144.216 15.6757\n", + "\n", + "-------------------- --------- --------\n", + "Total climate risk: 576.866 (USD bn)\n", + "Average annual risk: 59.5067 (USD bn)\n", + "Residual risk: -118.118 (USD bn)\n", + "-------------------- --------- --------\n", + "Net Present Values\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# spoiler\n", + "from io import StringIO\n", + "import pandas as pd\n", + "\n", + "class TestCalc(unittest.TestCase):\n", + " \"\"\"Test calc\"\"\"\n", + "\n", + " def test_calc_change_pass(self):\n", + " \"\"\"Test calc with future change\"\"\"\n", + " # present\n", + " hazard = Hazard.from_mat(HAZ_TEST_MAT)\n", + " entity = Entity.from_excel(ENT_DEMO_TODAY)\n", + " entity.exposures.gdf.rename(columns={'impf_': 'impf_TC'}, inplace=True)\n", + " entity.check()\n", + " entity.exposures.ref_year = 2018\n", + "\n", + " # future\n", + " ent_future = Entity.from_excel(ENT_DEMO_FUTURE)\n", + " ent_future.check()\n", + " ent_future.exposures.ref_year = 2040\n", + "\n", + " haz_future = copy.deepcopy(hazard)\n", + " haz_future.intensity.data += 25\n", + "\n", + " cost_ben = CostBenefit()\n", + " cost_ben.calc(hazard, entity, haz_future, ent_future)\n", + "\n", + " self.assertEqual(cost_ben.present_year, 2018)\n", + " self.assertEqual(cost_ben.future_year, 2040)\n", + " self.assertAlmostEqual(cost_ben.tot_climate_risk, 5.768659152882021e+11, places=3)\n", + " \n", + " expected = pd.read_csv(StringIO(\"\"\"\n", + " , no measure, Mangroves, Beach nourishment, Seawall, Building code\n", + " present, 6.51220115756442e+09, 4.850407096284983e+09, 5.188921355413834e+09, 4.736400526119911e+09, 4.884150868173321e+09\n", + " future, 5.9506659786664024e+10, 4.826231151473135e+10, 5.0647250923231674e+10, 21089567135.7345, 4.462999483999791e+10\n", + " cbratio, , , , ,\n", + " benefit, , , , ,\n", + " \"\"\"), skipinitialspace=True, index_col=0)\n", + " \n", + " for measure in expected.columns:\n", + " self.assertAlmostEqual(cost_ben.imp_meas_present[measure]['risk'],\n", + " expected.loc['present', measure], places=3)\n", + " self.assertAlmostEqual(cost_ben.imp_meas_future[measure]['risk'],\n", + " expected.loc['future', measure], places=3)\n", + " \n", + " self.assertAlmostEqual(cost_ben.benefit['Mangroves'], 113345027690.81276, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Beach nourishment'], 89444869971.53653, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Seawall'], 347977469896.1333, places=2)\n", + " self.assertAlmostEqual(cost_ben.benefit['Building code'], 144216478822.05154, places=2)\n", + "\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Mangroves'], 0.011573232523528404)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Beach nourishment'], 0.01931916274851638)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Seawall'], 0.025515385913577368)\n", + " self.assertAlmostEqual(cost_ben.cost_ben_ratio['Building code'], 0.06379298728650741)\n", + "\n", + " self.assertAlmostEqual(cost_ben.tot_climate_risk, 576865915288.2021, places=3)\n", + "\n", + "\n", + "run_test(TestCalc.test_calc_change_pass, \"ignore\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "837d52e1", + "metadata": { + "heading_collapsed": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Work with Data" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "42cb626a-421d-428d-a6d2-8d78acaf5640", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Test Files (I/O)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "12873905-84e2-47df-9676-581863565842", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "- **read and run**\n", + " \n", + "      arrange _file_ \\\n", + "      _file_ → _obj_ → f(_obj_) \\\n", + "      assert f(_obj_) as expected\n", + " \n", + " unfortunate for unit tests: mixes I/O and calculation, separates data from code\n", + " \n", + " example: [TestImpactCalc.test_calc_impact_TC_pass](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/engine/test/test_impact_calc.py#L104)\n", + " \n", + "- **just read**\n", + "\n", + "      arrange _file_ \\\n", + "      _file_ → _obj_ \\\n", + "      assert _obj_ as expected\n", + " \n", + " classic I/O integration test: create object from file\n", + " \n", + "- **write** \n", + " \n", + "      arrange _obj_ \\\n", + "      _obj_ → _file_ \\\n", + "      assert _file_ as expected\n", + " \n", + " unfortunate 😐 files are hard to scrutinize, writing routines often depend on OS\n", + " \n", + "- **dump-reload**\n", + " \n", + "      arrange _obj_ \\\n", + "      _obj_ → _file_ → _obj_' \\\n", + "      assert _obj_ == _obj_'\n", + "\n", + " fortunate 😀 this is the preferable way of unit testing when both methods are implemented" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "3d74a530-eb45-4860-bf3c-48ccf1f7e652", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### how to use a temporary file for dump reload" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "39ca1ffe-9acd-4965-8552-95fe11da9bc0", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "package [`tempfile`](https://docs.python.org/3/library/tempfile.html)\n", + "\n", + "takes care of cleaning up afterwards\n", + "\n", + "`tempfile.TemporaryDirectory` creates a directory that is removed afterwards" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e7dc607b-8570-4ef6-9313-030c56df1f39", + "metadata": { + "ExecuteTime": { + "end_time": "2023-06-13T12:26:15.592115Z", + "start_time": "2023-06-13T12:26:15.586136Z" + }, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Proof of concept" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2b0e343a-35c0-4454-bfae-455d9bdcb29e", + "metadata": { + "hidden": true, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('C:\\\\Users\\\\me\\\\AppData\\\\Local\\\\Temp\\\\tmpksak0ip9', True)\n" + ] + }, + { + "data": { + "text/plain": [ + "('C:\\\\Users\\\\me\\\\AppData\\\\Local\\\\Temp\\\\tmpksak0ip9', False)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pathlib import Path\n", + "import tempfile\n", + "\n", + "with tempfile.TemporaryDirectory() as tempdir:\n", + " print((tempdir, Path(tempdir).is_dir()))\n", + "\n", + "tempdir, Path(tempdir).is_dir()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "78927b81-16e8-4db1-b052-3523e44e1a07", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Exercise\n", + "\n", + "use the `tempfile.TemporaryDirectory()` context manager to refactor [climada.entity.disc_rates.test.test_base.TestWriter.test_write_read_pass](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/entity/disc_rates/test/test_base.py#L270) so that it does not \"pollute\" the local file system any more." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "0544fa05-3f7e-426c-a2f3-a24e7265495a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.170s\n", + "\n", + "OK\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "\n", + "from climada.util.config import Config\n", + "from climada.entity.disc_rates import DiscRates\n", + "\n", + "CONFIG = Config.from_dict({\"disc_rates\":{\"test_data\": \"data\"}})\n", + "\n", + "\n", + "class TestWriter(unittest.TestCase):\n", + " \"\"\"Test excel reader for discount rates\"\"\"\n", + "\n", + " def test_write_read_pass(self):\n", + " \"\"\"Read demo excel file.\"\"\"\n", + " years = np.arange(1950, 2150)\n", + " rates = np.ones(years.size) * 0.03\n", + " disc_rate = DiscRates(years=years, rates=rates)\n", + "\n", + " file_name = CONFIG.disc_rates.test_data.dir().joinpath('test_disc.xlsx')\n", + " disc_rate.write_excel(file_name)\n", + "\n", + " disc_read = DiscRates.from_excel(file_name)\n", + "\n", + " self.assertTrue(np.array_equal(disc_read.years, disc_rate.years))\n", + " self.assertTrue(np.array_equal(disc_read.rates, disc_rate.rates))\n", + "\n", + " self.assertEqual(disc_read.tag.file_name, str(file_name))\n", + " self.assertEqual(disc_read.tag.description, '')\n", + "\n", + "run_test(TestWriter.test_write_read_pass)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5d4d14fa-7065-4463-928d-7a4a751a94dd", + "metadata": { + "collapsed": true, + "jupyter": { + "outputs_hidden": true, + "source_hidden": true + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 0.096s\n", + "\n", + "OK\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# spoiler\n", + "\n", + "class TestWriter(unittest.TestCase):\n", + " \"\"\"Test excel reader for discount rates\"\"\"\n", + "\n", + " def test_write_read_pass(self):\n", + " \"\"\"Read demo excel file.\"\"\"\n", + " years = np.arange(1950, 2150)\n", + " rates = np.ones(years.size) * 0.03\n", + " disc_rate = DiscRates(years=years, rates=rates)\n", + "\n", + " with tempfile.TemporaryDirectory() as tempdir:\n", + " file_name = Path(tempdir).joinpath('test_disc.xlsx')\n", + " disc_rate.write_excel(file_name)\n", + "\n", + " disc_read = DiscRates.from_excel(file_name)\n", + "\n", + " self.assertTrue(np.array_equal(disc_read.years, disc_rate.years))\n", + " self.assertTrue(np.array_equal(disc_read.rates, disc_rate.rates))\n", + "\n", + " self.assertEqual(disc_read.tag.file_name, str(file_name))\n", + " self.assertEqual(disc_read.tag.description, '')\n", + "\n", + "run_test(TestWriter.test_write_read_pass)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e8c8d170-fe79-4ec3-964a-f47d2e7ab616", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### how to use the data-api for I/O integration tests" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f1f8cf74", + "metadata": { + "hidden": true + }, + "source": [ + "Only for the integration tests!\n", + "\n", + "e.g., create objects from file" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "26757b08-2792-40bb-a141-72b1b49a7273", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Example" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f1b38e03-e4d3-48ab-8263-0a8069b4d96b", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "Test: [TestStormEurope.test_icon_read](https://github.com/CLIMADA-project/climada_python/blob/main/climada/hazard/test/test_storm_europe.py#L200)\n", + "\n", + "Method under test: [StormEurope.from_icon_grib](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/hazard/storm_europe.py#L396)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "111fbcbb-82e0-4f07-a78e-28085d767697", + "metadata": { + "hidden": true, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ecCodes provides no latitudes/longitudes for gridType='unstructured_grid'\n", + "ecCodes provides no latitudes/longitudes for gridType='unstructured_grid'\n", + ".\n", + "----------------------------------------------------------------------\n", + "Ran 1 test in 4.248s\n", + "\n", + "OK\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import datetime as dt\n", + "from unittest import TestCase\n", + "\n", + "from scipy import sparse\n", + "\n", + "from climada import CONFIG\n", + "\n", + "from climada.hazard.storm_europe import StormEurope\n", + "from climada.util.api_client import Client\n", + "\n", + "\n", + "class TestStormEurope(TestCase):\n", + "\n", + " def test_icon_read(self):\n", + " \"\"\"test reading from icon grib\"\"\"\n", + " # for this test the forecast file is supposed to be already downloaded from the dwd\n", + " # another download would fail because the files are available for 24h only\n", + " # instead, we download it as a test dataset through the climada data api\n", + " apiclient = Client()\n", + " ds = apiclient.get_dataset_info(name='test_storm_europe_icon_2021012800', status='test_dataset')\n", + " dsdir, _ = apiclient.download_dataset(ds)\n", + " haz = StormEurope.from_icon_grib(\n", + " dt.datetime(2021, 1, 28),\n", + " dt.datetime(2021, 1, 28),\n", + " model_name='test',\n", + " grib_dir=dsdir,\n", + " delete_raw_data=False)\n", + " self.assertEqual(haz.tag.haz_type, 'WS')\n", + " self.assertEqual(haz.units, 'm/s')\n", + " self.assertEqual(haz.event_id.size, 40)\n", + " self.assertEqual(haz.date.size, 40)\n", + " self.assertEqual(dt.datetime.fromordinal(haz.date[0]).year, 2021)\n", + " self.assertEqual(dt.datetime.fromordinal(haz.date[0]).month, 1)\n", + " self.assertEqual(dt.datetime.fromordinal(haz.date[0]).day, 28)\n", + " self.assertEqual(haz.event_id[-1], 40)\n", + " self.assertEqual(haz.event_name[-1], '2021-01-28_ens40')\n", + " self.assertIsInstance(haz.intensity,\n", + " sparse.csr.csr_matrix)\n", + " self.assertIsInstance(haz.fraction,\n", + " sparse.csr.csr_matrix)\n", + " self.assertEqual(haz.intensity.shape, (40, 49))\n", + " self.assertAlmostEqual(haz.intensity.max(), 17.276321, places=3)\n", + " self.assertEqual(haz.fraction.shape, (40, 49))\n", + " with self.assertLogs('climada.hazard.storm_europe', level='WARNING') as cm:\n", + " with self.assertRaises(ValueError):\n", + " haz = StormEurope.from_icon_grib(\n", + " dt.datetime(2021, 1, 28, 6),\n", + " dt.datetime(2021, 1, 28),\n", + " model_name='test',\n", + " grib_dir=CONFIG.hazard.test_data.str(),\n", + " delete_raw_data=False)\n", + " self.assertEqual(len(cm.output), 1)\n", + " self.assertIn('event definition is inaccuratly implemented', cm.output[0])\n", + "\n", + "run_test(TestStormEurope, \"ignore\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b33fa294-b203-4149-9fba-280921e210eb", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Improvement Exercise" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "339a91b3", + "metadata": {}, + "source": [ + "- any ideas how thiw test could be improved?" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "4d45d0f1", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "# spoiler\n", + "'''\n", + "- split into pass and fail\n", + "- remove relic `CONFIG.hazard.test_data`\n", + "- make the failing part a proper unit test by explicitly defining data\n", + "- ?\n", + "''';" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "863d6a3b-d7b1-49b1-82c3-f60b9a2f74a6", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "#### Overview test data files available on API" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b99e73b2-dae6-4e7f-8aa2-897009338328", + "metadata": { + "hidden": true, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
data_typedata_type_groupuuidnameversionstatusdoidescriptionlicenseactivation_date...date_creationclimada_versionspatial_coverageyear_rangeres_arcsecurlfile_namefile_formatfile_sizecheck_sum
0measuresmeasures2306fb7a-d32f-4f0b-b2f6-9ee4f0d55946test_measv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/2306fb7a-d32f...test_meas.xlsxxlsx5673md5:c1a90eede3240671b73db87dc0ed9878
1earth_engineexposures8cca2e12-b262-4d6c-acdb-501750322bf7population-density_medianv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/8cca2e12-b262...population-density_median.tiftif247646md5:337eb33d7a4fd51a2e901e32fa561973
2earth_engineexposures1d2a7850-9ee6-480f-b5a6-4aa13c770256dresdenv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/1d2a7850-9ee6...dresden.tiftif664471md5:3e33539fee034a68aa626105e7a1e80f
3baseexposures890ea94d-ecab-4b5b-a617-2098dc930bf7test_write_hazardv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/890ea94d-ecab...test_write_hazard.tiftif446md5:be78239036e62a4443eedb38bf451cc8
4impactimpactc80c9370-777b-4f63-ad7f-9852adbb9b0aWIOT2012_Nov16_ROWv1test_datasetNoneWIOT 2012, Nov 16, ROWAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/c80c9370-777b...WIOT2012_Nov16_ROW.xlsbxlsb62042190md5:71e279a6ba7d58ff1990dd4c3f9ec61a
..................................................................
118baseexposurescc078ce5-4a37-41bd-8603-2d0afaf81e60test_point_expv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/cc078ce5-4a37...test_point_exp.hdf5hdf51064184md5:cd907ac937bdd8a4759d80c0a0aa643b
119baseexposuresc1f27aca-728b-4891-99f8-740777f35e57test_line_expv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/c1f27aca-728b...test_line_exp.hdf5hdf51068184md5:7f45d6cc6cb586a7120f97033d2f0a89
120baseexposures04cf37a2-261c-4094-aa95-a81349413e84test_exposure_US_flood_random_locationsv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/04cf37a2-261c...test_exposure_US_flood_random_locations.hdf5hdf51065360md5:8b2ac82188da9fed60fcfdf6dad96f13
121river_floodhazardc17f8cdd-6a9b-4aaa-83b5-ceea6e8b1df0test_hazard_US_flood_random_locationsv1test_datasetNoneNoneAttribution 4.0 International (CC-BY-4.0)None...2022-07-01v3.1.2countryNaN150https://data.iac.ethz.ch/climada/c17f8cdd-6a9b...test_hazard_US_flood_random_locations.hdf5hdf51063139md5:44b005d8f3eeab8ed6b0627517c69685
122testteste10f3cdb-2fcb-4f64-921e-3fc628a96a49test_tc_floridav1test_datasetNoneNoneAttribution 4.0 International (CC BY 4.0)None...NaNNaNNaNNaNNaNhttps://data.iac.ethz.ch/climada/e10f3cdb-2fcb...test_tc_florida.hdf5hdf52014134md5:5abc506c804d27a35bb261b368d51136
\n", + "

123 rows × 25 columns

\n", + "
" + ], + "text/plain": [ + " data_type data_type_group uuid \\\n", + "0 measures measures 2306fb7a-d32f-4f0b-b2f6-9ee4f0d55946 \n", + "1 earth_engine exposures 8cca2e12-b262-4d6c-acdb-501750322bf7 \n", + "2 earth_engine exposures 1d2a7850-9ee6-480f-b5a6-4aa13c770256 \n", + "3 base exposures 890ea94d-ecab-4b5b-a617-2098dc930bf7 \n", + "4 impact impact c80c9370-777b-4f63-ad7f-9852adbb9b0a \n", + ".. ... ... ... \n", + "118 base exposures cc078ce5-4a37-41bd-8603-2d0afaf81e60 \n", + "119 base exposures c1f27aca-728b-4891-99f8-740777f35e57 \n", + "120 base exposures 04cf37a2-261c-4094-aa95-a81349413e84 \n", + "121 river_flood hazard c17f8cdd-6a9b-4aaa-83b5-ceea6e8b1df0 \n", + "122 test test e10f3cdb-2fcb-4f64-921e-3fc628a96a49 \n", + "\n", + " name version status doi \\\n", + "0 test_meas v1 test_dataset None \n", + "1 population-density_median v1 test_dataset None \n", + "2 dresden v1 test_dataset None \n", + "3 test_write_hazard v1 test_dataset None \n", + "4 WIOT2012_Nov16_ROW v1 test_dataset None \n", + ".. ... ... ... ... \n", + "118 test_point_exp v1 test_dataset None \n", + "119 test_line_exp v1 test_dataset None \n", + "120 test_exposure_US_flood_random_locations v1 test_dataset None \n", + "121 test_hazard_US_flood_random_locations v1 test_dataset None \n", + "122 test_tc_florida v1 test_dataset None \n", + "\n", + " description license \\\n", + "0 None Attribution 4.0 International (CC-BY-4.0) \n", + "1 None Attribution 4.0 International (CC-BY-4.0) \n", + "2 None Attribution 4.0 International (CC-BY-4.0) \n", + "3 None Attribution 4.0 International (CC-BY-4.0) \n", + "4 WIOT 2012, Nov 16, ROW Attribution 4.0 International (CC-BY-4.0) \n", + ".. ... ... \n", + "118 None Attribution 4.0 International (CC-BY-4.0) \n", + "119 None Attribution 4.0 International (CC-BY-4.0) \n", + "120 None Attribution 4.0 International (CC-BY-4.0) \n", + "121 None Attribution 4.0 International (CC-BY-4.0) \n", + "122 None Attribution 4.0 International (CC BY 4.0) \n", + "\n", + " activation_date ... date_creation climada_version spatial_coverage \\\n", + "0 None ... NaN NaN NaN \n", + "1 None ... NaN NaN NaN \n", + "2 None ... NaN NaN NaN \n", + "3 None ... NaN NaN NaN \n", + "4 None ... NaN NaN NaN \n", + ".. ... ... ... ... ... \n", + "118 None ... NaN NaN NaN \n", + "119 None ... NaN NaN NaN \n", + "120 None ... NaN NaN NaN \n", + "121 None ... 2022-07-01 v3.1.2 country \n", + "122 None ... NaN NaN NaN \n", + "\n", + " year_range res_arcsec url \\\n", + "0 NaN NaN https://data.iac.ethz.ch/climada/2306fb7a-d32f... \n", + "1 NaN NaN https://data.iac.ethz.ch/climada/8cca2e12-b262... \n", + "2 NaN NaN https://data.iac.ethz.ch/climada/1d2a7850-9ee6... \n", + "3 NaN NaN https://data.iac.ethz.ch/climada/890ea94d-ecab... \n", + "4 NaN NaN https://data.iac.ethz.ch/climada/c80c9370-777b... \n", + ".. ... ... ... \n", + "118 NaN NaN https://data.iac.ethz.ch/climada/cc078ce5-4a37... \n", + "119 NaN NaN https://data.iac.ethz.ch/climada/c1f27aca-728b... \n", + "120 NaN NaN https://data.iac.ethz.ch/climada/04cf37a2-261c... \n", + "121 NaN 150 https://data.iac.ethz.ch/climada/c17f8cdd-6a9b... \n", + "122 NaN NaN https://data.iac.ethz.ch/climada/e10f3cdb-2fcb... \n", + "\n", + " file_name file_format file_size \\\n", + "0 test_meas.xlsx xlsx 5673 \n", + "1 population-density_median.tif tif 247646 \n", + "2 dresden.tif tif 664471 \n", + "3 test_write_hazard.tif tif 446 \n", + "4 WIOT2012_Nov16_ROW.xlsb xlsb 62042190 \n", + ".. ... ... ... \n", + "118 test_point_exp.hdf5 hdf5 1064184 \n", + "119 test_line_exp.hdf5 hdf5 1068184 \n", + "120 test_exposure_US_flood_random_locations.hdf5 hdf5 1065360 \n", + "121 test_hazard_US_flood_random_locations.hdf5 hdf5 1063139 \n", + "122 test_tc_florida.hdf5 hdf5 2014134 \n", + "\n", + " check_sum \n", + "0 md5:c1a90eede3240671b73db87dc0ed9878 \n", + "1 md5:337eb33d7a4fd51a2e901e32fa561973 \n", + "2 md5:3e33539fee034a68aa626105e7a1e80f \n", + "3 md5:be78239036e62a4443eedb38bf451cc8 \n", + "4 md5:71e279a6ba7d58ff1990dd4c3f9ec61a \n", + ".. ... \n", + "118 md5:cd907ac937bdd8a4759d80c0a0aa643b \n", + "119 md5:7f45d6cc6cb586a7120f97033d2f0a89 \n", + "120 md5:8b2ac82188da9fed60fcfdf6dad96f13 \n", + "121 md5:44b005d8f3eeab8ed6b0627517c69685 \n", + "122 md5:5abc506c804d27a35bb261b368d51136 \n", + "\n", + "[123 rows x 25 columns]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c = Client()\n", + "testdata = c.list_dataset_infos(status='test_dataset')\n", + "test_files = c.into_files_df(testdata)\n", + "test_files" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "33366b33-7a18-4f81-b925-f3151a5b7cae", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### how to use data files in the repository for testing" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "da5acd51-da71-48cf-9053-acf40344d4d8", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "Not! 😝" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ab4ab061", + "metadata": { + "heading_collapsed": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "# Time for some exercises! " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "55e45a6c", + "metadata": { + "hidden": true + }, + "source": [ + "Take some tests in CLIMADA, and refactor the unit tests, move parts to the integration tests, refactor integration tests." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "718dd4e6", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "- replace big default test data with concise test data\n", + "- replace test data read from file with test data defined on the spot\n", + "- implement dump reload tests with `tempfile`\n", + "- clean up tests with side effects\n", + "- implement `__eq__` and use it for assertions\n", + "- add missing test cases\n", + "- remove superfluous test cases" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ae7f4f13", + "metadata": { + "heading_collapsed": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "# Extra infos" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1ac38dfb", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Test Cases" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "89a2afe6", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Parameter Test Set\n", + "\n", + "- Each parameter should be covered in test cases by as many values as there are qualitative ditinctions\n", + "\n", + "**Example**\n", + "\n", + "- How many test cases do we need for `f`? " + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "1c73fbc8-d823-4fda-b2e6-0c95f963e524", + "metadata": { + "hidden": true, + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAD4CAYAAADhNOGaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAT10lEQVR4nO3dbYxc133f8e+vlNkithpb1kqmScokDKIIm0aKuqCdKqitSDQoVTGtogWotoqQ1iBcSIBtJG3Zukjz0oiRuEismmBSwQrqWEhhqyIc2nogUqhp4JQrVQ+kZEUsa1c0WXGtBFZatRAY//ti79LT9S65y7m7w8P7/QCDuQ/nzvwHovY355x756aqkCQN11+YdAGSpMkyCCRp4AwCSRo4g0CSBs4gkKSBu2LSBVyMq6++urZs2TLpMiSpKU899dR3q2pq4fYmg2DLli3MzMxMugxJakqSby+23aEhSRo4g0CSBs4gkKSBMwgkaeAMAkkauF6CIMkDSc4kObrE/iT59STHkzyX5MaRfbuSvNTt29dHPZKk5eurR/AFYNd59t8GbOsee4HPAyRZB9zf7d8O3JVke081SZKWoZfrCKrqySRbztNkN/DbNfeb199I8vYkG4AtwPGqOgGQ5KGu7Qt91CWttUPPn+abp1+fdBm6jN154ya2Xv3WXl9zrS4o2wi8MrJ+stu22Pb3LfYCSfYy15vguuuuW50qpTHt+/JzvP5/z5JMuhJdrm58zzuaDYLF/reo82z/4Y1VB4ADANPT095NR5ek7xd89Ke38i/vcIRT7VirIDgJbB5Z3wScAtYvsV1qknf8U4vW6vTRg8DPdWcPvR/4XlWdBo4A25JsTbIe2NO1lSStkV56BEm+BHwQuDrJSeBfAW8BqKr9wCHgduA48Abw892+s0nuAx4F1gEPVNWxPmqSJsX5AbWmr7OG7rrA/gLuXWLfIeaCQmqeA0NqkVcWS9LAGQRSj6ogjg2pMQaBJA2cQSD1zP6AWmMQSD0qp4vVIINAkgbOIJB6VEv9cIp0CTMIJGngDAKpZ7FLoMYYBFKPnCpWiwwCSRo4g0DqU/mjc2qPQSBJA2cQSD2zQ6DWGARSj7yyWC0yCCRp4HoJgiS7kryU5HiSfYvs/ydJnukeR5P8eZKrun3fSvJ8t2+mj3qkSSkni9Wgse9QlmQdcD+wk7mb1B9JcrCqXphvU1WfAT7Ttf9Z4JNV9ScjL3NzVX133FokSSvXR49gB3C8qk5U1ZvAQ8Du87S/C/hSD+8rSepBH0GwEXhlZP1kt+2HJPkRYBfw5ZHNBTyW5Kkke5d6kyR7k8wkmZmdne2hbKl/c78559iQ2tJHECz2r36pUyd+FvjPC4aFbqqqG4HbgHuT/M3FDqyqA1U1XVXTU1NT41UsSTqnjyA4CWweWd8EnFqi7R4WDAtV1anu+QzwMHNDTVKznCxWa/oIgiPAtiRbk6xn7o/9wYWNkvwo8AHgkZFtb01y5fwy8CHgaA81SRNR5XUEas/YZw1V1dkk9wGPAuuAB6rqWJKPdfv3d03vBB6rqv89cvi1wMOZ+wp1BfA7VfX1cWuSJC3f2EEAUFWHgEMLtu1fsP4F4AsLtp0Aru+jBulS4A3K1CKvLJakgTMIpL45W6zGGARSj5wrVosMAkkaOINA6pkDQ2qNQSBJA2cQSD1zrlitMQiknnhVsVplEEjSwBkEUk/mOwT+DLVaYxBI0sAZBFLPnCxWawwCqSdOFatVBoEkDZxBIPVk/vRRR4bUGoNAkgaulyBIsivJS0mOJ9m3yP4PJvlekme6xy8t91hJ0uoa+w5lSdYB9wM7mbuR/ZEkB6vqhQVN/1NV3XGRx0qXvPnJYs8aUmv66BHsAI5X1YmqehN4CNi9BsdKknrQRxBsBF4ZWT/ZbVvop5I8m+RrSf7qCo8lyd4kM0lmZmdneyhb6te5K4vtEqgxfQTBYv/qF55S/TTwnqq6HvgN4D+s4Ni5jVUHqmq6qqanpqYutlZJ0gJ9BMFJYPPI+ibg1GiDqnq9qv5Xt3wIeEuSq5dzrCRpdfURBEeAbUm2JlkP7AEOjjZI8q50/eUkO7r3fW05x0qtKK8tVqPGPmuoqs4muQ94FFgHPFBVx5J8rNu/H/g7wD9Ochb4P8Cemrv6ZtFjx61JkrR8YwcBnBvuObRg2/6R5c8Bn1vusVKLfjBZPNk6pJXyymJJGjiDQJIGziCQeuYdytQag0CSBs4gkHriZLFaZRBI0sAZBJI0cAaB1JP5K4sdGVJrDAJJGjiDQOqJk8VqlUEgSQNnEEjSwBkEUk/O3bPY6WI1xiCQpIEzCKSeVHljGrWplyBIsivJS0mOJ9m3yP6/n+S57vGHSa4f2fetJM8neSbJTB/1SJPkWUNqzdg3pkmyDrgf2MncPYiPJDlYVS+MNPvvwAeq6k+T3AYcAN43sv/mqvruuLVIklaujx7BDuB4VZ2oqjeBh4Ddow2q6g+r6k+71W8wd5N66bLiwJBa1UcQbAReGVk/2W1byj8CvjayXsBjSZ5Ksnepg5LsTTKTZGZ2dnasgiVJP9DHPYsXGxFd9MtRkpuZC4KfHtl8U1WdSnIN8HiSb1bVkz/0glUHmBtSYnp62i9fuuQ4V6xW9dEjOAlsHlnfBJxa2CjJTwC/Beyuqtfmt1fVqe75DPAwc0NNUrPibLEa00cQHAG2JdmaZD2wBzg42iDJdcBXgLur6o9Htr81yZXzy8CHgKM91CRJWqaxh4aq6myS+4BHgXXAA1V1LMnHuv37gV8C3gn8m+7b0tmqmgauBR7utl0B/E5VfX3cmqSJmP/RuclWIa1YH3MEVNUh4NCCbftHlj8KfHSR404A1y/cLklaO15ZLEkDZxBIPTl3hzLHhtQYg0CSBs4gkHpSTharUQaBJA2cQSBJA2cQSD05d4cyZ4vVGINAkgbOIJB6Mn+HMjsEao1BIEkDZxBI0sAZBFJPzk0WT7QKaeUMAkkaOINA6ol3KFOrDAKpb542pMYYBJI0cL0EQZJdSV5KcjzJvkX2J8mvd/ufS3Ljco+VWnHuZ6gnXIe0UmMHQZJ1wP3AbcB24K4k2xc0uw3Y1j32Ap9fwbGSpFXUR49gB3C8qk5U1ZvAQ8DuBW12A79dc74BvD3JhmUeK7XByWI1qo8g2Ai8MrJ+stu2nDbLORaAJHuTzCSZmZ2dHbtoabU4V6zW9BEEi/2zX/jdaKk2yzl2bmPVgaqarqrpqampFZYoSVrKFT28xklg88j6JuDUMtusX8axUhN+cGWxXQK1pY8ewRFgW5KtSdYDe4CDC9ocBH6uO3vo/cD3qur0Mo+VJK2isXsEVXU2yX3Ao8A64IGqOpbkY93+/cAh4HbgOPAG8PPnO3bcmqRJ8MpitaqPoSGq6hBzf+xHt+0fWS7g3uUeK7XMyWK1xiuLJWngDAKpJ15ZrFYZBJI0cAaB1BMni9Uqg0DqmZPFao1BIEkDZxBIPfHKYrXKIJCkgTMIpJ6Us8VqlEEg9c2RITXGIJCkgTMIpJ44MqRWGQRSzxwZUmsMAkkaOINA6lm8tFiNMQgkaeDGCoIkVyV5PMnL3fM7FmmzOcnvJ3kxybEkHx/Z98tJvpPkme5x+zj1SJPkZLFaNW6PYB9wuKq2AYe79YXOAr9QVT8GvB+4N8n2kf2fraobuod3KlPzHBhSa8YNgt3Ag93yg8BHFjaoqtNV9XS3/GfAi8DGMd9XuuQUdgnUpnGD4NqqOg1zf/CBa87XOMkW4CeBPxrZfF+S55I8sNjQ0sixe5PMJJmZnZ0ds2xp9ThXrNZcMAiSPJHk6CKP3St5oyRvA74MfKKqXu82fx54L3ADcBr41aWOr6oDVTVdVdNTU1MreWtJ0nlccaEGVXXrUvuSvJpkQ1WdTrIBOLNEu7cwFwJfrKqvjLz2qyNtfhP46kqKly4lTharVeMODR0E7umW7wEeWdggcydV/1vgxar6tQX7Noys3gkcHbMeaeIcGlJrxg2CTwM7k7wM7OzWSfLuJPNnAN0E3A38zCKnif5KkueTPAfcDHxyzHokSSt0waGh86mq14BbFtl+Cri9W/4DljijrqruHuf9pUuJdyhTq7yyWJIGziCQeuIdytQqg0DqmZPFao1BIEkDZxBIPXFgSK0yCCRp4AwCqSfOFatVBoHUM+9QptYYBJI0cAaB1BvHhtQmg0DqmQNDao1BIPXEyWK1yiCQeuZcsVpjEEjSwBkEUk8cGVKrxgqCJFcleTzJy93zojefT/Kt7gY0zySZWenxUku8H4FaM26PYB9wuKq2AYe79aXcXFU3VNX0RR4vXdKcLFarxg2C3cCD3fKDwEfW+HjpkuNksVozbhBcW1WnAbrna5ZoV8BjSZ5KsvcijifJ3iQzSWZmZ2fHLFuSNO+C9yxO8gTwrkV2fWoF73NTVZ1Kcg3weJJvVtWTKzieqjoAHACYnp62E65LTjldrEZdMAiq6tal9iV5NcmGqjqdZANwZonXONU9n0nyMLADeBJY1vFSSxwZUmvGHRo6CNzTLd8DPLKwQZK3Jrlyfhn4EHB0ucdLrXCyWK0aNwg+DexM8jKws1snybuTHOraXAv8QZJngf8C/F5Vff18x0stc7JYrbng0ND5VNVrwC2LbD8F3N4tnwCuX8nxkqS145XFUk8cGlKrDAKpd44NqS0GgdQTTx9VqwwCqWdOFqs1BoEkDZxBIPXEyWK1yiCQeubIkFpjEEjSwBkEkjRwBoHUs3jakBpjEEg9cbJYrTIIpJ7ZH1BrDAKpJ15ZrFYZBJI0cAaB1DPnitUag0DqiZPFatVYQZDkqiSPJ3m5e37HIm3+SpJnRh6vJ/lEt++Xk3xnZN/t49QjXQrsEag14/YI9gGHq2obcLhb//9U1UtVdUNV3QD8deAN4OGRJp+d319VhxYeL7XCDoFaNW4Q7AYe7JYfBD5ygfa3AP+tqr495vtKknoybhBcW1WnAbrnay7Qfg/wpQXb7kvyXJIHFhtampdkb5KZJDOzs7PjVS2tonglgRpzwSBI8kSSo4s8dq/kjZKsBz4M/PuRzZ8H3gvcAJwGfnWp46vqQFVNV9X01NTUSt5aWhPlbLEadcWFGlTVrUvtS/Jqkg1VdTrJBuDMeV7qNuDpqnp15LXPLSf5TeCryytbuoTZIVBjxh0aOgjc0y3fAzxynrZ3sWBYqAuPeXcCR8esR5oY+wNq1bhB8GlgZ5KXgZ3dOkneneTcGUBJfqTb/5UFx/9KkueTPAfcDHxyzHokSSt0waGh86mq15g7E2jh9lPA7SPrbwDvXKTd3eO8v3QpcmRIrfHKYqknzhWrVQaBJA2cQSD1zDuUqTUGgdQbx4bUJoNA6pn9AbXGIJB64mSxWmUQSNLAGQRSz5wrVmsMAqknjgypVQaB1DN/hlqtMQiknjhZrFYZBJI0cAaB1DMni9Uag0DqiXcoU6sMAqlndgjUGoNA6on9AbVqrCBI8neTHEvy/STT52m3K8lLSY4n2Tey/aokjyd5uXt+xzj1SJJWbtwewVHgbwNPLtUgyTrgfuZuXr8duCvJ9m73PuBwVW0DDnfrUtscG1Jjxr1V5Ytwwd9f3wEcr6oTXduHgN3AC93zB7t2DwL/Efhn49R0Pr9x+GUOPntqtV5eA/fGm38+6RKkizJWECzTRuCVkfWTwPu65Wur6jRAVZ1Ocs1SL5JkL7AX4LrrrruoQqau/Itsu/ZtF3WstBx/473v5Mc3/uiky5BW5IJBkOQJ4F2L7PpUVT2yjPdYrLuw4nm1qjoAHACYnp6+qHm5PTuuY8+OiwsRSbpcXTAIqurWMd/jJLB5ZH0TMD8+82qSDV1vYANwZsz3kiSt0FqcPnoE2JZka5L1wB7gYLfvIHBPt3wPsJwehiSpR+OePnpnkpPATwG/l+TRbvu7kxwCqKqzwH3Ao8CLwO9W1bHuJT4N7EzyMrCzW5ckraG0eFn89PR0zczMTLoMSWpKkqeq6oeu+fLKYkkaOINAkgbOIJCkgTMIJGngmpwsTjILfHvSdVyEq4HvTrqINeZnvvwN7fNCu5/5PVU1tXBjk0HQqiQzi83YX878zJe/oX1euPw+s0NDkjRwBoEkDZxBsLYOTLqACfAzX/6G9nnhMvvMzhFI0sDZI5CkgTMIJGngDIIJSPKLSSrJ1ZOuZbUl+UySbyZ5LsnDSd4+6ZpWS5JdSV5KcjzJZX//7SSbk/x+kheTHEvy8UnXtFaSrEvyX5N8ddK19MEgWGNJNjP3k9v/Y9K1rJHHgR+vqp8A/hj45xOuZ1UkWQfcD9wGbAfuSrJ9slWturPAL1TVjwHvB+4dwGee93Hmflb/smAQrL3PAv+Ui7hdZ4uq6rHunhQA32DuDnWXox3A8ao6UVVvAg8Buydc06qqqtNV9XS3/GfM/WHcONmqVl+STcDfAn5r0rX0xSBYQ0k+DHynqp6ddC0T8g+Br026iFWyEXhlZP0kA/ijOC/JFuAngT+acClr4V8z92Xu+xOuozcXvGexVibJE8C7Ftn1KeBfAB9a24pW3/k+c1U90rX5FHNDCV9cy9rWUBbZNoheX5K3AV8GPlFVr0+6ntWU5A7gTFU9leSDEy6nNwZBz6rq1sW2J/lrwFbg2SQwN0TydJIdVfU/17DE3i31mecluQe4A7ilLt8LV04Cm0fWNwGnJlTLmknyFuZC4ItV9ZVJ17MGbgI+nOR24C8BfznJv6uqfzDhusbiBWUTkuRbwHRVtfgLhsuWZBfwa8AHqmp20vWsliRXMDcZfgvwHeAI8PdG7s992cncN5oHgT+pqk9MuJw11/UIfrGq7phwKWNzjkCr7XPAlcDjSZ5Jsn/SBa2GbkL8PuBR5iZNf/dyDoHOTcDdwM90/22f6b4pqzH2CCRp4OwRSNLAGQSSNHAGgSQNnEEgSQNnEEjSwBkEkjRwBoEkDdz/A8KLzQxiTFv3AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "\n", + "def f(x):\n", + " if x < 0: return -1\n", + " if x > 0: return +1\n", + " return 0\n", + "\n", + "xa = np.arange(-5,5,0.01)\n", + "ya = np.array([f(x) for x in xa])\n", + " \n", + "plt.plot(xa, ya);" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "861e4829-385a-444a-b487-6147b20db90a", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "### Should `None` be in the test set?\n", + "\n", + "- Null pointer exceptions are notoriously hard to debug and very annoying.\n", + "- However - the `NoneType` is only another type and Python is not a type safe language. We cannot test _everyghing_.\n", + "- Suggestion: Check only if it's (implicitly or explicitly) part of the method description." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "fe4c15b1-3092-42af-b97c-8f3a9cd81c8b", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "### Combinatorics\n", + "\n", + "```\n", + "def f(a: A, b: B, c: C):\n", + " ...\n", + "```\n", + "\n", + "- Full parameter space, nominally: `A x B x C` \\\n", + " _watch out_! this is often _de facto_ `(A | B) x C`\n", + "- make sure each parameter is covered by itself sufficiently\\\n", + " and combinations are covered if they are linked, i.e., _if they make a difference together_.\n", + "- many parameters? think about refactoring!\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "489e39f5", + "metadata": { + "hidden": true + }, + "source": [ + "### Bottom line\n", + "\n", + "- test all valid cases that make a difference" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f326c4f8", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Arrange" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a34ad5d8-643a-495c-b296-701a37096bdf", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "Aim for \n", + "- minimal objects\n", + "- containing crucial data\n", + "- illustrating the case\n", + "- defined on the spot" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6c39d97c-07b7-41e1-8b02-b77a33ec01c5", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Assert" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "bfb64fe3-ea6d-4f11-a3b4-8e799832d87b", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Checking Results\n", + "\n", + "comparing an expected result with the actual may be cumbersome:\n", + "\n", + "```\n", + "e = expected()\n", + "r = calculated()\n", + "\n", + "assert r.x == e.x\n", + "assert r.y == e.y\n", + "assert len(r.z) == len(e.z)\n", + "...\n", + "\n", + "```\n", + "\n", + "- it's easy to lose track\n", + "- changes in target demand that each such test must be updated\n", + "\n", + "Example: [TestImpactCalc.test_calc_impact_TC_pass](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/engine/test/test_impact_calc.py#L104)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a54859fc-cf3b-47fb-9820-617da1b94d7b", + "metadata": { + "hidden": true + }, + "source": [ + "#### Check equality if possible\n", + "\n", + "- e.g., dataclasses: if `__eq__` is overwritten to compare _value_ instead of _id_, this can be shortened to\n", + "\n", + "```\n", + "e = expected()\n", + "r = calculated()\n", + "\n", + "assert r == e\n", + "\n", + "```\n", + "\n", + "- advantage: complete test, not depending on random samples, when target changes, only `__eq__` must be updated.\n", + "- downside: performance may be an issue." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "fe7eb949-8ac1-4b7b-8da0-b1c76bc285c4", + "metadata": { + "hidden": true + }, + "source": [ + "#### Custom helper method\n", + "\n", + "- when overwriting `__eq__` is not an option, we can still isolate the comparison and equality check in a helper method.\n", + "\n", + "Example: [climada.util.test.test_lines_polys_handler](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/util/test/test_lines_polys_handler.py#L68)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a203166f-bbd7-434b-a241-4a58c2f1a60f", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Check for errors thrown\n", + "\n", + "- thrown exceptions are, i.g., corner cases worth testing\n", + "- i.g., do not check for exceptions raised by called methods.\n", + "\n", + "Example: [entity.disc_rates.test.test_base.TestChecker.test_check_wrongRates_fail](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/entity/disc_rates/test/test_base.py#L32)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "44f97f1d-4e2b-42c1-aa2d-4c2aa7181ed8", + "metadata": { + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### Check for log content\n", + "\n", + "(errors/warnings/infos)\n", + "\n", + "- i.g. not to be tested\n", + "\n", + "Exceptions:\n", + "\n", + "- it _is_ important for the user\n", + "- it is a means of checking the behavior of the method. (In this case mocking may be an alternative.)\n", + "\n", + "Example: [util.test.test_dwd_icon.TestDeleteIcon.test_file_not_exist_warning](https://github.com/CLIMADA-project/climada_python/blob/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/util/test/test_dwd_icon.py#L86)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "600516f3-20e8-471d-aa90-3ffec39952ea", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "### Testing Plot Functions" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "296d7006-87d9-4e9e-9b6a-5bf30a6452c8", + "metadata": { + "hidden": true + }, + "source": [ + "Testing plot functions _properly_ is demanding!\n", + "\n", + "Suggestions:\n", + "\n", + "- stick to regression tests, i.e. just check for making plots without raising an Exception\n", + "- mock ?\n", + "- ai ?" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "939273ae-0895-4bcf-b8d3-b080bb7ea90b", + "metadata": { + "heading_collapsed": true, + "hidden": true, + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Mocking" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6ede723c-9697-4be4-b8dd-3de28a84dd65", + "metadata": { + "hidden": true, + "tags": [] + }, + "source": [ + "In most cases the target method calls other methods.\\\n", + "In theory a unit test must not care whether these methods further down are correct or not.\\\n", + "In practice it often doesn't matter.\n", + "\n", + "Where it does matter:\n", + "\n", + "- the called methods access external resources (e.g., `requests.get()`)\n", + "- the called methods are expensive\n", + "- the tested method is primarily a wrapper around other methods\\\n", + " (juggling arguments)\n", + " \n", + "For these cases you can use a _Mock_ (or _Stub_) object. These do not really do anything but take arguments and provide predefined results for testing purposes.\n", + "\n", + "The difference between _Mock_ and _Stub_ is that _Mock_ additionally keeps track of the methods that were called and the arguments that were provided during the test. After the test has been performed the _Mock_ object can be inspected for this information.\n", + "\n", + "package: [unittest.mock](https://docs.python.org/3/library/unittest.mock.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "8ad68a98-9c81-4a7a-9c88-f64ff22f5f3f", + "metadata": { + "hidden": true, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mocked: (0, {})\n", + "reset: ((1, 2), {'k': <__main__.A object at 0x0000027C0DB535E0>})\n" + ] + }, + { + "data": { + "text/plain": [ + "[1]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from unittest.mock import patch, ANY\n", + "\n", + "side_effects = []\n", + "\n", + "class A():\n", + " def do_something(self, *args, **kwargs):\n", + " side_effects.append(1)\n", + " return args, kwargs\n", + "\n", + "with patch(\"__main__.A.do_something\") as do_something_else:\n", + " # within this context block, `do_something` returns a predefined result\n", + " do_something_else.return_value = 0, {}\n", + " t = A()\n", + " print(\"mocked:\", t.do_something(1, 2, k=A()))\n", + "\n", + "# from here on `do_something` works normally again\n", + "print(\"reset:\", t.do_something(1, 2, k=A()))\n", + "\n", + "# inspect call history\n", + "do_something_else.assert_called_once_with(1, 2, k=ANY) # using A() instead of ANY raises an exception: A() == A() is False!\n", + "\n", + "side_effects" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "efa04c7c-c9cd-4337-900a-d6a7ac1b9fb6", + "metadata": { + "hidden": true + }, + "source": [ + "example: [hazard.test.test_base_xarray.TestReadDimsCoordsNetCDF.test_load_dataset_rechunk](https://github.com/CLIMADA-project/climada_python/blame/4d8fa43b42fd8322313cc0a8c8b6f347cfaeed5b/climada/hazard/test/test_base_xarray.py#L471)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c643154e", + "metadata": { + "heading_collapsed": true, + "tags": [] + }, + "source": [ + "# Take Home Message" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f9b1ebd9-6896-414b-a3dc-8737361db98e", + "metadata": { + "hidden": true + }, + "source": [ + "if writing tests for an existing method turns out to be hard work, consider refactoring the method!" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + }, + "latex_envs": { + "LaTeX_envs_menu_present": true, + "autoclose": false, + "autocomplete": true, + "bibliofile": "biblio.bib", + "cite_by": "apalike", + "current_citInitial": 1, + "eqLabelWithNumbers": true, + "eqNumInitial": 1, + "hotkeys": { + "equation": "Ctrl-E", + "itemize": "Ctrl-I" + }, + "labels_anchors": false, + "latex_user_defs": false, + "report_style_numbering": false, + "user_envs_cfg": false + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}