diff --git a/exercises/problem/climate_model.py b/exercises/problem/climate_model.py
new file mode 100644
index 0000000..d791bdc
--- /dev/null
+++ b/exercises/problem/climate_model.py
@@ -0,0 +1,15 @@
+solar_constant = 1370
+sigma = 5.67e-8 # Stefan-Boltzmann constant
+
+def absorbed_solar_radiation(albedo):
+ return solar_constant*(1-albedo)/4
+
+def emitted_radiation(emissivity, temperature):
+ return emissivity * sigma * temperature**4
+
+def energy_balanced_temperature(emissivity, albedo):
+ # dT/dt = 0 => in = out
+ return (solar_constant*(1-albedo)/(4*emissivity * sigma))**0.25
+
+def emissivity_given_temp(albedo, temperature):
+ return solar_constant*(1-albedo)/(4*sigma*temperature**4)
diff --git a/exercises/problem/plot4.py b/exercises/problem/plot4.py
new file mode 100644
index 0000000..afa599a
--- /dev/null
+++ b/exercises/problem/plot4.py
@@ -0,0 +1,25 @@
+import climate_model
+
+import xarray
+import matplotlib.pyplot as plt
+
+global_ds = xarray.open_dataset("../data/HadCRUT.5.0.0.0_analysis_summary-series_global_annual.nc")
+
+fig, axs = plt.subplots(1, 1, figsize=(10,6), layout='constrained')
+
+# Absolute mean temperature between 1961 and 1990
+# https://crudata.uea.ac.uk/cru/data/temperature/abs_glnhsh.txt
+absolute_value = 13.974 + 273.5
+time = global_ds['time'].data
+temperature = global_ds['tas_mean'].data + absolute_value
+albedo = 0.3
+
+epsilon = climate_model.emissivity_given_temp(albedo, temperature)
+
+axs.plot(time, epsilon)
+
+axs.set_xlabel("Year")
+axs.set_ylabel("Effective emissivity of Earth")
+
+plt.show()
+
diff --git a/exercises/testing_exercises.qmd b/exercises/testing_exercises.qmd
new file mode 100644
index 0000000..eba8a7b
--- /dev/null
+++ b/exercises/testing_exercises.qmd
@@ -0,0 +1,13 @@
+## Exercises
+
+In the example repository, there is a file called 'climate_model.py'.
+This file contains some functions that are used in 'plot4.py'
+
+Task:
+
+1. Write some basic tests for these functions (think about inputs that give known
+ outputs)
+2. Think about what properties/behaviours these functions should have, and write
+ tests for them
+3. Look at the other python files, and see if any functions can be extracted and
+ tested
diff --git a/src/_pytest.qmd b/src/_pytest.qmd
new file mode 100644
index 0000000..5bed2b1
--- /dev/null
+++ b/src/_pytest.qmd
@@ -0,0 +1,162 @@
+# Testing with pytest
+
+## What is pytest?
+
+:::{.columns}
+::::{.column width=50%}
+- A testing *framework* for python
+- Makes it a lot easier to write tests
+- Is used extensively by python packages
+- Automatically discovers and runs tests
+::::
+::::{.column width=50%}
+{style="width:50%;"}
+::::
+:::
+
+## pytest basics
+
+\
+
+- Tests are .py files and live in a `tests/` directory
+- Each test group is a function with prefix `test_`
+- Use the `assert` keyword to validate an expression is true
+
+\
+
+:::{.columns}
+::::{.column width=30%}
+```{.python filename="src/add.py"}
+def add(a, b):
+ return a+b
+```
+```{.python filename="tests/test.py"}
+def test_add():
+ assert add(0, 0) == 0
+ assert add(1, 0) == 1
+ assert add(0, 1) == 1
+ assert add(1, 1) == 2
+ ...
+```
+::::
+::::{.column width=70%}
+
+```{=html}
+
+
+===================================== test session starts =====================================
+platform linux -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0
+rootdir: /tmp/example
+collected 1 item
+
+tests/test.py . [100%]
+
+====================================== 1 passed in 0.00s ======================================
+
+```
+::::
+:::
+
+## pytest basics
+
+What if we implemented `add` wrong?
+
+```{.python filename="src/add,py"}
+def add(a, b):
+ return a-b # Incorrect
+```
+
+\
+
+```{=html}
+
+[jack@nixos:~]$ pytest tests/test.py
+platform linux -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0
+rootdir: /tmp/example
+collected 1 item
+
+tests/test.py F [100%]
+
+========================================== FAILURES ===========================================
+__________________________________________ test_add ___________________________________________
+
+ def test_add():
+ assert add(0, 0) == 0
+ assert add(1, 0) == 1
+> assert add(0, 1) == 1
+E assert -1 == 1
+E + where -1 = add(0, 1)
+
+tests/test.py:7: AssertionError
+=================================== short test summary info ===================================
+FAILED tests/test.py::test_add - assert -1 == 1
+====================================== 1 failed in 0.02s ======================================
+
+```
+
+## Parametrized tests
+
+- pytest implements a lot of useful tools to make writing tests easier
+- One of these is test parametrization, which allows us to reuse tests for different
+ inputs
+- We only need to give the `pytest.mark.parametrize` decorator:
+ - The names of the input variables
+ - A list of tuples of inputs
+
+```{.python filename="tests/test.py"}
+@pytest.mark.parametrize("x,y,expected", [(0, 0, 0), (1, 0, 1), (0, 1, 1), (1, 1, 2)])
+def test_add(x, y, expected):
+ assert add(x, y) == expected
+```
+
+## What to test
+
+- Beyond using fixed points, we can think about how a function should behave
+- For example, our add function should have the following *properties*
+ - `add(x,0) == x`
+ - `add(x, y) == add(y, x)`
+ - `add(x, add(y, z)) == add(add(x, y), z)`
+
+## Writing code for unit testing
+
+\
+
+- Unit testing forces us to write code that is minimal
+- Decompose functions into their smallest logical unit
+- Smaller functions are easier to test (and review!)
+ - Fewer/simpler inputs
+ - Can help to think of edge cases
+
+### Example
+
+:::{.columns}
+::::{.column width=50%}
+Easier to test this:
+```{.python}
+def complicated_computation(data):
+ z = computation_1(data.x, data.y)
+ return computation_2(data.t, data.w, z)
+
+def computation_1(x, y):
+ result = atan2(x, y)
+ # Maybe do something else here
+ return result
+
+def computation_2(t, w, z):
+ result = exp((w+z)/t)
+ # Maybe do something else here
+ return result
+```
+::::
+::::{.column width=50%}
+Than this:
+```{.python}
+def complicated_computation(data):
+ z = atan2(data.x, data.y)
+ z = ...
+ result = exp((data.w+z)/data.t)
+ result = ...
+ return result
+```
+::::
+:::
diff --git a/src/custom.scss b/src/custom.scss
index b082f18..b986bc7 100644
--- a/src/custom.scss
+++ b/src/custom.scss
@@ -83,6 +83,17 @@ $accent: #0c4e8a;
font-weight: 500;
}
+$presentation-header-margin: 5%;
+
+$presentation-font-size-root: 1.5em;
+$presentation-line-height: 1.5em;
+
+/*-- scss:rules --*/
+
+.reveal .slide .li {
+ margin-top: 1em;
+}
+
/*-- vertically center columns (or any container) --*/
.v-center-container {
display: flex;
@@ -104,3 +115,304 @@ $accent: #0c4e8a;
}
}
}
+
+.term-container {
+ background: #171717;
+ border-radius: 5px;
+ border-color: white;
+ color: white;
+ word-break: break-word;
+ overflow-wrap: break-word;
+ font-family: "SFMono-Regular", Monaco, Menlo, Consolas, "Liberation Mono", Courier, monospace;
+ font-size: 12px;
+ line-height: 20px;
+ white-space: pre-wrap;
+}
+
+.term-container img { max-width: 100%; }
+
+.term-container time { padding-right: 1ex; }
+
+.term a { color: inherit; text-decoration: underline; text-decoration-style: dashed; }
+.term a:hover { color: #2882F9 }
+
+@keyframes blink-animation {
+ to {
+ visibility: hidden;
+ }
+}
+
+.term-fg1 { } /* don't bold beccause it looks weird */
+.term-fg2 { color: #838887; } /* faint (decreased intensity) - same as gray really */
+.term-fg3 { font-style: italic; } /* italic */
+.term-fg4 { text-decoration: underline; } /* underline */
+.term-fg5 { animation: blink-animation 1s steps(3, start) infinite; } /* blink */
+.term-fg9 { text-decoration: line-through; } /* crossed-out */
+
+.term-fg30 { color: #666666; } /* black (but we can't use black, so a diff color) */
+.term-fg31 { color: #ff7070; } /* red */
+.term-fg32 { color: #b0f986; } /* green */
+.term-fg33 { color: #c6c502; } /* yellow */
+.term-fg34 { color: #8db7e0; } /* blue */
+.term-fg35 { color: #f271fb; } /* magenta */
+.term-fg36 { color: #6bf7ff; } /* cyan */
+
+/* high intense colors */
+.term-fgi1 { color: #5ef765; }
+.term-fgi90 { color: #838887; } /* grey */
+.term-fgi91 { color: #ff3333; } /* red */
+.term-fgi92 { color: #00FF00; } /* green */
+.term-fgi93 { color: #fffc67; } /* yellow */
+.term-fgi94 { color: #6871ff; } /* blue */
+.term-fgi95 { color: #ff76ff; } /* magenta */
+.term-fgi96 { color: #60fcff; } /* cyan */
+
+/* background colors */
+.term-bg40 { background: #676767; } /* grey */
+.term-bg41 { background: #ff4343; } /* red */
+.term-bg42 { background: #99ff5f; } /* green */
+
+/* custom foreground/background combos for readability */
+.term-fg31.term-bg40 { color: #F8A39F; }
+
+/* xterm colors */
+.term-fgx16 { color: #000000; }
+.term-fgx17 { color: #00005f; }
+.term-fgx18 { color: #000087; }
+.term-fgx19 { color: #0000af; }
+.term-fgx20 { color: #0000d7; }
+.term-fgx21 { color: #0000ff; }
+.term-fgx22 { color: #005f00; }
+.term-fgx23 { color: #005f5f; }
+.term-fgx24 { color: #005f87; }
+.term-fgx25 { color: #005faf; }
+.term-fgx26 { color: #005fd7; }
+.term-fgx27 { color: #005fff; }
+.term-fgx28 { color: #008700; }
+.term-fgx29 { color: #00875f; }
+.term-fgx30 { color: #008787; }
+.term-fgx31 { color: #0087af; }
+.term-fgx32 { color: #0087d7; }
+.term-fgx33 { color: #0087ff; }
+.term-fgx34 { color: #00af00; }
+.term-fgx35 { color: #00af5f; }
+.term-fgx36 { color: #00af87; }
+.term-fgx37 { color: #00afaf; }
+.term-fgx38 { color: #00afd7; }
+.term-fgx39 { color: #00afff; }
+.term-fgx40 { color: #00d700; }
+.term-fgx41 { color: #00d75f; }
+.term-fgx42 { color: #00d787; }
+.term-fgx43 { color: #00d7af; }
+.term-fgx44 { color: #00d7d7; }
+.term-fgx45 { color: #00d7ff; }
+.term-fgx46 { color: #00ff00; }
+.term-fgx47 { color: #00ff5f; }
+.term-fgx48 { color: #00ff87; }
+.term-fgx49 { color: #00ffaf; }
+.term-fgx50 { color: #00ffd7; }
+.term-fgx51 { color: #00ffff; }
+.term-fgx52 { color: #5f0000; }
+.term-fgx53 { color: #5f005f; }
+.term-fgx54 { color: #5f0087; }
+.term-fgx55 { color: #5f00af; }
+.term-fgx56 { color: #5f00d7; }
+.term-fgx57 { color: #5f00ff; }
+.term-fgx58 { color: #5f5f00; }
+.term-fgx59 { color: #5f5f5f; }
+.term-fgx60 { color: #5f5f87; }
+.term-fgx61 { color: #5f5faf; }
+.term-fgx62 { color: #5f5fd7; }
+.term-fgx63 { color: #5f5fff; }
+.term-fgx64 { color: #5f8700; }
+.term-fgx65 { color: #5f875f; }
+.term-fgx66 { color: #5f8787; }
+.term-fgx67 { color: #5f87af; }
+.term-fgx68 { color: #5f87d7; }
+.term-fgx69 { color: #5f87ff; }
+.term-fgx70 { color: #5faf00; }
+.term-fgx71 { color: #5faf5f; }
+.term-fgx72 { color: #5faf87; }
+.term-fgx73 { color: #5fafaf; }
+.term-fgx74 { color: #5fafd7; }
+.term-fgx75 { color: #5fafff; }
+.term-fgx76 { color: #5fd700; }
+.term-fgx77 { color: #5fd75f; }
+.term-fgx78 { color: #5fd787; }
+.term-fgx79 { color: #5fd7af; }
+.term-fgx80 { color: #5fd7d7; }
+.term-fgx81 { color: #5fd7ff; }
+.term-fgx82 { color: #5fff00; }
+.term-fgx83 { color: #5fff5f; }
+.term-fgx84 { color: #5fff87; }
+.term-fgx85 { color: #5fffaf; }
+.term-fgx86 { color: #5fffd7; }
+.term-fgx87 { color: #5fffff; }
+.term-fgx88 { color: #870000; }
+.term-fgx89 { color: #87005f; }
+.term-fgx90 { color: #870087; }
+.term-fgx91 { color: #8700af; }
+.term-fgx92 { color: #8700d7; }
+.term-fgx93 { color: #8700ff; }
+.term-fgx94 { color: #875f00; }
+.term-fgx95 { color: #875f5f; }
+.term-fgx96 { color: #875f87; }
+.term-fgx97 { color: #875faf; }
+.term-fgx98 { color: #875fd7; }
+.term-fgx99 { color: #875fff; }
+.term-fgx100 { color: #878700; }
+.term-fgx101 { color: #87875f; }
+.term-fgx102 { color: #878787; }
+.term-fgx103 { color: #8787af; }
+.term-fgx104 { color: #8787d7; }
+.term-fgx105 { color: #8787ff; }
+.term-fgx106 { color: #87af00; }
+.term-fgx107 { color: #87af5f; }
+.term-fgx108 { color: #87af87; }
+.term-fgx109 { color: #87afaf; }
+.term-fgx110 { color: #87afd7; }
+.term-fgx111 { color: #87afff; }
+.term-fgx112 { color: #87d700; }
+.term-fgx113 { color: #87d75f; }
+.term-fgx114 { color: #87d787; }
+.term-fgx115 { color: #87d7af; }
+.term-fgx116 { color: #87d7d7; }
+.term-fgx117 { color: #87d7ff; }
+.term-fgx118 { color: #87ff00; }
+.term-fgx119 { color: #87ff5f; }
+.term-fgx120 { color: #87ff87; }
+.term-fgx121 { color: #87ffaf; }
+.term-fgx122 { color: #87ffd7; }
+.term-fgx123 { color: #87ffff; }
+.term-fgx124 { color: #af0000; }
+.term-fgx125 { color: #af005f; }
+.term-fgx126 { color: #af0087; }
+.term-fgx127 { color: #af00af; }
+.term-fgx128 { color: #af00d7; }
+.term-fgx129 { color: #af00ff; }
+.term-fgx130 { color: #af5f00; }
+.term-fgx131 { color: #af5f5f; }
+.term-fgx132 { color: #af5f87; }
+.term-fgx133 { color: #af5faf; }
+.term-fgx134 { color: #af5fd7; }
+.term-fgx135 { color: #af5fff; }
+.term-fgx136 { color: #af8700; }
+.term-fgx137 { color: #af875f; }
+.term-fgx138 { color: #af8787; }
+.term-fgx139 { color: #af87af; }
+.term-fgx140 { color: #af87d7; }
+.term-fgx141 { color: #af87ff; }
+.term-fgx142 { color: #afaf00; }
+.term-fgx143 { color: #afaf5f; }
+.term-fgx144 { color: #afaf87; }
+.term-fgx145 { color: #afafaf; }
+.term-fgx146 { color: #afafd7; }
+.term-fgx147 { color: #afafff; }
+.term-fgx148 { color: #afd700; }
+.term-fgx149 { color: #afd75f; }
+.term-fgx150 { color: #afd787; }
+.term-fgx151 { color: #afd7af; }
+.term-fgx152 { color: #afd7d7; }
+.term-fgx153 { color: #afd7ff; }
+.term-fgx154 { color: #afff00; }
+.term-fgx155 { color: #afff5f; }
+.term-fgx156 { color: #afff87; }
+.term-fgx157 { color: #afffaf; }
+.term-fgx158 { color: #afffd7; }
+.term-fgx159 { color: #afffff; }
+.term-fgx160 { color: #d70000; }
+.term-fgx161 { color: #d7005f; }
+.term-fgx162 { color: #d70087; }
+.term-fgx163 { color: #d700af; }
+.term-fgx164 { color: #d700d7; }
+.term-fgx165 { color: #d700ff; }
+.term-fgx166 { color: #d75f00; }
+.term-fgx167 { color: #d75f5f; }
+.term-fgx168 { color: #d75f87; }
+.term-fgx169 { color: #d75faf; }
+.term-fgx170 { color: #d75fd7; }
+.term-fgx171 { color: #d75fff; }
+.term-fgx172 { color: #d78700; }
+.term-fgx173 { color: #d7875f; }
+.term-fgx174 { color: #d78787; }
+.term-fgx175 { color: #d787af; }
+.term-fgx176 { color: #d787d7; }
+.term-fgx177 { color: #d787ff; }
+.term-fgx178 { color: #d7af00; }
+.term-fgx179 { color: #d7af5f; }
+.term-fgx180 { color: #d7af87; }
+.term-fgx181 { color: #d7afaf; }
+.term-fgx182 { color: #d7afd7; }
+.term-fgx183 { color: #d7afff; }
+.term-fgx184 { color: #d7d700; }
+.term-fgx185 { color: #d7d75f; }
+.term-fgx186 { color: #d7d787; }
+.term-fgx187 { color: #d7d7af; }
+.term-fgx188 { color: #d7d7d7; }
+.term-fgx189 { color: #d7d7ff; }
+.term-fgx190 { color: #d7ff00; }
+.term-fgx191 { color: #d7ff5f; }
+.term-fgx192 { color: #d7ff87; }
+.term-fgx193 { color: #d7ffaf; }
+.term-fgx194 { color: #d7ffd7; }
+.term-fgx195 { color: #d7ffff; }
+.term-fgx196 { color: #ff0000; }
+.term-fgx197 { color: #ff005f; }
+.term-fgx198 { color: #ff0087; }
+.term-fgx199 { color: #ff00af; }
+.term-fgx200 { color: #ff00d7; }
+.term-fgx201 { color: #ff00ff; }
+.term-fgx202 { color: #ff5f00; }
+.term-fgx203 { color: #ff5f5f; }
+.term-fgx204 { color: #ff5f87; }
+.term-fgx205 { color: #ff5faf; }
+.term-fgx206 { color: #ff5fd7; }
+.term-fgx207 { color: #ff5fff; }
+.term-fgx208 { color: #ff8700; }
+.term-fgx209 { color: #ff875f; }
+.term-fgx210 { color: #ff8787; }
+.term-fgx211 { color: #ff87af; }
+.term-fgx212 { color: #ff87d7; }
+.term-fgx213 { color: #ff87ff; }
+.term-fgx214 { color: #ffaf00; }
+.term-fgx215 { color: #ffaf5f; }
+.term-fgx216 { color: #ffaf87; }
+.term-fgx217 { color: #ffafaf; }
+.term-fgx218 { color: #ffafd7; }
+.term-fgx219 { color: #ffafff; }
+.term-fgx220 { color: #ffd700; }
+.term-fgx221 { color: #ffd75f; }
+.term-fgx222 { color: #ffd787; }
+.term-fgx223 { color: #ffd7af; }
+.term-fgx224 { color: #ffd7d7; }
+.term-fgx225 { color: #ffd7ff; }
+.term-fgx226 { color: #ffff00; }
+.term-fgx227 { color: #ffff5f; }
+.term-fgx228 { color: #ffff87; }
+.term-fgx229 { color: #ffffaf; }
+.term-fgx230 { color: #ffffd7; }
+.term-fgx231 { color: #ffffff; }
+.term-fgx232 { color: #080808; }
+.term-fgx233 { color: #121212; }
+.term-fgx234 { color: #1c1c1c; }
+.term-fgx235 { color: #262626; }
+.term-fgx236 { color: #303030; }
+.term-fgx237 { color: #3a3a3a; }
+.term-fgx238 { color: #444444; }
+.term-fgx239 { color: #4e4e4e; }
+.term-fgx240 { color: #585858; }
+.term-fgx241 { color: #626262; }
+.term-fgx242 { color: #6c6c6c; }
+.term-fgx243 { color: #767676; }
+.term-fgx244 { color: #808080; }
+.term-fgx245 { color: #8a8a8a; }
+.term-fgx246 { color: #949494; }
+.term-fgx247 { color: #9e9e9e; }
+.term-fgx248 { color: #a8a8a8; }
+.term-fgx249 { color: #b2b2b2; }
+.term-fgx250 { color: #bcbcbc; }
+.term-fgx251 { color: #c6c6c6; }
+.term-fgx252 { color: #d0d0d0; }
+.term-fgx253 { color: #dadada; }
+.term-fgx254 { color: #e4e4e4; }
+.term-fgx255 { color: #eeeeee; }
diff --git a/src/slides.qmd b/src/slides.qmd
index 8753614..dda7f20 100644
--- a/src/slides.qmd
+++ b/src/slides.qmd
@@ -44,6 +44,8 @@ execute:
{{< include ../exercises/automation_exercises.qmd >}}
{{< include testing.qmd >}}
+{{< include _pytest.qmd >}}
+{{< include ../exercises/testing_exercises.qmd >}}
{{< include documentation.qmd >}}
{{< include ../exercises/documentation_exercises.qmd >}}
diff --git a/src/testing.qmd b/src/testing.qmd
index 0221e71..d3f1c43 100644
--- a/src/testing.qmd
+++ b/src/testing.qmd
@@ -24,6 +24,9 @@
## A very short intro to testing
+:::{.columns}
+::::{.column width=50%}
+
### Unit tests
- Test the smallest logical unit of the code
@@ -31,6 +34,9 @@
- Test functions for known results
- Compare to previously produced results
+::::
+::::{.column width=50%}
+
### Integration tests
- Test that components work together
@@ -38,6 +44,9 @@
- Can use previous results to validate model
- Ensure no regression of results
+::::
+:::
+
---
## Adding tests to a project