Your first code was:
def square(x):
"""Finds the square of the input.
Args:
x (float): The number to be squared.
Returns:
x2 (float): The squared number.
"""
return x**2
The first thing that appears is def square(x): this defines a
function called square which takes a single argument x. Everything
underneath this definition that is indented is part of the
function. The documentation for the function, all the text in
triple quotes, comes next. This in the sphinx google style
documentation. Since documenting code is extremely important I will
explain how it usually works.
Documentation is always started with and ends with triple double
quotes and comes immediately after the function, function, or
classes definition. The first thing inside the documentation is a
brief description of what the function should do. After that the
input, or arguments, of the function are listed below the Args:
tag. Each argument is listed so that it's name appears first followed
by it's type in parenthesis. After the arguments type a description of
what the argument is, i.e., a number to be squared, a list to be
sorted, an instance of a class ...., appears. Once all the arguments
have been listed the variables that the function returns also need
to be described using the same format. There is additional information
you can include in the function's documentation like if it raises
any exception or errors (ValueErrors, TypeErrors, ...) and examples of
usage examples of which can be found
here.
Once you are done with the documentation comes your actual code. This sample function is extremely simple consisting of a single line of code, this will not be the case most of the time.
Having written this first function we need to test it to ensure it functions. We will do that using the python packages tox and pytest, tox helps to automate unit tests over multiple python versions while pytest is a package containing many tools that help us write unit tests. Tox is a python package that handles unit testing in multiple python versions ensuring your code will be compatible with the python versions you desire.
To make our first unit test we will go to the directory containing your setup.py and setup.cfg files. Once there make a new directory called tests and navigate into it:
mkdir tests
cd tests
In this directory we will write all of our codes for testing. Each
testing module or code file that you want tox to find and run needs to
start with the word test. For this first test lets make the file
test_my_pkg.py. Inside the file write the following code:
"""Tests the mathematical functions defined in my_pkg/trail.py
"""
import pytest
def test_square():
"""Tests the squaring function"""
from my_pkg.trial import square
assert 4 == square(2)
The first thing in our testing code file is a line of documentation
describing what the tests in it will do, this is optional but
generally a good idea as it helps other developers know what is going
on. Next we import pytest and define a function test_square. The
name is not arbitrary. Just like the file name for tests, each
function that contains tests also needs to start with the name test.
(Anything can come after test.) Otherwise, pytest and tox will not find
the tests when they are run. Inside the function we give a
description of what it will do then import the function that is
going to be tested:
from my_pkg.trial import square
Remember to replace my_pkg with your package name. Next we write the actual test:
assert 4 == square(2)
The assert in the line of code above checks that any equality or statement that follows is true. It is possible to include multiple asserts in a single test function, for example:
assert 4 == square(2)
assert 4 == square(-2)
assert 12.25 == square(3.5)
assert 2 == round(square(sqrt(2)), 5)
It is usually a good idea to have each test check for a different possible case where the code could break. For example, here we test to make sure that our function correctly computes the square of negative numbers, fractions and irrational numbers. We want to try and break our code with these tests so we can fix it now rather than later.
With this test code written we need to write a file that will tell tox what to do. Go back up a directory to where setup.py is:
cd ../
Here we will make a file called tox.ini as sample of which can be found in the getting_started repository (or by following the link). The example file looks like this:
[tox]
envlist = py27, py34
[testenv]
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
deps=
pytest
coverage
codecov
commands=
coverage run --source=my_pkg -m pytest
The first two lines of the file tell tox which python environments you want to test your code in. It's usually a good idea to check that your code runs in python 2, since many users haven't switched to python 3 yet, and in one or two python 3 versions. The sample file tells tox to run the tests in python 2.7 and python 3.4. Modify this line to test the python versions you would like to include.
The rest of the code tells tox what it's doing, the first assignment,
passenv informs tox that it will (eventually) be run on the Travis
CI server for continuous integration. We will
discuss how to setup up Travis in a later
section. The
next assignment, deps tells tox which packages it will need to run
your tests, in this case we've listed pytest (which actually runs the
tests), coverage (which makes code coverage reports discussed
later),
and codecov (which also handles coverage reports). Finally we use
commands to tell tox what it's doing, the command we supply it with
is coverage run --source=my_pkg -m pytest (replace my_pkg
with your package name). This tells tox to use the command coverage run on your package (--source=my_pkg), with the method pytest (-m pytest). You can use tox.ini to do a lot of other things as well, if
your interested read the
documentation,
but in most cases what you see here is all you will need.
At this point your repository folder should look like:
'my_repo'/'package'/__init__.py
'my_repo'/'package'/trial.py
'my_repo'/setup.py
'my_repo'/setup.cfg
'my_repo'/tests/test_my_pkg.py
'my_repo'/tox.ini
'my_repo'/README.md
'my_repo'/LICENSE
'my_repo'/.gitignore
Now we're going to copy your repository to the docker container to run the test and make sure that they work (just like you did when testing your python package setup):
cd ../
docker cp 'my_repo' my_container:.
Now go back to the terminal that is running your docker container (if you exited it earlier you will need to restart it before you can copy the code over) and type:
cd 'my_repo'
tox
A bunch of stuff should print to your screen at the end of which you should see something like:
____________________________________________________________ summary _____________________________________________________________
py27: commands succeeded
py34: commands succeeded
congratulations :)
But with py27 and py34 replaced with the python versions you chose to test on. If you see any errors at this point please double check everything you've done in this section and try again. If it still won't work please submit an issue describing the error and we'll get back to you ASAP with help. If all went well then return to your local machine where we will make your second python function using test-driven development.
The idea behind test driven development is to start by writing tests for what you want your code to do, and then writing the code so that it will pass the tests. This makes identifying errors and debugging much easier. To illustrate how this works we'll write a second function inside of trial.py using the test-driven framework.
Inside of trial.py start a new function called factorial:
def factorial(n):
The factorial function will find the factorial of an integer. Let's write the documentation for our new function:
"""Factorial calculates the factorial of the provided integer.
Args:
n (int): The value that the factorial will be computed from.
Returns:
fact (int): The factorial of n.
Raises:
ValueError: If n is not an integer.
"""
We've included a raises descriptor in the documentation since if the user tries to pass 'factorial' a float (say 3.5) we want to warn them of the error rather than try to find the factorial. Now before we write a single line of code let's write some unit tests.
Inside of tests/test_my_pkg.py add the following lines of code for tests, you can add additional tests if you so desire but make sure you have at least these ones:
def test_factorial():
"""Tests the factorial function."""
from my_pkg.trial import factorial
assert 24 == factorial(4)
assert 6 == factorial(3.0)
assert 1 == factorial(0)
assert 1 == factorial(-1)
with pytest.raises(ValueError):
factorial(3.5)
Everything here should look very familiar with the exception of the lines:
with pytest.raises(ValueError):
factorial(3.5)
These code lines will check to make sure that a ValueError is raised
if the user passes the function a float instead of an int. Now that we
have tests we can go back to our function and finish writing the code:
if not isinstance(n,int):
raise ValueError("The input to factorial must be an integer.")
if n < 0:
fact = 1
else:
fact = n
for i in range(1,n):
fact = i*fact
return fact
Now we run our unit tests to see if our code will do what we expect.
Copy your code into the docker container and run your unit tests just like you did when making your first unit-tests. This time you should see something that looks like:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
n = 3.0
def factorial(n):
"""Factorial calculates the factorial of the provided integer.
Args:
n (int): The value that the factorial will be computed from.
Returns:
fact (int): The factorial of n.
Raises:
ValueError: If n is not an integer.
"""
if not isinstance(n,int):
> raise ValueError("The input to factorial must be an integer.")
E ValueError: The input to factorial must be an integer.
my_pkg/trial.py:31: ValueError
=============================================== 1 failed, 1 passed in 0.08 seconds ===============================================
ERROR: InvocationError: '/Users/wileymorgan/codes/getting-started/.tox/py34/bin/coverage run --source=my_pkg -m pytest'
____________________________________________________________ summary _____________________________________________________________
ERROR: py27: commands failed
ERROR: py34: commands failed
Our tests failed! It looks like when we passed in a value of 3.0, which we know is a valid integer, the code raised a ValueError. We'll need to fix this. In your code replace:
if not isinstance(n,int):
raise ValueError("The input to factorial must be an integer.")
With:
if not isinstance(n,int):
if int(n) == n:
n = int(n)
else:
raise ValueError("The input to factorial must be an integer.")
Now if the input value isn't an integer, we check to see if the input
value is equivalent to an integer. If it is, we replace it with
one. With that fixed, let's check to see if we're passing all our tests
again in the docker container. (Don't forget to copy the code you've
changed into the container before running tox).
The result should be something like:
_________________________________________________________ test_factorial _________________________________________________________
def test_factorial():
"""Tests the factorial function."""
from my_pkg.trial import factorial
assert 24 == factorial(4)
assert 6 == factorial(3.0)
> assert 1 == factorial(0)
E assert 1 == 0
E + where 0 = <function factorial at 0x1074c81e0>(0)
tests/test_my_pkg.py:20: AssertionError
=============================================== 1 failed, 1 passed in 0.06 seconds ===============================================
ERROR: InvocationError: '/Users/wileymorgan/codes/getting-started/.tox/py34/bin/coverage run --source=my_pkg -m pytest'
____________________________________________________________ summary _____________________________________________________________
ERROR: py27: commands failed
ERROR: py34: commands failed
We passed the factorial(3.0) test but failed another test now. It seems that when passed a value of zero the function returns 0 instead of 1. We'll need to fix this as well so in your code replace:
if n < 0:
fact = 1
With:
if n =< 0:
fact = 1
Now when we copy the revised code to the docker container and run tox we get:
==================================================== 2 passed in 0.02 seconds ====================================================
____________________________________________________________ summary _____________________________________________________________
py27: commands succeeded
py34: commands succeeded
congratulations :)
Just to be clear, the work flow in test-driven development is to decide what your code should do and write tests to verify that performance before you write any code. In other words each time you start to write a new function you should (1) define the function, (2) write its documentation, (3) write tests to model the desired output, and only then (4) write the code. This will ensure that all your functions have unit tests and that they all behave as expected.
Now that you program in a test-driven way let's move on to continuous integration