[30m# How to write great unit tests with pytest
In this advanced tutorial for experts, we'll use Python 3.9 and `pytest` to write a suite of unit tests to verify the behavior of the following function.
```python
def is_palindrome(s):
return s == s[::-1]
```
Before writing any unit tests, let's review what each element of the function is doing exactly and what the author's intentions may have been.
- First,[92m we have a function definition. This is where we give the function a name, `is_palindrome`, and specify the arguments that the function accepts. In this case, the function accepts a single string argument, `s`.
- Next, we have a return statement. This is where we specify the value that the function returns. In this case, the function returns `s == s[::-1]`.
- Finally, we have a function call. This is where we actually call the function with a specific set of arguments. In this case, we're calling the function with the string `"racecar"`.[30m
A good unit test suite should aim to:
- Test the function's behavior for a wide range of possible inputs
- Test edge cases that the author may not have foreseen
- Take advantage of the features of `pytest` to make the tests easy to write and maintain
- Be easy to read and understand, with clean code and descriptive names
- Be deterministic, so that the tests always pass or fail in the same way
`pytest` has many convenient features that make it easy to write and maintain unit tests. We'll use them to write unit tests for the function above.
For this particular function, we'll want our unit tests to handle the following diverse scenarios (and under each scenario, we include a few examples as sub-bullets):
-[92m The input is a palindrome
- `"racecar"`
- `"madam"`
- `"anna"`
- The input is not a palindrome
- `"python"`
- `"test"`
- `"1234"`
- The input is an empty string
- `""`
- The input is `None`
- The input is not a string
- `1`
- `1.0`
- `True`
- `False`
- `[]`
- `{}`[30m
In addition to the scenarios above, we'll also want to make sure we don't forget to test rare or unexpected edge cases (and under each edge case, we include a few examples as sub-bullets):
-[92m The input is a palindrome with spaces
- `"race car"`
- `" madam "`
- `" anna "`
- The input is not a palindrome with spaces
- `" python "`
- `" test "`
- `" 1234 "`
- The input is a palindrome with punctuation
- `"racecar!"`
- `"Madam, I'm Adam."`
- `"Anna's"`
- The input is not a palindrome with punctuation
- `"python!"`
- `"test."`
- `"1234!"`
- The input is a palindrome with mixed case
- `"Racecar"`
- `"Madam"`
- `"Anna"`
- The input is not a palindrome with mixed case
- `"Python"`
- `"Test"`
- `"1234"`[30m
Before going into the individual tests, let's first look at the complete suite of unit tests as a cohesive whole. We've added helpful comments to explain what each line does.
```python
import pytest # used for our unit tests
def is_palindrome(s):
return s == s[::-1]
#Below, each test case is represented by a tuple passed to the @pytest.mark.parametrize decorator[92m.
#The first element of the tuple is a name for the test case, and the second element is a list of arguments for the test case.
#The @pytest.mark.parametrize decorator will generate a separate test function for each test case.
#The generated test function will be named test_is_palindrome_<name> where <name> is the name of the test case.
#The generated test function will be given the arguments specified in the list of arguments for the test case.
#The generated test function will be given the fixture specified in the decorator, in this case the function itself.
#The generated test function will call the function with the arguments and assert that the result is equal to the expected value.
@pytest.mark.parametrize(
"name,args,expected",
[
# Test the function's behavior for a wide range of possible inputs
("palindrome", ["racecar"], True),
("palindrome", ["madam"], True),
("palindrome", ["anna"], True),
("non-palindrome", ["python"], False),
("non-palindrome", ["test"], False),
("non-palindrome", ["1234"], False),
("empty string", [""], True),
("None", [None], False),
("non-string", [1], False),
("non-string", [1.0], False),
("non-string", [True], False),
("non-string", [False], False),
("non-string", [[]], False),
("non-string", [{}], False),
# Test edge cases that the author may not have foreseen
("palindrome with spaces", ["race car"], True),
("palindrome with spaces", [" madam "], True),
("palindrome with spaces", [" anna "], True),
("non-palindrome with spaces", [" python "], False),
("non-palindrome with spaces", [" test "], False),
("non-palindrome with spaces", [" 1234 "], False),
("palindrome with punctuation", ["racecar!"], True),
("palindrome with punctuation", ["Madam, I'm Adam."], True),
("palindrome with punctuation", ["Anna's"], True),
("non-palindrome with punctuation", ["python!"], False),
("non-palindrome with punctuation", ["test."], False),
("non-palindrome with punctuation", ["1234!"], False),
("palindrome with mixed case", ["Racecar"], True),
("palindrome with mixed case", ["Madam"], True),
("palindrome with mixed case", ["Anna"], True),
("non-palindrome with mixed case", ["Python"], False),
("non-palindrome with mixed case", ["Test"], False),
("non-palindrome with mixed case", ["1234"], False),
],
)
def test_is_palindrome(is_palindrome, args, expected):
assert is_palindrome(*args) == expected
'.\n#The first element of the tuple is a name for the test case, and the second element is a list of arguments for the test case.\n#The @pytest.mark.parametrize decorator will generate a separate test function for each test case.\n#The generated test function will be named test_is_palindrome_<name> where <name> is the name of the test case.\n#The generated test function will be given the arguments specified in the list of arguments for the test case.\n#The generated test function will be given the fixture specified in the decorator, in this case the function itself.\n#The generated test function will call the function with the arguments and assert that the result is equal to the expected value.\n@pytest.mark.parametrize(\n "name,args,expected",\n [\n # Test the function\'s behavior for a wide range of possible inputs\n ("palindrome", ["racecar"], True),\n ("palindrome", ["madam"], True),\n ("palindrome", ["anna"], True),\n ("non-palindrome", ["python"], False),\n ("non-palindrome", ["test"], False),\n ("non-palindrome", ["1234"], False),\n ("empty string", [""], True),\n ("None", [None], False),\n ("non-string", [1], False),\n ("non-string", [1.0], False),\n ("non-string", [True], False),\n ("non-string", [False], False),\n ("non-string", [[]], False),\n ("non-string", [{}], False),\n # Test edge cases that the author may not have foreseen\n ("palindrome with spaces", ["race car"], True),\n ("palindrome with spaces", [" madam "], True),\n ("palindrome with spaces", [" anna "], True),\n ("non-palindrome with spaces", [" python "], False),\n ("non-palindrome with spaces", [" test "], False),\n ("non-palindrome with spaces", [" 1234 "], False),\n ("palindrome with punctuation", ["racecar!"], True),\n ("palindrome with punctuation", ["Madam, I\'m Adam."], True),\n ("palindrome with punctuation", ["Anna\'s"], True),\n ("non-palindrome with punctuation", ["python!"], False),\n ("non-palindrome with punctuation", ["test."], False),\n ("non-palindrome with punctuation", ["1234!"], False),\n ("palindrome with mixed case", ["Racecar"], True),\n ("palindrome with mixed case", ["Madam"], True),\n ("palindrome with mixed case", ["Anna"], True),\n ("non-palindrome with mixed case", ["Python"], False),\n ("non-palindrome with mixed case", ["Test"], False),\n ("non-palindrome with mixed case", ["1234"], False),\n ],\n)\ndef test_is_palindrome(is_palindrome, args, expected):\n assert is_palindrome(*args) == expected\n'