COMP1531

🐶 Software Engineering

1.4 - Testing - Intro

Author: Hayden Smith 2021

In this lecture

Why?

  • Writing tests are critical to ensure an application works
  • Approaching testing the right way will yield better results

What?

  • Abstraction & Black boxes
  • Design by contract
  • Pytest

 

Seeing if your code works

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

Seeing if your code works

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

Seeing if your code works

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

Using python assert

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.

Blackbox Testing & Abstraction

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.

Blackbox Testing & Abstraction

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

Blackbox Testing & Abstraction

What do we notice when writing these tests?

  • The tests are complete, even if they aren't being passed
  • We don't need to know how the function is implemented to test the function
  • Now we can go and implement it, and we have tests already done!
# 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

Design by contract

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

Systematic Python Testing

Let's take a look at pytest

 

What is pytest?

  • A structured method of writing, organising, and running tests
  • pytest is a library that helps us write small tests, but can also be used to write larger and more complex tests
  • pytest comes with a binary that we run on command line
  • pytest detects any function prefixed with test and runs that function, processing the assertions inside

pytest - basic

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

pytest - more complicated

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

pytest - prefixes

If you just run

$ pytest

 

It will automatically look for any files in that directory in the shape:

  • test_*.py
  • *_test.py

 

And then any functions that are prefixed with test_ in those files will be run

pytest - particular files

You can run specific functions within your test files with the -k command. For example, we if want to run the following:

  • test_small
  • test_small_negative
  • test_large

 

We could run

 

$ pytest -k small

or try

$ pytest -k small -v

pytest - markers

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"

pytest - more

There are a number of tutorials online for pytest. This is a very straightforward one.

pytest - project structure

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.

Feedback

COMP1531 21T3 - 1.4 - Testing - Intro

By haydensmith

COMP1531 21T3 - 1.4 - Testing - Intro

  • 1,100