diff --git a/.gitignore b/.gitignore index 600d2d3..47064d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +**__pycache__** \ No newline at end of file diff --git a/chap11_testing.md b/chap11_testing.md new file mode 100644 index 0000000..952dd16 --- /dev/null +++ b/chap11_testing.md @@ -0,0 +1,173 @@ +# Chapter 11. Testing Your Code + +Learn how to test code using Python's `unittest` module. + +## Testing a Function + +Function to be tested as shown below. + +```python +def get_formatted_name(first, last): + """Generate a neatly formatted full name.""" + full_name = f"{first} {last}" + return full_name.title() +``` + +A script that should use `get_formatted_name` + +```python +from name_function import get_formatted_name + +print("Enter 'q' at any time to quit.") +while True: + first = input("\nPlease give me a first name: ") + if first == 'q': + break + last = input("Please give me a last name: ") + if last == 'q': + break + + formatted_name = get_formatted_name(first, last) + print(f"\tNeatly formatted name: {formatted_name}.") +``` + +To make sure `get_formatted_name` function properly, using `unittest` module of Python + +```python +import unittest +from name_function import get_formatted_name + +class NamesTestCase(unittest.TestCase): + """Test for name_function.py""" + + def test_first_last_name(self): + """Do names like 'Janis Joplin' work?""" + formatted_name = get_formatted_name('janis', 'joplin') + self.assertEqual(formatted_name, 'Janis Joplin') + +if __name__ == '__main__': + unittest.main() +``` + +Key points: + +* Import `unittest` module as shown +* Import function to be tested +* Here, `unittest`'s most useful feature `assert` is used. `assertEqual` can compare two objects +* `__name__` is a **special variable**, which is set when the program is executed. If the file is being run as the main program, the value of `__name__` is set to `'__main__'` + +Result of running this test: + +``` +. +---------------------------------------------------------------------- +Ran 1 test in 0.000s + +OK +``` + +* First dot represent the single test passed. + +### A Failing Test + +Failure case returns + +``` +F +====================================================================== +FAIL: test_first_last_name (__main__.NamesTestCase) +Do names like 'Janis Joplin' work? +---------------------------------------------------------------------- +Traceback (most recent call last): + File "test_name_function.py", line 10, in test_first_last_name + self.assertEqual(formatted_name, 'Janis Joplins') +AssertionError: 'Janis Joplin' != 'Janis Joplins' +- Janis Joplin ++ Janis Joplins +? + + + +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (failures=1) +``` + +Highlight: + +* `FAIL: test_first_last_name` tell's where the error coming from. + +## Fix them + +If employing TDD, we can first create test case, then develop code to be test against. + +## Testing a Class + +e.g. above shown how to write test case for single function, we also need to write test for class. + +### A Variety of Assert Methods + +Python `unittest` module provide various assert methods in `unittest.TestCase` class. + +List of available assert methods + +| Method | Use | +| ------------------------- | -------------------------------- | +| `assertEqual(a,b)` | Verify `a == b` | +| `assertNotEqual(a,b)` | Verify that `a != b` | +| `assertTrue(x)` | Verify that x is True | +| `assertFalse(x)` | Verify that x is False | +| `assertIn(item, list)` | Verify that item is in list | +| `assertNotIn(item, list)` | Verrify that item is not in list | + +### A Class to Test + +* class to be tested in *survey.py* have class `AnonymousSurvey` + +```python +class AnonymousSurvey: + """Collect anonymous answers to a survey questions""" + + def __init__(self, question): + """Store a question, and prepare to store response.""" + self.question = question + self.responses = [] + + def show_question(self): + """Show the survey question""" + print(self.question) + + def store_response(self, new_response): + """Store a single response to the survey""" + self.responses.append(new_response) + + def show_results(self): + """Show all the responses that have been given""" + print("Survey results:") + for response in self.responses: + print(f"- {response}") +``` +* script to use `AnonymousSurvey` *language_survey.py* + +```python +from survey import AnonymousSurvey + +# Define a question, and make a survey. +question = "What language did you first learn to speak?" +my_survey = AnonymousSurvey(question) + +# Show the questions, and store responses to the question +my_survey.show_question() +print("Enter 'q' at any time to quit.\n") +while True: + response = input("Language: ") + if response == 'q': + break + my_survey.store_response(response) + +# Show the survey results +print("\nThank you to everyone who participate in the survey") +my_survey.show_results() +``` + +If we want to \ No newline at end of file diff --git a/src/chap11/language_survey.py b/src/chap11/language_survey.py new file mode 100644 index 0000000..57978fc --- /dev/null +++ b/src/chap11/language_survey.py @@ -0,0 +1,18 @@ +from survey import AnonymousSurvey + +# Define a question, and make a survey. +question = "What language did you first learn to speak?" +my_survey = AnonymousSurvey(question) + +# Show the questions, and store responses to the question +my_survey.show_question() +print("Enter 'q' at any time to quit.\n") +while True: + response = input("Language: ") + if response == 'q': + break + my_survey.store_response(response) + +# Show the survey results +print("\nThank you to everyone who participate in the survey") +my_survey.show_results() \ No newline at end of file diff --git a/src/chap11/name_function.py b/src/chap11/name_function.py new file mode 100644 index 0000000..045b966 --- /dev/null +++ b/src/chap11/name_function.py @@ -0,0 +1,4 @@ +def get_formatted_name(first, last): + """Generate a neatly formatted full name""" + full_name = f"{first} {last}" + return full_name.title() \ No newline at end of file diff --git a/src/chap11/names.py b/src/chap11/names.py new file mode 100644 index 0000000..57229dd --- /dev/null +++ b/src/chap11/names.py @@ -0,0 +1,13 @@ +from name_function import get_formatted_name + +print("Enter 'q' at any time to quit.") +while True: + first = input("\nPlease give me a first name: ") + if first == 'q': + break + last = input("Please give me a last name: ") + if last == 'q': + break + + formatted_name = get_formatted_name(first, last) + print(f"\tNeatly formatted name: {formatted_name}.") \ No newline at end of file diff --git a/src/chap11/survey.py b/src/chap11/survey.py new file mode 100644 index 0000000..58e15fb --- /dev/null +++ b/src/chap11/survey.py @@ -0,0 +1,21 @@ +class AnonymousSurvey: + """Collect anonymous answers to a survey questions""" + + def __init__(self, question): + """Store a question, and prepare to store response.""" + self.question = question + self.responses = [] + + def show_question(self): + """Show the survey question""" + print(self.question) + + def store_response(self, new_response): + """Store a single response to the survey""" + self.responses.append(new_response) + + def show_results(self): + """Show all the responses that have been given""" + print("Survey results:") + for response in self.responses: + print(f"- {response}") \ No newline at end of file diff --git a/src/chap11/test_name_function.py b/src/chap11/test_name_function.py new file mode 100644 index 0000000..b6f700e --- /dev/null +++ b/src/chap11/test_name_function.py @@ -0,0 +1,13 @@ +import unittest +from name_function import get_formatted_name + +class NamesTestCase(unittest.TestCase): + """Test for name_function.py""" + + def test_first_last_name(self): + """Do names like 'Janis Joplin' work?""" + formatted_name = get_formatted_name('janis', 'joplin') + self.assertEqual(formatted_name, 'Janis Joplins') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/src/chap11/test_survey.py b/src/chap11/test_survey.py new file mode 100644 index 0000000..f567d06 --- /dev/null +++ b/src/chap11/test_survey.py @@ -0,0 +1,15 @@ +import unittest +from survey import AnonymousSurvey + +class TestAnonymousSurvey(unittest.TestCase): + """Tests for the class AnonymouseSurvey""" + + def test_store_single_response(self): + """Tests that a single response is stored properly""" + question = "What language did you first learn to speak?" + my_survey = AnonymousSurvey(question) + my_survey.store_response('English') + self.assertIn('English', my_survey.responses) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file