Why?
What?
If I left you alone right now, how would you check if this function works correctly?
def get_even(nums):
evens = []
for number in nums:
if number % 2 == 0:
evens.append(number)
return evens
even_testing.py
Would you do something like this?
def get_even(nums):
evens = []
for number in nums:
if number % 2 == 0:
evens.append(number)
return evens
print(get_event([1,2,3]))
print(get_event([4,5,6]))
print(get_event([7]))
even_testing.py
Or something like this?
def get_even(nums):
evens = []
for number in nums:
if number % 2 == 0:
evens.append(number)
return evens
if get_event([1,2,3]) != [2]:
print("Doesn't work 1")
if get_event([4,5,6]) != [4,6]:
print("Doesn't work 2")
if get_event([7]) != []:
print("Doesn't work 3")
even_testing.py
Printing errors or visually inspecting output is a method of debugging not testing. You can't call something a testing method if it doesn't scale well.If I left you alone right now, how would you check if this function works correctly?
def get_evens(nums):
evens = []
for number in nums:
if number % 2 == 0:
evens.append(number)
return evens
assert(get_evens([1,2,3]) == [2])
assert(get_evens([4,5,6]) == [4,6])
assert(get_evens([7]) == [])
even_assert.py
Python has an assert function built-in that will cause an error if what it's provided is not true.
Abstraction is the notion of focusing on a higher level understanding of the problem and not worrying about the underlying detail.
We do this all the time when we drive a car, use our computers, order something online. You're typically focused on expressing an input and wanting an output, with little regard for how you get that output.
When we look at systems in an abstract way we could also say that we're treating them like black boxes.
When we're testing our code, we always want to view the functions we're testing as abstractions / black boxes.
Let's try and write some tests
# Returns a new string with vowels removed
def remove_vowels(string):
pass
# Calculates the factorial of a number
def factorial(num):
pass
blackbox.py
What do we notice when writing these tests?
# Returns a new string with vowels removed
def remove_vowels(string):
pass
# Calculates the factorial of a number
def factorial(num):
pass
assert(remove_vowels("abcde") == "bcd")
assert(remove_vowels("frog") == "frg")
assert(factorial(3) == 6)
assert(factorial(5) == 120)
blackbox_testing.py
When we're testing or implementing a function, we will typically be working with information that tells us the constraints placed on at least the inputs.
The documentation can come in a variety of forms.
This information tells us what we do and don't need to worry about when writing tests.
# Returns a new string with vowels removed
# Input is a non-empty string type
# Return type is another string
def remove_vowels(string):
pass
# Calculates the factorial of a number
# Input is a number between 1 and 10
# Output is a positive number
def factorial(num):
pass
blackbox_contract.py
Let's take a look at pytest
What is pytest?
def sum(x, y):
return x * y
def test_sum1():
assert sum(1, 2) == 3
test_sum1()
$ python3 test1_nopytest.py
test1_nopytest.py
def sum(x, y):
return x * y
def test_sum1():
assert sum(1, 2) == 3, "1 + 2 == 3"
$ pytest test1_pytest.py
test1_pytest.py
import pytest
def sum(x, y):
return x + y
def test_small():
assert sum(1, 2) == 3, "1, 2 == "
assert sum(3, 5) == 8, "3, 5 == "
assert sum(4, 9) == 13, "4, 9 == "
def test_small_negative():
assert sum(-1, -2) == -3, "-1, -2 == "
assert sum(-3, -5) == -8, "-3, -5 == "
assert sum(-4, -9) == -13, "-4, -9 == "
def test_large():
assert sum(84*52, 99*76) == 84*52 + 99*76, "84*52, 99*76 == "
assert sum(23*98, 68*63) == 23*98 + 68*63, "23*98, 68*63 == "
A more complicated test
test_multiple.py
If you just run
$ pytest
It will automatically look for any files in that directory in the shape:
And then any functions that are prefixed with test_ in those files will be run
You can run specific functions within your test files with the -k command. For example, we if want to run the following:
We could run
$ pytest -k small
or try
$ pytest -k small -v
We can also use a range of decorators to specify tests in python:
import pytest
def pointchange(point, change):
x, y = point
x += change
y += change
return (x, y)
@pytest.fixture
def supply_point():
return (1, 2)
@pytest.mark.up
def test_1(supply_point):
assert pointchange(supply_point, 1) == (2, 3)
@pytest.mark.up
def test_2(supply_point):
assert pointchange(supply_point, 5) == (6, 7)
@pytest.mark.up
def test_3(supply_point):
assert pointchange(supply_point, 100) == (101, 102)
@pytest.mark.down
def test_4(supply_point):
assert pointchange(supply_point, -5) == (-4, -3)
@pytest.mark.skip
def test_5(supply_point):
assert False == True, "This test is skipped"
@pytest.mark.xfail
def test_6(supply_point):
assert False == True, "This test's output is muted"
There are a number of tutorials online for pytest. This is a very straightforward one.
import mymath
def test_sum1():
assert mymath.sum(1, 2) == 3, "1 + 2 == 3"
$ pytest
mymath_test.py
def sum(x, y):
return x * y
mymath.py
Whilst importing is covered in week 2, it's worth mentioning at a high level now for the project.
For the major project, your tests and implementation will be separated in different files. So writing tests will consist of importing other files to use them.