0% found this document useful (0 votes)
3 views

03 Test Automation

This document provides a comprehensive guide to testing in Python, covering topics such as unit testing, using PyHamcrest matchers, and mocking techniques. It introduces various Python test frameworks, explains how to write and run tests, and offers examples of testing classes and functions. Additionally, it discusses advanced topics like parameterized tests and test-driven development (TDD).

Uploaded by

Ivan Igic
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

03 Test Automation

This document provides a comprehensive guide to testing in Python, covering topics such as unit testing, using PyHamcrest matchers, and mocking techniques. It introduces various Python test frameworks, explains how to write and run tests, and offers examples of testing classes and functions. Additionally, it discusses advanced topics like parameterized tests and test-driven development (TDD).

Uploaded by

Ivan Igic
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
You are on page 1/ 39

Click

Testto edit Master title style


Automation

1. Getting started with testing


2. Using PyHamcrest matchers
3. Mocking

Annex
• Additional testing techniques
Section 1: Getting Started with Testing
Click to edit Master title style
• Setting the scene
• Python test frameworks
• Example class-under-test
• How to write a test
• Example test
• Running tests
• Arrange / Act / Assert
• Testing for exceptions
• Setup and teardown code
Setting the Scene
Click to edit Master title style
• Unit testing verifies the behaviour of code artifacts in
isolation
• In Python, a "unit" is usually a function

• You typically write several unit tests per function


• To exercise all the possible paths through the function

• The FIRST principles of unit testing:


• Fast
• Isolated / independent
• Repeatable
• Self-validating
• Timely
Python Test Frameworks
Click to edit Master title style
• There are several test frameworks for Python, including:
• PyTest - simple and fast, most widely used (function-based)
• Unittest (aka PyUnit) - part of standard library (OO-based)
• TestProject - generates HTML reports, use with PyTest or
Unittest
• Behave - BDD test framework
• Robot - primarily for Acceptance Testing

• We'll use PyTest (plus a few other libraries, discussed


later). Here's a full set of libraries to install for this
pip install pytest
chapter:
pip install PyHamcrest Discussed in section 2 in this chapter
pip install pytest-mock Discussed in section 3 in this chapter
Example Class-Under-Test
Click to edit Master title style
• In the next few slides, we'll see how to test this simple
class:
class BankAccount:

def __init__(self, name):


self.name = name
self.balance = 0

def deposit(self, amount):


self.balance += amount

def withdraw(self, amount):


if amount > self.balance:
raise Exception("Insufficient funds")
self.balance -= amount

def __str__(self):
return f"{self.name}, {self.balance}"
bankAccount.py
How to Write a Test
Click to edit Master title style
• To write tests in PyTest:
• Define Python files named test_xxx.py or xxx_test.py

• Define each test as a separate function


• The function name must be test_yyy()

• Each test function should focus on a particular scenario,


and should have a meaningful name
• E.g., test_Function_Scenario
• E.g., test_Function_StateUnderTest_ExpectedBehaviour
Example Test
Click to edit Master title style
• When writing your tests, go for the low-hanging fruit first
• Test the simplest functions and scenarios first
• Then test the more complex functions and scenarios later

• Here's a simple first test


from bankAccount import BankAccount

def test_accountCreated_zeroBalanceInitially():
acc = BankAccount("David")
assert acc.balance == 0
test_BankAccount1.py
• assert is a standard Python keyword
• If the assert test returns false, it throws an
AssertionError
• This causes your test function to terminate immediately
Running Tests
Click to edit Master title style
• To run tests in PyTest, run the following command:
py.test

If all tests pass

If any test fails


Arrange / Act / Assert
Click to edit Master title style
• It's common for a test function to have 3 parts
• Arrange
• Act
• Assert

• Example
def test_singleDeposit_correctBalance():
acc = BankAccount("David")
acc.deposit(100)
assert acc.balance == 100
test_BankAccount1.py
Testing for Exceptions (1 of 2)
Click to edit Master title style
• When you write industrial-strength code, sometimes you
actually want the code to throw an exception
• The code should be robust enough to detect error situations
• You can write a test to verify the code raises an error
import pytest

def test_withdrawalsExceedLimit_exceptionOccursV1():

# Arrange.
acc = BankAccount("David")

# Act.
acc.deposit(600)

# Verify expected exception occurs.


with pytest.raises(Exception):
acc.withdraw(601)
test_BankAccount1.py
Testing for Exceptions (2 of 2)
Click to edit Master title style
• If you want to examine the exception that was thrown:
import pytest

def test_withdrawalsExceedLimit_exceptionOccursV2():

# Arrange.
acc = BankAccount("David")

# Act.
acc.deposit(600)

# Verify expected exception occurs.


with pytest.raises(Exception) as excinfo:
acc.withdraw(601)

# Assert the exception type and error message are correct.


assert excinfo.typename == "Exception"
assert excinfo.value.args[0] == "Insufficient funds"
test_BankAccount1.py
Setup and Teardown Code
Click to edit Master title style
• You can define a fixture function to do common code
before and after each test
import pytest
… To see the console output
for a test, use the -s option
acc = None

@pytest.fixture(autouse=True)
def run_around_tests():

# Code that will run before each test.


print("Do something before a test")
global acc
acc = BankAccount("David")

# A test function will be run at this point.


yield

# Code that will run after each test.


print("Do something after a test") test_BankAccount2.py
Section 2: Using PyHamcrest Matchers
Click to edit Master title style
• A reminder about simple assertions
• Introducing PyHamcrest
• Getting started with PyHamcrest
• Example class-under-test
• Example test
• Defining a custom PyHamcrest matcher
• Using a custom PyHamcrest matcher
A Reminder About Simple Assertions
Click to edit Master title style
• As we've seen, Python has a simple assert keyword
• assert a == b

• What if you want to write some specific tests, such as:


• Does a collection contain a value?
• Does a variable point to a particular type of subclass?
• Does an integer lie in a certain range?
• Does a float equal a value, to a specified accuracy?
Introducing PyHamcrest
Click to edit Master title style

• The PyHamcrest library provides a


higher-level vocabulary for writing
your tests, with matcher functions
such as:
• equal_to, close_to, not
• greater_than, greater_than_or_equal_to
• less_than, less_than_or_equal_to
• starts_with, ends_with, contains_string,
Getting Started with PyHamcrest
Click to edit Master title style

• Install PyHamcrest as follows:


pip install PyHamcrest

from hamcrest import *


assert_that(… … …)
• You can then use PyHamcrest as
follows in your tests:
Example Class-Under-Test
Click to edit Master title style

• To illustrate PyHamcrest, we'll test


class Product:

def __init__(self, description, price, *ratings):

the following class:


self.description = description
self.price = price
self.ratings = list(ratings)

def taxPayable(self):
return self.price * 0.20

def __str__(self):
return f"{self.description}, £{self.price}, {self.ratings}"
product.py
Example Test
Click to edit Master title style
• Here's how we can use PyHamcrest to test the class:
from hamcrest import *
import pytest
from Product import Product

product = None

@pytest.fixture(autouse=True)
def run_around_tests():
global product
product = Product("TV", 1500, 5, 4, 3, 5, 4, 3)
yield

def test_product_taxPayable_correct():
assert_that(product.taxPayable(), close_to(300, 0.1))

def test_product_ratings_containsRating():
assert_that(product.ratings, has_item(3))

def test_product_ratings_doesntContainAbsentRating():
assert_that(product.ratings, not(has_item(2))) test_Product1.py
Defining a Custom PyHamcrest Matcher
Click to edit Master title style
• The PyHamcrest matcher model is extensible
• You can define your own custom matcher classes
from hamcrest.core.base_matcher import BaseMatcher

class PriceMatcher(BaseMatcher):

def __init__(self, maxInclusive):


self.maxInclusive = maxInclusive

def _matches(self, price):


return 0 < price <= self.maxInclusive

def describe_to(self, description):


description.append_text("0..." + str(self.maxInclusive))

def valid_price():
return PriceMatcher(2500) priceMatcher.py
Using a Custom PyHamcrest Matcher
Click to edit Master title style
• Here's how to use a custom PyHamcrest matcher
• Exactly the same as for the standard PyHamcrest matchers 
from hamcrest import *
from priceMatcher import valid_price
from product import Product

def test_product_validPrice_priceAccepted():
product1 = Product("TV", 1500)
assert_that(product1.price, valid_price())

def test_product_negativePrice_priceRejected():
product2 = Product("TV", -1)
assert_that(product2.price, is_not(valid_price()))

def test_product_tooExpensivePrice_priceRejected():
product3 = Product("TV", 2501)
assert_that(product3.price, is_not(valid_price())) test_Product2.py
Section 3: Mocking
Click to edit Master title style
• Overview of mocking
• Mocking in Python
• Mocking values
• Mocking functions
• Mocking methods
Overview of Mocking
Click to edit Master title style
• Real-world systems involve lots of interacting code
• Unit testing focuses on the behaviour of an isolated unit
• We can use mocks to blank off other code

• Mocking
• Use a mocking framework to create a mock
value/function/object
• In tests, use the mock value/function/object rather than a
real one
Mocking in Python
Click to edit Master title style
• The PyUnit module has standard support for mocking
• Via the unittest.mock module

• PyTest-mock is a thin wrapper over unittest.mock


• Provides a mocker fixture
• You can use mocker to patch values/functions/classes

• To install PyTest-mock:
pip install pytest-mock

• Let's see how to use PyTest-mock


• See the mockdemo package folder
Mocking Values (1 of 2)
Click to edit Master title style

• Consider this value defined


BASE_URL = "https://example.com/api/products" myDependencies.py

mockdemo/myDependencies.py

somewhere in our app:


from mockdemo.myDependencies import BASE_URL

def build_url(**kwargs):
url = BASE_URL
if len(kwargs) != 0:
myCode.py
url += "?"
for k, v in kwargs.items():
• We use the value in our code-under-
url += f"{k}={v}&"
url = url.rstrip('&')
return url
test: mockdemo/myCode.py
Mocking Values (2 of 2)
Click to edit Master title style
• We can mock the value in our test as follows:
• Receive a mocker fixture object (from PyTest-mock)
• Use the mocker object to define a mock value for BASE_URL
import mockdemo.myCode as myCode

def test_build_url(mocker):

mocker.patch.object(myCode, 'BASE_URL', 'http://localhost/products')

actual = myCode.build_url(minPrice=100, maxPrice=500, order='desc')

expected = 'http://localhost/products?minPrice=100&maxPrice=500&order=desc'

assert expected == actual

mockdemo/test.py
Mocking Functions (1 of 2)
Click to edit Master title style

• Consider this (slow) function


def calculate_meaning_of_life():
time.sleep(60)
return 42

somewhere in our app: mockdemo/myDependencies.py

from mockdemo.myDependencies import calculate_meaning_of_life

def get_meaning_of_life():
result = calculate_meaning_of_life()
return 'The meaning of life is ' + str(result)
mockdemo/myCode.py

• We call the function in our code-


Mocking Functions (2 of 2)
Click to edit Master title style
• We can mock the function in our test as follows:
• Receive a mocker fixture object (from PyTest-mock)
• Use the mocker object to mock the return value for the
function
import mockdemo.myCode as myCode test.py

def test_get_meaning_of_life(mocker):

mocker.patch(
'mockdemo.myCode.calculate_meaning_of_life',
return_value='wibble'
)

actual = myCode.get_meaning_of_life()

expected = 'The meaning of life is wibble'

assert expected == actual


mockdemo/test.py
Mocking Methods (1 of 2)
Click to edit Master title style

• Consider this class (with slow


class DataLoader:

def __init__(self):

methods) somewhere in our app:


self.delay = 60

def load_product(self):
time.sleep(self.delay)
return 'Bugatti Divo'

def load_great_football_team(self):
time.sleep(self.delay) myDependencies.py
return 'Swansea City' mockdemo/myDependencies.py

from mockdemo.myDependencies import DataLoader

def get_product():
dataLoader = DataLoader()
result = dataLoader.load_product()
return 'Product is ' + str(result) mockdemo/myCode.py
Mocking Methods (2 of 2)
Click to edit Master title style
• We can mock methods in our test as follows:
• Receive a mocker fixture object (from PyTest-mock)
• Use the mocker object to mock methods as needed
import mockdemo.myCode as myCode

def test_get_product(mocker):

mocker.patch(
'mockdemo.myCode.DataLoader.load_product',
lambda self: 'wibble'
)

actual = myCode.get_product()

expected = 'Product is wibble'

assert expected == actual

mockdemo/test.py
Click to edit Master title style
Summary

• Getting started with testing


• Using PyHamcrest matchers
• Mocking
Annex: Additional Testing Techniques
Click to edit Master title style
• Parameterized tests
• Running tests selectively
• Grouping tests into sets
• Test Driven Development (TDD)
• Refactoring
Parameterized Tests (1 of 2)
Click to edit Master title style
• When you start writing tests, you might notice some of
the tests are quite similar and repetitive
• E.g., imagine a function that returns the grade for an exam
• How would you test it always returns the correct grade?
def get_grade(mark):
if mark >= 75:
return "A*"
elif mark >= 70:
return "A"
elif mark >= 60:
return "B"
elif mark >= 50:
return "C"
elif mark >= 40:
return "D"
elif mark >= 30:
return "E"
else:
return "U" util.py
Parameterized Tests (2 of 2)
Click to edit Master title style
• You can write a parameterized test as follows:
import pytest
from util import get_grade

@pytest.mark.parametrize("mark, grade", [
(99, "A*"),
(70, "A"),
(69, "B"),
(60, "B"),
(59, "C"),
(50, "C"),
(49, "D"),
(40, "D"),
(39, "E"),
(30, "E"),
(29, "U")
])
def test_marks_and_grades(mark, grade):
assert grade == get_grade(mark)
test_util.py
Running Tests Selectively
Click to edit Master title style
• py.test lets you specify which test functions to run…

• E.g., run all test functions that have 'deposit' in their


name
• The -k option specifies the key (function name fragment)
• The -v option
py.test displays
-k deposit -v verbose test results
Grouping Tests into Sets (1 of 3)
Click to edit Master title style
• You can group tests into sets
• You can then run all the tests in a particular set

• The first step is to specify your custom sets


• Define a file named pytest.ini as follows:
[pytest]
markers =
numtest: mark a test as a numeric test
strtest: mark a test as a string test
pytest.ini
Grouping Tests into Sets (2 of 3)
Click to edit Master title style
• You can then mark a test function so it belongs to a set(s)
• Decorate the test function with @pytest.mark.aSetName
import pytest

@pytest.mark.numtest
def test_add_numbers():
assert 3 + 4 == 7

@pytest.mark.numtest
def test_multiply_numbers():
assert 3 * 4 == 12

@pytest.mark.strtest
def test_concatenate_strings():
assert "hello " + "world" == "hello world"

@pytest.mark.strtest
def test_uppercase_strings():
assert "hello world".upper() == "HELLO WORLD"

test_setsDemo.py
Grouping Tests into Sets (3 of 3)
Click to edit Master title style
• To run all the tests in a particular set:
• Use the -m option
• Specifies which marked tests to run (i.e., the name of the
set)
py.test -m numtest -v
Test Driven Development (TDD)
Click to edit Master title style
• TDD is a simple concept
• You write the tests first, before your write the code
• The tests are for functionality you're about to implement

• In TDD, you perform the following tasks repeatedly:


• Write a test
• Run the test - it must fail!
• Write the minimum amount of code, to make the test pass
• Refactor your code

• Benefits of TDD
• Helps you focus on functionality rather than implementation
• Ensures every line of code is tested
Refactoring
Click to edit Master title style
• Refactoring is an often-overlooked aspect of TDD
• After each iteration through the test-code-pass cycle, you
should refactor your code
• That is, step back and see if you can/should reorganize your
code to eliminate duplication, restructure inheritance, etc.

• Typical refactoring activities:


• Rename a variable / function / class / module / package
• Extract duplicate code into a common function
• Extract common class functionality into a superclass
• Introduce another level of inheritance

You might also like