Software testing
I recently completed a Udacity course on software testing. It was a quick course, but I learned a few interesting techniques. Mostly, I got the chance to use a couple of tools I had heard about but never actually used.
Python coverage measurements
Python has a tool called Coverage.py that generates statement and branch coverage metrics for a given set of tests. For instance, we can write the following Fizz Buzz program and set of tests.
def fizzbuzz(n):
if n % 15 == 0:
return "FizzBuzz"
if n % 3 == 0:
return "Fizz"
if n % 5 == 0:
return "Buzz"
return str(n)
assert fizzbuzz(3) == "Fizz"
assert fizzbuzz(4) == "4"
assert fizzbuzz(5) == "Buzz"
We can evaluate our tests with the coverage tool.
coverage erase
coverage run --branch fizzbuzz.py
coverage html
open htmlcov/index.html
This produces the following output.
Based on this, we can easily see that we don’t have any tests covering
the condition where n
is a multiple of 15. The red line indicates a
lack of statement-level coverage, while the yellow line shows a
partially covered branch. We can fix both of these issues by adding
one more test to our program.
assert fizzbuzz(30) == "FizzBuzz"
Fuzzing a program
My favorite exercise of the course was writing a simple random fuzzer. Random fuzzing is surprising effective across many programs, and although I did not find any bugs I am glad to have practiced the technique.
Random fuzzing involves starting with good inputs, mutating random
bytes, and feeding those modified inputs to the software under test. I
decided to test feh
, a photo viewer. The fuzzing code is pretty
self-explanatory, which I am excited about: it turns out writing a
basic fuzzer is easy!
import math
import os
import random
import string
import subprocess
import time
file_dir = "input-photos/"
file_list = os.listdir(file_dir)
app = "/usr/local/bin/feh"
output_dir = "output-photos/"
FuzzFactor = 250
num_tests = 10000
for i in range(num_tests):
file_choice = random.choice(file_list)
with open(file_dir + file_choice, 'rb') as f:
buf = bytearray(f.read())
numwrites = random.randrange(math.ceil((float(len(buf)) / FuzzFactor))) + 1
for j in range(numwrites):
rbyte = random.randrange(256)
rn = random.randrange(len(buf))
buf[rn] = rbyte
output = output_dir + str(i) + ".tif"
with open(output, 'w+b') as f:
f.write(buf)
process = subprocess.Popen([app, output], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
time.sleep(2)
crashed = process.poll()
if crashed and crashed != 0 and crashed != 1:
print("Crash on test {}!".format(i))
else:
process.terminate()