" SELECT student.id, avg(class.grade) as average \n",
" FROM students JOIN classes ON students.id = classes.student_id\n",
" GROUP BY student.id HAVING average >= 70\n",
" ''')\n",
" students = c.fetchall()\n",
" for student in students:\n",
" c.execute('UPDATE student_enrollment SET grade = grade + 1 WHERE student_id = ?', (student[0],))\n",
" conn.commit()\n",
" conn.close()\n",
"\n",
"```\n",
"\n",
"How would you test this function? You'd need to have a database with a specific set of data in it and then run the function and check that the data was updated as expected.\n",
"\n",
"If you break the function up into smaller functions, you can test each function in isolation:\n",
"\n",
"```python\n",
"\n",
"def get_students(conn, grade_threshold=70):\n",
" ...\n",
"\n",
"def advance_student(conn, student):\n",
" ...\n",
"\n",
"```\n",
"\n",
"Now you can test each function in isolation.\n",
"\n",
"By having the function take parameters, you can also test the function with different inputs.\n",
"\n",
"It is also possible to test the function with a mock database connection that doesn't actually connect to a database but provides the same interface.\n",
"\n",
"This is called \"mocking\" and is a useful technique for testing code that interacts with external systems.\n",
"\n",
"## `pytest`\n",
"\n",
"`pytest` is a popular testing framework for Python, and the one we've been using in class.\n",
"\n",
"It is easy to use and provides a lot of useful features. Python has a built in `unittest` module, but it is more verbose and less flexible.\n",
"\n",
"`pytest` provides both a command line tool `pytest`, which you've been using, and a library that you can use to help you write tests.\n",
"\n",
"## `pytest` command line tool\n",
"\n",
"When you run `pytest`, it will look for files named `test_*.py` in the current directory and its subdirectories. It will then run any functions in those files that start with `test_`.\n",
"\n",
"If you take a look at any PA, you'll see that there are files named `test_*.py` in the `tests` directory. This is a common convention, but you can also place the files in other directories.\n",
"\n",
"Within each `test_module1.py` file, there are functions that start with `test_`. These are the tests that `pytest` will run. You can include other functions in the file as helper functions, but they won't be run by `pytest`.\n",
"\n",
"### Simple Example\n",
"\n",
"```python\n",
"# my_module.py\n",
"from collections import namedtuple\n",
"\n",
"Point = namedtuple('Point', ['x', 'y'])\n",
"\n",
"def circle_contains(radius: float, center: Point, point: Point):\n",
" assert circle_contains(1, origin, Point(1, 0)) # on the circle\n",
" assert not circle_contains(1, origin, Point(1.1, 0))\n",
"```\n",
"\n",
"If the first assertion fails, the second assertion will not be run. This makes it harder to debug the problem.\n",
"\n",
"As you've no doubt noticed throughout these courses, granular tests make debugging easier. It is also easier to reason about what isn't yet being tested.\n",
"\n",
"**What bugs could be lurking in the code above?**\n",
"\n",
"### General Rules\n",
"\n",
"* Each test should test one thing.\n",
"* Each test should be independent of the others.\n",
"* Each test should be repeatable.\n",
"* Each test should be easy to understand.\n",
"\n",
"### What Tests To Write\n",
"\n",
"When considering what to test, usually there are a couple of obvious cases. For a string distance function, you might consider you need to have at least one test for when the strings are identical, and one test for when the strings are completely different.\n",
"\n",
"Those are the obvious cases, it is then worth considering edge cases. What about an empty string? Perhaps a string with one character as well?\n",
"\n",
"**(0, 1, Many)** is a good rule to consider.\n",
"\n",
"\n",
"### Test One Thing\n",
"\n",
"If you were testing a function that summed a list of numbers, consider these two approaches:\n",
" assert db.get_user(username=\"alice\") is None\n",
"```\n",
"\n",
"Note: `test_delete_user` will fail if `create_user` doesn't work. There's still a dependency in terms of behavior in this case, but you can see that the tests can now be run independently of one another since each starts with a blank database.\n",
"\n",
"#### `pytest` Fixtures\n",
"\n",
"Another way to handle this is to use `pytest` fixtures. A fixture is a function that is run before each test. It can be used to set up the test environment.\n",
" assert db.get_user(username=\"alice\") is None\n",
"```\n",
"\n",
"This is a powerful feature that can be used to set up complex test environments.\n",
"\n",
"### Test Repeatability\n",
"\n",
"Tests should be repeatable. This means that if a test fails, it should be possible to run it again and get the same result.\n",
"\n",
"This means that tests should not depend on external factors such as:\n",
"\n",
"* The current time or date\n",
"* Random numbers\n",
"* The state of the network\n",
"* The state of the database\n",
"\n",
"To reduce the chance of a test failing due to an external factor, you can use a library like `freezegun` to freeze the current time that a test sees. The `mock` module can be used to mock out external functions so they return consistent data for the purpose of the test.\n",
"Tests should be easy to understand. This means that the test should be written in a way that makes it clear what is being tested and what the expected result is.\n",
"\n",
"Make liberal use of comments and descriptive test names to make it clear what is being tested so that when a modification to the code in the future breaks a test, it is easy to understand why.\n",
"\n",
"## Test Driven Devleopment\n",
"\n",
"Test Driven Development (TDD) is a software development process that involves writing tests before writing the code that will be tested.\n",
"\n",
"In many ways this is how your PAs have been structured. By knowing what the expected output is, you can write tests that will fail if the code is not working correctly.\n",
"\n",
"It can be useful to write tests before writing the code that will be tested. This can help you think through the problem and make sure you understand what the code is supposed to do.\n",
"\n",
"## Unit Testing vs. Integration Testing\n",
"\n",
"Unit tests are tests that test individual units of code. These are usually functions or methods.\n",
"\n",
"Integration tests are tests that test how different units of code work together. It can be useful to have a mixture of both, but unit tests are usually easier to write and maintain.\n",
"\n",
"In our initial example, we broke `advance_all_students_with_passing_grade` into smaller functions. It may still make sense in some cases to test the integration of these functions to make sure that (e.g.) the list of users being returned is still in the same format expected by the `advance_student` function.\n",
"\n",
"## `pytest` Features\n",
"\n",
"### `pytest.fixture`\n",
"\n",
"As shown above, `pytest` fixtures can be used to set up the test environment or provide data to tests.\n",