diff --git a/episodes/01-why-test-my-code.Rmd b/episodes/01-why-test-my-code.Rmd index 8ee4886c..c9c75aec 100644 --- a/episodes/01-why-test-my-code.Rmd +++ b/episodes/01-why-test-my-code.Rmd @@ -18,16 +18,22 @@ exercises: 2 ## What is software testing? -Software testing is the process of checking that code is working as expected. You may have data processing functions or automations that you use in your work - how do you know that they are doing what you expect them to do? +Software testing is the process of checking that code is working as expected. +You may have data processing functions or automations that you use in your work. +How do you know that they are doing what you expect them to do? -Software testing is most commonly done by writing code (tests) that check that your code works as expected. +Software testing is most commonly done by writing test code that check that +your code works as expected. -This might seem like a lot of effort, so let's go over some of the reasons you might want to add tests to your project. +This might seem like a lot of effort, so let's go over some of the reasons you +might want to add tests to your project. ## Catching bugs -Whether you are writing the occasional script or developing a large software, mistakes are inevitable. Sometimes you don't even know when a mistake creeps into the code, and it gets published. +Whether you are writing the occasional script or developing a large software, +mistakes are inevitable. Sometimes you don't even know when a mistake creeps +into the code, and it gets published. Consider the following function: @@ -36,22 +42,32 @@ def add(a, b): return a - b ``` -When writing this function, I made a mistake. I accidentally wrote `a - b` instead of `a + b`. This is a simple mistake, but it could have serious consequences in a project. +When writing this function, I made a mistake. I accidentally wrote `a - b` +instead of `a + b`. This is a simple mistake, but it could have serious +consequences in a project. -When writing the code, I could have tested this function by manually trying it with different inputs and checking the output, but: +When writing the code, I could have tested this function by manually trying it +with different inputs and checking the output, but: - This takes time. - I might forget to test it again when we make changes to the code later on. -- Nobody else in my team knows if I tested it, or how I tested it, and therefore whether they can trust it. +- Nobody else in my team knows if I tested it, or how I tested it, and + therefore whether they can trust it. This is where automated testing comes in. ## Automated testing -Automated testing is where we write code that checks that our code works as expected. Every time we make a change, we can run our tests to automatically make sure that our code still works as expected. +Automated testing is where we write code that checks that our code works as +expected. Every time we make a change, we can run our tests to automatically +make sure that our code still works as expected. -If we were writing a test from scratch for the `add` function, think for a moment on how we would do it. -We would need to write a function that runs the `add` function on a set of inputs, checking each case to ensure it does what we expect. Let's write a test for the `add` function and call it `test_add`: +If we were writing a test from scratch for the `add` function, think for a +moment on how we would do it. + +We would need to write a function that runs the `add` function on a set of +inputs, checking each case to ensure it does what we expect. Let's write a test +for the `add` function and call it `test_add`: ```python def test_add(): @@ -66,16 +82,19 @@ def test_add(): print("Test failed!") ``` -Here we check that the function works for a set of test cases. We ensure that it works for positive numbers, negative numbers, and zero. +Here we check that the function works for a set of test cases. We ensure that +it works for positive numbers, negative numbers, and zero. ::::::::::::::::::::::::::::::::::::: challenge -## Challenge 1: What could go wrong? +## What could go wrong? -When writing functions, sometimes we don't anticipate all the ways that they could go wrong. +When writing functions, sometimes we don't anticipate all the ways that they +could go wrong. -Take a moment to think about what is wrong, or might go wrong with these functions: +Take a moment to think about what is wrong, or might go wrong with these +functions: ```python def greet_user(name): @@ -89,11 +108,12 @@ def gradient(x1, y1, x2, y2): :::::::::::::::::::::::: solution -## Answer - -The first function will incorrectly greet the user, as it is missing a space after "Hello". It would print `HelloAlice!` instead of `Hello Alice!`. +The first function will incorrectly greet the user, as it is missing a space +after "Hello". It would print `HelloAlice!` instead of `Hello Alice!`. + +If we wrote a test for this function, we would have noticed that it was not +working as expected: -If we wrote a test for this function, we would have noticed that it was not working as expected: ```python def test_greet_user(): if greet_user("Alice") != "Hello Alice!": @@ -102,7 +122,8 @@ def test_greet_user(): The second function will crash if `x2 - x1` is zero. -If we wrote a test for this function, it may have helped us to catch this unexpected behaviour: +If we wrote a test for this function, it may have helped us to catch this +unexpected behaviour: ```python def test_gradient(): @@ -114,7 +135,7 @@ def test_gradient(): print("Test failed!") ``` -And we could have ammened the function: +And we could have amended the function: ```python def gradient(x1, y1, x2, y2): @@ -129,7 +150,8 @@ def gradient(x1, y1, x2, y2): ## Finding the root cause of a bug -When a test fails, it can help us to find the root cause of a bug. For example, consider the following function: +When a test fails, it can help us to find the root cause of a bug. For example, +consider the following function: ```python @@ -143,45 +165,57 @@ def triangle_area(base, height): return divide(multiply(base, height), 2) ``` -There is a bug in this code too, but since we have several functions calling each other, it is not immediately obvious where the bug is. Also, the bug is not likely to cause a crash, so we won't get a helpful error message telling us what went wrong. If a user happened to notice that there was an error, then we would have to check `triangle_area` to see if the formula we used is right, then `multiply`, and `divide` to see if they were working as expected too! +There is a bug in this code too, but since we have several functions calling +each other, it is not immediately obvious where the bug is. Also, the bug is +not likely to cause a crash, so we won't get a helpful error message telling us +what went wrong. If a user happened to notice that there was an error, then we +would have to check `triangle_area` to see if the formula we used is right, +then `multiply`, and `divide` to see if they were working as expected too! -However, if we had written tests for these functions, then we would have seen that both the `triangle_area` and `multiply` functions were not working as expected, allowing us to quickly see that the bug was in the `multiply` function without having to check the other functions. +However, if we had written tests for these functions, then we would have seen +that both the `triangle_area` and `multiply` functions were not working as +expected, allowing us to quickly see that the bug was in the `multiply` +function without having to check the other functions. ## Increased confidence in code -When you have tests for your code, you can be more confident that it works as expected. This is especially important when you are working in a team or producing software for users, as it allows everyone to trust the code. If you have a test that checks that a function works as expected, then you can be confident that the function will work as expected, even if you didn't write it yourself. +When you have tests for your code, you can be more confident that it works as +expected. This is especially important when you are working in a team or +producing software for users, as it allows everyone to trust the code. If you +have a test that checks that a function works as expected, then you can be +confident that the function will work as expected, even if you didn't write it +yourself. ## Forcing a more structured approach to coding -When you write tests for your code, you are forced to think more carefully about how your code behaves and how you will verify that it works as expected. This can help you to write more structured code, as you will need to think about how to test it as well as how it could fail. +When you write tests for your code, you are forced to think more carefully +about how your code behaves and how you will verify that it works as expected. +This can help you to write more structured code, as you will need to think +about how to test it as well as how it could fail. ::::::::::::::::::::::::::::::::::::: challenge -## Challenge 2: What could go wrong? +## What could go wrong? Consider a function that controls a driverless car. - What checks might we add to make sure it is not dangerous to use? ```python - def drive_car(speed, direction): ... # complex car driving code return speed, direction, brake_status - - ``` :::::::::::::::::::::::: solution -## Answer - We might want to check that the speed is within a safe range. - -- We might want to check that the direction is a valid direction. ie not towards a tree, and if so, the car should be applying the brakes. +- We might want to check that the direction is a valid direction. ie not + towards a tree, and if so, the car should be applying the brakes. ::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::: diff --git a/episodes/02-simple-tests.Rmd b/episodes/02-simple-tests.Rmd index 1f7d0420..c0d2c181 100644 --- a/episodes/02-simple-tests.Rmd +++ b/episodes/02-simple-tests.Rmd @@ -25,12 +25,15 @@ The most basic thing you will want to do in a test is check that an output for a function is correct by checking that it is equal to a certain value. Let's take the `add` function example from the previous chapter and the test we -conceptualised for it and write it in code. +conceptualised for it and write it in code. We'll aim to write the test in such +a way that it can be run using Pytest, the most commonly used testing framework +in Python. -- Make a folder called `my_project` (or whatever you want to call it for these lessons) -and inside it, create a file called 'calculator.py', and another file called 'test_calculator.py'. +- Make a folder called `my_project` (or whatever you want to call it for these + lessons) and inside it, create a file called 'calculator.py', and another + file called 'test_calculator.py'. -So your directory structure should look like this: +Your directory structure should look like this: ```bash project_directory/ @@ -39,8 +42,8 @@ project_directory/ └── test_calculator.py ``` -`calculator.py` will contain our Python functions that we want to test, and `test_calculator.py` -will contain our tests for those functions. +`calculator.py` will contain our Python functions that we want to test, and +`test_calculator.py` will contain our tests for those functions. - In `calculator.py`, write the add function: @@ -49,8 +52,9 @@ def add(a, b): return a + b ``` -- And in `test_calculator.py`, write the test for the add function that we conceptualised -in the previous lesson: +- And in `test_calculator.py`, write the test for the add function that we + conceptualised in the previous lesson, but use the `assert` keyword in place + of if statements and print functions: ```python # Import the add function so the test can use it @@ -58,28 +62,26 @@ from calculator import add def test_add(): # Check that it adds two positive integers - if add(1, 2) != 3: - print("Test failed!") - raise AssertionError("Test failed!") + assert add(1, 2) == 3 # Check that it adds zero - if add(5, 0) != 5: - print("Test failed!") - raise AssertionError("Test failed!") + assert add(5, 0) == 5 # Check that it adds two negative integers - if add(-1, -2) != -3: - print("Test failed!") - raise AssertionError("Test failed!") + assert add(-1, -2) == -3 ``` -(Note that the `AssertionError` is a way to tell Python to crash the test, so Pytest knows that the test has failed.) +The `assert` statement will crash the test by raising an `AssertionError` if +the condition following it is false. Pytest uses these to tell that the test +has failed. This system of placing functions in a file and then tests for those functions in -another file, is a common pattern in software development. It allows you to keep your +another file is a common pattern in software development. It allows you to keep your code organised and separate your tests from your actual code. -With Pytest, the expectation is to name your test functions with the prefix `test_`. +With Pytest, the expectation is to name your test files and functions with the +prefix `test_`. If you do so, Pytest will automatically find and execute each +test function. Now, let's run the test. We can do this by running the following command in the terminal: @@ -89,79 +91,28 @@ Now, let's run the test. We can do this by running the following command in the ❯ pytest ./ ``` -This command tells pytest to run all the tests in the current directory. +This command tells Pytest to run all the tests in the current directory. -When you run the test, you should see that the test runs successfully, indicated -by some **green**. text in the terminal. We will go through the output and what it means -in the next lesson, but for now, know that **green** means that the test passed, and **red** -means that the test failed. +When you run the test, you should see that the test runs successfully, +indicated by some **green**. text in the +terminal. We will go through the output and what it means in the next lesson, +but for now, know that **green** means that +the test passed, and **red** means that the test +failed. -Try changing the `add` function to return the wrong value, and run the test again to see that the test now fails -and the text turns **red** - neat! +Try changing the `add` function to return the wrong value, and run the test +again to see that the test now fails and the text turns **red** - neat! If this was a real testing situation, +we would know to investigate the `add` function to see why it's not behaving as +expected. -## The `assert` keyword - -Writing these `if` blocks for each test case is cumbersome. Fortunately, Python -has a keyword to do this for us - the `assert` keyword. - -The `assert` keyword checks if a statement is true and if it is, the test continues, but -if it isn't, then the test will crash, printing an error in the terminal. This enables us -to write succinct tests without lots of if-statements. - -The `assert` keyword is used like this: - -```python -assert add(1, 2) == 3 -``` - -which is equivalent to: - -```python -if add(1, 2) != 3: - # Crash the test - raise AssertionError -``` - -::::::::::::::::::::::::::::::::::::: challenge - -## Challenge 1: Use the assert keyword to update the test for the add function - -Use the `assert` keyword to update the test for the `add` function to make it more concise and readable. - -Then re-run the test using `pytest ./` to check that it still passes. - -:::::::::::::::::::::::: solution - -```python -from calculator import add - -def test_add(): - assert add(1, 2) == 3 # Check that it adds to positive integers - assert add(5, 0) == 5 # Check that it adds zero - assert add(-1, -2) == -3 # Check that it adds wro negative numbers -``` - -::::::::::::::::::::::::::::::::: - -:::::::::::::::::::::::::::::::::::::::::::::::: - -Now that we are using the `assert` keyword, pytest will let us know if our test fails. - -What's more, is that if any of these assert statements fail, it will flag to -pytest that the test has failed, and pytest will let you know. - - -Make the `add` function return the wrong value, and run the test again to see that the test -fails and the text turns **red** as we expect. - - -So if this was a real testing situation, we would know to investigate the `add` function to see why it's not behaving as expected. ::::::::::::::::::::::::::::::::::::: challenge -## Challenge 2: Write a test for a multiply function +## Write a test for a multiply function -Try using what we have covered to write a test for a `multiply` function that multiplies two numbers together. +Try using what we have covered to write a test for a `multiply` function that +multiplies two numbers together. - Place this multiply function in `calculator.py`: @@ -170,7 +121,9 @@ def multiply(a, b): return a * b ``` -- Then write a test for this function in `test_calculator.py`. Remember to import the `multiply` function from `calculator.py` at the top of the file like this: +- Then write a test for this function in `test_calculator.py`. Remember to + import the `multiply` function from `calculator.py` at the top of the file + like this: ```python from calculator import multiply @@ -178,8 +131,9 @@ from calculator import multiply :::::::::::::::::::::::: solution -## Solution: -There are many different test cases that you could include, but it's important to check that different types of cases are covered. A test for this function could look like this: +There are many different test cases that you could include, but it's important +to check that different types of cases are covered. A test for this function +could look like this: ```python def test_multiply(): @@ -197,11 +151,9 @@ def test_multiply(): :::::::::::::::::::::::::::::::::::::::::::::::: -Run the test using `pytest ./` to check that it passes. If it doesn't, don't worry, that's the point of testing - to find bugs in code. - ::::::::::::::::::::::::::::::::::::: keypoints -- The `assert` keyword is used to check if a statement is true and is a shorthand for writing `if` statements in tests. +- The `assert` keyword is used to check if a statement is true. - Pytest is invoked by running the command `pytest ./` in the terminal. - `pytest` will run all the tests in the current directory, found by looking for files that start with `test_`. - The output of a test is displayed in the terminal, with **green** text indicating a successful test and **red** text indicating a failed test.