From bbe8895b68c7e279ee3dd7712f7bea6db404df80 Mon Sep 17 00:00:00 2001 From: gcroci2 Date: Wed, 27 Mar 2024 13:44:53 +0100 Subject: [PATCH 1/8] customize config.yaml for testing --- config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config.yaml b/config.yaml index b3bfd79..b5eced5 100644 --- a/config.yaml +++ b/config.yaml @@ -8,16 +8,16 @@ # lc: Library Carpentry # cp: Carpentries (to use for instructor training for instance) # incubator: The Carpentries Incubator -carpentry: 'incubator' +carpentry: 'incubator' # FIXME # Overall title for pages. -title: 'Lesson Title' # FIXME +title: 'Testing' # Date the lesson was created (YYYY-MM-DD, this is empty by default) -created: ~ # FIXME +created: '2024-03-27' # Comma-separated list of keywords for the lesson -keywords: 'software, data, lesson, The Carpentries' # FIXME +keywords: 'testing, pytest, unit test, integration test' # Life cycle stage of the lesson # possible values: pre-alpha, alpha, beta, stable @@ -27,13 +27,13 @@ life_cycle: 'pre-alpha' # FIXME license: 'CC-BY 4.0' # Link to the source repository for this lesson -source: 'https://github.com/carpentries/workbench-template-md' # FIXME +source: 'https://github.com/esciencecenter-digital-skills/good-practices-lesson' # Default branch of your lesson branch: 'main' # Who to contact if there are any issues -contact: 'team@carpentries.org' # FIXME +contact: 'training@esciencecenter.nl' # Navigation ------------------------------------------------ # From 53e2d8f1c11200f68f3ae153896a02c1c7d91875 Mon Sep 17 00:00:00 2001 From: gcroci2 Date: Wed, 27 Mar 2024 14:35:20 +0100 Subject: [PATCH 2/8] improve config.yaml --- config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yaml b/config.yaml index b5eced5..06b503b 100644 --- a/config.yaml +++ b/config.yaml @@ -11,13 +11,13 @@ carpentry: 'incubator' # FIXME # Overall title for pages. -title: 'Testing' +title: 'Good Practices in Python' # Date the lesson was created (YYYY-MM-DD, this is empty by default) created: '2024-03-27' # Comma-separated list of keywords for the lesson -keywords: 'testing, pytest, unit test, integration test' +keywords: 'Modular code, documentation, testing, continuous integration' # Life cycle stage of the lesson # possible values: pre-alpha, alpha, beta, stable From 2d8a4b56df121a0a49c3bb27979735dc60aefbe8 Mon Sep 17 00:00:00 2001 From: gcroci2 Date: Wed, 27 Mar 2024 16:58:52 +0100 Subject: [PATCH 3/8] add testing.md --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 06b503b..b60d59f 100644 --- a/config.yaml +++ b/config.yaml @@ -59,7 +59,7 @@ contact: 'training@esciencecenter.nl' # Order of episodes in your lesson episodes: -- introduction.md +- testing.md # Information for Learners learners: From c1c3a003ef16058244f50f5a537c6b9937cb6247 Mon Sep 17 00:00:00 2001 From: gcroci2 Date: Wed, 27 Mar 2024 17:00:34 +0100 Subject: [PATCH 4/8] add testing episode --- episodes/introduction.md | 114 --------------- episodes/testing.md | 295 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+), 114 deletions(-) delete mode 100644 episodes/introduction.md create mode 100644 episodes/testing.md diff --git a/episodes/introduction.md b/episodes/introduction.md deleted file mode 100644 index 7065d23..0000000 --- a/episodes/introduction.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: "Using Markdown" -teaching: 10 -exercises: 2 ---- - -:::::::::::::::::::::::::::::::::::::: questions - -- How do you write a lesson using Markdown and `{sandpaper}`? - -:::::::::::::::::::::::::::::::::::::::::::::::: - -::::::::::::::::::::::::::::::::::::: objectives - -- Explain how to use markdown with The Carpentries Workbench -- Demonstrate how to include pieces of code, figures, and nested challenge blocks - -:::::::::::::::::::::::::::::::::::::::::::::::: - -## Introduction - -This is a lesson created via The Carpentries Workbench. It is written in -[Pandoc-flavored Markdown](https://pandoc.org/MANUAL.txt) for static files and -[R Markdown][r-markdown] for dynamic files that can render code into output. -Please refer to the [Introduction to The Carpentries -Workbench](https://carpentries.github.io/sandpaper-docs/) for full documentation. - -What you need to know is that there are three sections required for a valid -Carpentries lesson: - - 1. `questions` are displayed at the beginning of the episode to prime the - learner for the content. - 2. `objectives` are the learning objectives for an episode displayed with - the questions. - 3. `keypoints` are displayed at the end of the episode to reinforce the - objectives. - -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: instructor - -Inline instructor notes can help inform instructors of timing challenges -associated with the lessons. They appear in the "Instructor View" - -:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - -::::::::::::::::::::::::::::::::::::: challenge - -## Challenge 1: Can you do it? - -What is the output of this command? - -```r -paste("This", "new", "lesson", "looks", "good") -``` - -:::::::::::::::::::::::: solution - -## Output - -```output -[1] "This new lesson looks good" -``` - -::::::::::::::::::::::::::::::::: - - -## Challenge 2: how do you nest solutions within challenge blocks? - -:::::::::::::::::::::::: solution - -You can add a line with at least three colons and a `solution` tag. - -::::::::::::::::::::::::::::::::: -:::::::::::::::::::::::::::::::::::::::::::::::: - -## Figures - -You can use standard markdown for static figures with the following syntax: - -`![optional caption that appears below the figure](figure url){alt='alt text for -accessibility purposes'}` - -![You belong in The Carpentries!](https://raw.githubusercontent.com/carpentries/logo/master/Badge_Carpentries.svg){alt='Blue Carpentries hex person logo with no text.'} - -::::::::::::::::::::::::::::::::::::: callout - -Callout sections can highlight information. - -They are sometimes used to emphasise particularly important points -but are also used in some lessons to present "asides": -content that is not central to the narrative of the lesson, -e.g. by providing the answer to a commonly-asked question. - -:::::::::::::::::::::::::::::::::::::::::::::::: - - -## Math - -One of our episodes contains $\LaTeX$ equations when describing how to create -dynamic reports with {knitr}, so we now use mathjax to describe this: - -`$\alpha = \dfrac{1}{(1 - \beta)^2}$` becomes: $\alpha = \dfrac{1}{(1 - \beta)^2}$ - -Cool, right? - -::::::::::::::::::::::::::::::::::::: keypoints - -- Use `.md` files for episodes when you want static content -- Use `.Rmd` files for episodes when you need to generate output -- Run `sandpaper::check_lesson()` to identify any issues with your lesson -- Run `sandpaper::build_lesson()` to preview your lesson locally - -:::::::::::::::::::::::::::::::::::::::::::::::: - -[r-markdown]: https://rmarkdown.rstudio.com/ diff --git a/episodes/testing.md b/episodes/testing.md new file mode 100644 index 0000000..b1c06ac --- /dev/null +++ b/episodes/testing.md @@ -0,0 +1,295 @@ +--- +title: Testing +teaching: 35 +exercises: 25 +--- + +:::::::::::::::::::::::::::::::::::::: questions + +- Why test? + +:::::::::::::::::::::::::::::::::::::::::::::::: + +::::::::::::::::::::::::::::::::::::: objectives + +- Understand the place of testing in a scientific workflow. +- Understand that testing has many forms. + +:::::::::::::::::::::::::::::::::::::::::::::::: + +## Basics of testing + +The first step toward getting the right answers from our programs is to assume +that mistakes *will* happen and to guard against them. This is called +**defensive programming** and the most common way to do it is to add alarms and +tests into our code so that it checks itself. + +**Testing** should be a seamless part of scientific software development process. +This is analogous to experiment design in the experimental science world: + +- At the beginning of a new project, tests can be used to help guide the + overall architecture of the project. +- The act of writing tests can help clarify how the software should be perform when you are done. +- In fact, starting to write the tests _before_ you even write the software + might be advisable. Such a practice is called _test-driven development_. + +## Tests types + +There are many ways to test software, such as: + +- Assertions +- Exceptions +- Unit Tests +- Integration Tests + +*Exceptions and Assertions*: While writing code, `exceptions` and `assertions` +can be added to sound an alarm as runtime problems come up. These kinds of +tests, are embedded in the software itself and handle, as their name implies, +exceptional cases rather than the norm. + +*Unit Tests*: Unit tests investigate the behavior of units of code (such as +functions, classes, or data structures), ideally the smallest possible units. +By validating each software unit across the valid range of its input and output +parameters, tracking down unexpected behavior that may appear when the units are +combined is made vastly simpler. + +*Integration Tests*: Integration tests check that various pieces of the +software work together as expected. They can be both on small scales, or system wide. + +::::::::::::::::::::::::::::::::::::: callout + +### How much testing is enough? + +Possible tests metrics are: + +- Lines of code. +- Test coverage [example](https://sonarcloud.io/component_measures?id=eWaterCycle_ewatercycle&metric=coverage&view=treemap&selected=eWaterCycle_ewatercycle%3Aewatercycle): to check to which extent the software is being coverted by the tests. + +:::::::::::::::::::::::::::::::::::::::::::::::: + +## PyTest + +Currently, [PyTest](docs.pytest.org) is the recommended Python testing framework. Let's see how it can be used to run the tests. + +First, create a directory and navigate into it: + +```bash +mkdir pytest-example +cd pytest-example +``` + +Then, create a file `example.py` containing an example test. You can use for favourite text editor for creating the file: + +```bash +nano example.py +``` + +And then, in the file, type: + +```python +def add(a, b): + return a + b + + +def test_add(): # Special name! + assert add(2, 3) == 5 # What's `assert`? 🤔 + assert add('space', 'ship') == 'spaceship' + +``` + +::::::::::::::::::::::::::::::::::::: callout + +### What's assert? + +Assertions are the simplest type of test. They are used as a tool for bounding acceptable behavior during runtime. The assert keyword in python has the following behavior: + +```python +assert 1==1 # passes +assert 1==2 # throws error: +``` + +```output +Traceback (most recent call last): + File "", line 1, in +AssertionError +``` + +That is, assertions raise an `AssertionError` if the comparison is false. It does nothing at all if the comparison is true. Assertions are therefore a simple way of writing tests. + +```python +assert mean([1,2,3]) == 2 +``` + +:::::::::::::::::::::::::::::::::::::::::::::::: + +Activate your conda environment: + +```bash +conda activate goodpractices +``` + +Check the version of `pytest`: + +```bash +pytest --version +``` + +Finally, run the test: + +```output +pytest example.py +======================== test session starts ======================== +platform linux -- Python 3.6.9, pytest-7.0.1, pluggy-1.0.0 +rootdir: /home/ole/Desktop/pytest-texample +collected 1 item + +example.py . [100%] + +========================= 1 passed in 0.00s ========================= + +``` + +When `pytest` is run, it will search all directories below where it was called, find all of the Python files in these directories whose names start or end with `test`, import them, and run all of the functions and classes whose names start with `test` or `Test`. This automatic registration of test code saves tons of human time and allows us to focus on what is important: writing more tests. + +When you run `pytest`, it will print a dot (`.`) on the screen for every test that passes, an `F` for every test that fails or where there was an unexpected error. The tests pass when they do not throw errors. In rarer situations you may also see an `s` indicating a skipped tests (because the test is not applicable on your system) or a `x` for a known failure (because the developers fixed the problem shown in the test). After the dots, pytest will print summary information. + +::::::::::::::::::::::::::::::::::::: callout + +### Tests collection + +If you do `pytest dir`, the `pytest` package ‘sniffs-out’ the tests in the directory and ran them together to produce a report of the sum of the files and functions matching the regular expression `[Tt]est[-_]*`. + +:::::::::::::::::::::::::::::::::::::::::::::::: + +What happens if we break the test on purpose? + +```python +def add(a, b): + return a - b # Uh oh, mistake! 😱 + + +def test_add(): + assert add(2, 3) == 5 + assert add('space', 'ship') == 'spaceship' +``` + +Let's save the edits and run `pytest` again: + +```output +======================== test session starts ========================= +platform linux -- Python 3.6.9, pytest-7.0.1, pluggy-1.0.0 +rootdir: /home/ole/Desktop/pytest-texample +collected 1 item + +example.py F [100%] + +============================== FAILURES ============================== +______________________________ test_add ______________________________ + + def test_add(): +> assert add(2, 3) == 5 +E assert -1 == 5 +E + where -1 = add(2, 3) + +example.py:6: AssertionError +``` + +You can notice that functions fail on the first error, but **all test functions are executed**. + +::::::::::::::::::::::::::::::::::::: callout + +### Pure vs impure functions + +**Pure functions** are deterministic, have a return value, have no side effects[1], and have referential transparency[2]. Thus, pure functions are easy to understand and test. + +[1] Side effects: interactions of a function with its surroundings. +[2] Replacing a function call with the return of that function should not change anything. + +Examples of pure functions: + +```python +def last(my_array): + return my_array[-1] +  +def add(a, b): + return a + b +``` + +On the other hand, impure functions can be both intuitive: + +```python +my_list = [] +  +def append_to_my_list(item): + my_list.append(item) +  +  +def read_data(file_name): + return pd.read_csv(file_name) +  +  +def get_random_number(number): + return random.random() +``` + +And not so intuitive: + +```python +def hello(name): + print("Hello", name) +  +  +nums = [1, 2] +  +def append(a_list, item): + a_list += [item] + return a_list +  +print(nums) # [1, 2] +print(append(nums, 3)) # [1, 2, 3] +print(nums) # [1, 2, 3] 😬 +``` + +Some side effects can be indeed necessary or hard to spot. + +Use pure functions when possible 👌 + +:::::::::::::::::::::::::::::::::::::::::::::::: + +::::::::::::::::::::::::::::::::::::: challenge + +## Test-Driven Development: FizzBuzz Function (15 min) + +The function `fizz_buzz()` takes an integer argument and returns it, BUT: + +- Fails on zero or negative numbers. +- Instead returns "Fizz" on multiples of 3. +- Instead returns "Buzz" on multiples of 5. +- Instead returns "FizzBuzz" on multiples of 3 and 5. + +Create an empty function `fizz_buzz()` and go through the conditions listed above, one by one: + +1. Write a test for the condition. +2. Edit the `fizz_buzz()` function until the test passes. + +Then discuss together the different solutions. + +:::::::::::::::::::::::: solution + +## Solution + +Add here the preferred solution. We don't have a ready one. + +::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::: + +::::::::::::::::::::::::::::::::::::: keypoints + +- Tests are meant for preserving functionality by detecting new errors early and facilitating reproducibility for research software. +- Tests can help users in verifying correct installation and improving correctness for research output. +- Tests enable developers to make refactoring easier and simplify external contributions. +- Test-Driven Development (TDD) is an optional tool in your toolbox. + +:::::::::::::::::::::::::::::::::::::::::::::::: + +[r-markdown]: https://rmarkdown.rstudio.com/ From 5fd5e7f6e6302e350d4e4fff2081e074e01a5175 Mon Sep 17 00:00:00 2001 From: Giulia Crocioni <55382553+gcroci2@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:20:11 +0100 Subject: [PATCH 5/8] Update config.yaml Co-authored-by: Sven van der Burg --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index b60d59f..e70f14c 100644 --- a/config.yaml +++ b/config.yaml @@ -11,7 +11,7 @@ carpentry: 'incubator' # FIXME # Overall title for pages. -title: 'Good Practices in Python' +title: 'Good practices in research software development' # Date the lesson was created (YYYY-MM-DD, this is empty by default) created: '2024-03-27' From c2e53adcf9d61a19720b0e1b8c2e7de9f583cb2d Mon Sep 17 00:00:00 2001 From: Giulia Crocioni <55382553+gcroci2@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:20:25 +0100 Subject: [PATCH 6/8] Update episodes/testing.md Co-authored-by: Sven van der Burg --- episodes/testing.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/episodes/testing.md b/episodes/testing.md index b1c06ac..1e15d20 100644 --- a/episodes/testing.md +++ b/episodes/testing.md @@ -6,7 +6,8 @@ exercises: 25 :::::::::::::::::::::::::::::::::::::: questions -- Why test? +- Why should I write automated tests for my code? +- How do I write a good unit test? :::::::::::::::::::::::::::::::::::::::::::::::: From a0f4e93daec7bf87fb23728122038c95b8dac4a1 Mon Sep 17 00:00:00 2001 From: Giulia Crocioni <55382553+gcroci2@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:20:34 +0100 Subject: [PATCH 7/8] Update episodes/testing.md Co-authored-by: Sven van der Burg --- episodes/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/episodes/testing.md b/episodes/testing.md index 1e15d20..ff0edab 100644 --- a/episodes/testing.md +++ b/episodes/testing.md @@ -13,7 +13,7 @@ exercises: 25 ::::::::::::::::::::::::::::::::::::: objectives -- Understand the place of testing in a scientific workflow. +- Use `pytest` to write and run unit tests - Understand that testing has many forms. :::::::::::::::::::::::::::::::::::::::::::::::: From e590289588fb1006435dde28ec66a26c4ae284d5 Mon Sep 17 00:00:00 2001 From: Giulia Crocioni <55382553+gcroci2@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:20:51 +0100 Subject: [PATCH 8/8] Update episodes/testing.md Co-authored-by: Sven van der Burg --- episodes/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/episodes/testing.md b/episodes/testing.md index ff0edab..a250dbb 100644 --- a/episodes/testing.md +++ b/episodes/testing.md @@ -279,7 +279,7 @@ Then discuss together the different solutions. ## Solution -Add here the preferred solution. We don't have a ready one. +We are still working on example code for a solution. But as long as you added tests for all the different conditions and your final `fizz_buzz()` function passes all of them your solution is correct. ::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::