- •Credits
- •About the Author
- •About the Reviewers
- •www.PacktPub.com
- •Preface
- •Getting started
- •More advanced graphics
- •Summary
- •Installing Sage
- •Starting Sage
- •Start Sage
- •Prerequisites
- •Installation
- •Summary
- •Command history
- •Working with code
- •Arithmetic operators
- •Strings
- •Functions
- •Functions with keyword arguments
- •Objects
- •Summary
- •Python 2 and Python 3
- •Running scripts
- •Strings
- •List comprehensions
- •Storing data in a dictionary
- •Summary
- •Vectors and vector spaces
- •Creating a vector space
- •Vector operators and methods
- •Decomposing matrices
- •Summary
- •Using graphics primitives
- •Summary
- •Substitutions
- •Finding roots
- •Derivatives
- •Integrals
- •Series and summations
- •Summary
- •Computing gradients
- •Constrained optimization
- •Probability
- •Summary
- •Making our tanks move
- •Unit testing
- •Summary
- •Introducing Python decorators
- •Making interactive graphics
- •Summary
- •Index
Learning Advanced Python Programming
Finally, we called both the move method and the fire method in the same try block. We use two except statements to catch two different exception classes and handle them differently. This is a good reason to define your own exception classes—if we had just raised ValueError exceptions, we would have had no way to know where an exception came from (other than looking at its argument). Note that the output only shows a MoveError, even though the call to tank.fire would have produced an error as well (elevation > 90). Because the move method raised an exception, the interpreter skipped directly to the except block, ignoring the rest of the statements in the try block.
Tips for using exceptions correctly
The whole idea of using exceptions is to make it easier to identify and handle specific runtime errors in your programs. You defeat the purpose of using exceptions if you place too many lines of code in a try block, because then it's hard to tell which statement raised the exception. It's also a bad idea to have a bare except: statement that doesn't specify the exception type that is being caught. This syntax will catch any type of exception, including SystemExit and KeyboardInterrupt exceptions, making it hard to terminate a misbehaving program. It's also considered bad practice to catch an exception without properly handling it, as this practice can mask errors.
Unit testing
As object-oriented programs get larger and more complicated, debugging can become more difficult. Unit testing is a paradigm for verifying and validating software. A unit is the smallest part of the program that can be tested, such as an individual function or method. Unit testing is the practice of testing each individual unit, by itself, to ensure that it responds correctly.
Python has a package in the standard library called unittest to help you implement unit tests for your code.
Timeforaction–creatingunittestsfortheTankclass
Let's see how unittest can help us test the Tank class. Enter the following code into a text file in the same directory as the combatsim package:
import combatsim
import combatsim.exceptions as ex import unittest
class TestTank(unittest.TestCase):
"""Tests for the combatsim package."""
def setUp(self):
"""Called before EACH test is run."""
# Define parameters for the tank
[ 284 ]
Chapter 9
armor_values = {'front' : 100, 'side' : 50, 'rear' : 25, 'turret' : 75}
main_gun_damage = 50 initial_position = (0.0, 0.0)
# Create a tank object
self.tank = combatsim.tank.Tank(armor_values, main_gun_damage, initial_position)
def test_get_position(self):
"""Test method get_position""" position = self.tank.get_position() self.assertEqual(position, (0.0, 0.0))
def test_move(self):
"""Test method move""" self.tank.move(0, 1)
position = self.tank.get_position() self.assertEqual(position, (1, 0))
def test_move_arg1(self):
"""Test method move, arg 1 invalid""" self.assertRaises(ex.MoveError, self.tank.move, 360, 1)
def test_move_arg2(self):
"""Test method move, arg 2 invalid""" self.assertRaises(ex.MoveError, self.tank.move, 159, -1)
def test_fire(self):
"""Test method fire. This test is designed to FAIL \ so you can see what a failed test looks like."""
result = self.tank.fire(90,30) self.assertEqual(result, True)
suite = unittest.TestLoader().loadTestsFromTestCase(TestTank) unittest.TextTestRunner(verbosity=2).run(suite)
Run the script. You should get output like this:
sage: load("4460_9_11.py")
Test method fire. This test is designed to FAIL so you can see what a failed test looks like. ... Bang!
FAIL
Test method get_position ... ok Test method move ... ok
Test method move, arg 1 invalid ... ok
[ 285 ]
Learning Advanced Python Programming
Test method move, arg 2 invalid ... ok
======================================================================
FAIL: Test method fire. This test is designed to FAIL so you can see what a failed test looks like.
----------------------------------------------------------------------
Traceback (most recent call last):
File "./4460_9_11.py", line 42, in test_fire self.assertEqual(result, True)
AssertionError: None != True
----------------------------------------------------------------------
Ran 5 tests in 0.006s
FAILED (failures=1)
What just happened?
We started out by importing the Tank class from combatsim.tank and importing the combatsim.exceptions module, just like we did before. We also imported the unittest module. We created a class called TestTank, which is derived from unittest.TestCase, for the purpose of testing the Tank class. Each of the methods of TestTank tests a specific feature of the Tank class. However, we need to create an instance of the Tank class before we can start testing. The method called setUp is called before each test is run. If we had to do some cleanup (such as closing a file or a database connection) after each test, we would have placed this code in a method called tearDown.
Each test method has a docstring that explains what it does. The docstring is especially important for test methods because it is used to document the result of the test. The method test_get_position starts by calling the get_position method of a Tank instance created by setUp. The next statement uses assertEqual to check that the position returned by get_position matches the position specified in setUp. This is all you have to do—the rest is handled automatically by unittest. The next method, test_move, works in a similar way.
[ 286 ]
Chapter 9
The methods test_move_arg1 and test_move_arg2 are somewhat different. These methods intentionally cause the move method to raise an exception by passing invalid arguments. The syntax for this is slightly different. The assertRaises method takes three arguments: the name of the exception that should be raised, the method to be tested, and any arguments for the method to be tested. In this case, the arguments are two numbers that represent direction and distance. I also included a method called test_fire, which fails because the fire method doesn't really do anything. This method was included so that you can see what happens when a test fails. Notice that the test method docstring is printed to help you understand which test failed.
The final two lines of the example are shortcuts to help us create a test suite and run the tests. The following statement creates a test suite called suite:
suite = unittest.TestLoader().loadTestsFromTestCase(TestTank)
The following line calls the run method of a class called TextTestRunner that comes with unittest:
unittest.TextTestRunner(verbosity=2).run(suite)
This class provides a simple text interface that runs the tests and prints the results.
This short example demonstrates only the most basic features of unittest. Look at the unittest documentation to get an idea of what it can do to help you test larger and more complex packages. There are more assert methods to handle various types of output from the methods you are testing. Here is a list of the assert methods available in Python 2.6; even more are available in Python 2.7 and higher:
assertTrue(expr) |
Test fails if expr is False |
assertEqual(first, second) |
Test fails if first is not equal to second |
assertNotEqual(first, second) |
Test fails if first is equal to second |
assertAlmostEqual(first, second, |
Test fails if the difference between first and second is |
[,places]) |
greater than places decimal places (default 7) |
assertNotAlmostEqual(first, |
Test fails if the difference between first and second is |
second[,places]) |
smaller than places decimal places (default 7) |
assertRaises(exception, callable |
Test fails unless exception is raised by callable |
[,args]) |
(with optional args passed to callable) |
assertFalse(expr) |
Test fails if expr is True |
[ 287 ]
Learning Advanced Python Programming
Strategiesforunittesting
Defining unit tests requires careful thought and a fair amount of judgement. It's impossible, or at least highly impractical, to test every possible execution path in most programs. Before writing tests, the developer should think about the intended use of the code and refer back to the requirements that may have been defined before the code was written. The following are some suggestions for unit tests:
Boundary (or edge) cases test how the code performs when one of its parameters reaches an extreme value
A corner case tests what happens when all of the parameters take on extreme values
Branch testing attempts to test all branches of the source code at least once Exception tests check to make sure that exceptions are raised and handled properly
Run the code with a set of fixed inputs to ensure that it reproduces known results (such as published results)
Randomly generate valid inputs and run the code to see if certain combinations of parameters lead to an error
For some types of tests, such as corner cases, the author of the code is the best person to write the test. On large projects, unit tests are often written by someone other than the author of the code. The tests are written to cover the requirements that were defined for the software. This approach has the advantage that the tests may uncover assumptions that were made by the person who wrote the code.
If you have written some code that you would like to have included in the Sage library, you will have to become familiar with the testing standards of the Sage project. The Sage project uses a type of testing called doctesting to ensure quality. The docstring for each function or module is written in a special format that includes a section with examples. Doctesting automatically searches for examples in the docstring (hence the name), runs them, and verifies that the results are correct. More information about doctesting Sage modules and the conventions for writing docstrings is available at:
http://www.sagemath.org/doc/developer/doctesting.html
http://www.sagemath.org/doc/developer/conventions.html
Haveagohero–creatingsomeunittests
Define two methods to verify that the fire method of the Tank class raises the right exception when invalid arguments are passed to the method. Use test_move_arg1 as an example.
[ 288 ]
