All A Level 21 55 min read 10 question quiz 19 exam marks

Testing, Unit Tests and Test-Driven Development

The levels and kinds of testing the specification names, then unit tests written and run with unittest, the red, green, refactor cycle of test driven development worked through on a real function, and exceptions that fail loudly instead of quietly.

Levels and kinds of testing

Testing is not one activity. The specification names several, and the exam asks you to say which one a scenario describes, and why.

The levels of testing

LevelWhat is testedBy whomTypical question
UnitOne subprogram or class, on its ownThe programmerDoes calculate_tax return the right figure?
IntegrationSeveral units working togetherThe programmer, or a testerDoes the basket pass the right total to the payment module?
SystemThe whole program, against the requirementsA testerDoes the finished shop do everything the specification said?
AcceptanceThe finished system, by the client, in their own settingThe customerIs this what we asked for, and will we accept it?

Two named stages of testing a finished product with real users:

  • Alpha testing is carried out inside the development organisation, by staff who are not the programmers. It finds the problems before outsiders see them.
  • Beta testing is carried out by a selected group of real users, in their own environment, before the general release. It finds the problems that the developers' own machines and habits could not.

Black box and white box

Black box testingWhite box testing
The tester knowsOnly the specification: what goes in and what should come outThe code: its branches, loops and paths
Test data chosen fromThe requirementsThe structure, so that every path is taken
FindsMissing or wrong behaviourFaulty logic, and code that was never run
ExampleEnter 49 and check that "Fail" is shownMake sure a test reaches the elif for a pass mark

A white box test aims for coverage: every statement, every branch, and every path executed by at least one test. 100 per cent coverage proves the code was run. It does not prove that it is right.

Other words the exam uses

TermMeans
Regression testingRe running the earlier tests after a change, to check that the change has not broken something that used to work
Static testingExamining code without running it: reading it, a walkthrough, a trace table, or a tool that checks it
Dynamic testingRunning the code with test data and checking the results
Normal, boundary, erroneous dataThe three kinds of test data, as at GCSE, now applied to every unit
Match the test to the scenario

Questions describe a situation and ask for the type of testing. A group of the company's own staff trying the software is alpha. Chosen customers trying it at home is beta. The client signing it off against what they ordered is acceptance. Re running old tests after a bug fix is regression.

The four levels of testing as a pyramid, and black box against white box testingFour levels of testing drawn as a pyramid. At the base are unit tests, one function or method on its own, which are many and fast. Above them are integration tests, which check modules working together, then system tests, which check the whole program against the requirements, and at the top acceptance tests, where the client or users decide whether it does the job. There are fewer tests as you go up but each covers more. Below, black box testing checks inputs and outputs without seeing the code, and white box testing checks every path through the code.Acceptancethe client or users decide ifit does the jobSystemthe whole program, against therequirementsIntegrationmodules working togetherUnitone function or method on itsown: many, fast tests1fewer tests as you go up, but each covers moreBlack box testing2?inputoutputtest inputs and outputs, without seeing the codeWhite box testing3test every path through the code
  1. Four levels: unit, integration, system and acceptance. Unit tests are many and fast, acceptance tests are few and cover the most.
  2. Black box: test the inputs and outputs without seeing the code.
  3. White box: test every path through the code.
Name the level, say what it tests and who runs it. Acceptance testing is done by the client or users.

Unit tests with unittest

A unit test is a small program that calls one piece of your code with chosen inputs and checks the result automatically. Python's unittest module provides the framework. A test case is a class that inherits from unittest.TestCase, and each method whose name starts with test is one test.

Python
import unittest

def grade_for(mark):
    if mark < 0 or mark > 100:
        raise ValueError("mark must be between 0 and 100")
    if mark >= 70:
        return "Distinction"
    if mark >= 50:
        return "Merit"
    if mark >= 40:
        return "Pass"
    return "Fail"

class TestGradeFor(unittest.TestCase):
    def test_normal(self):
        self.assertEqual(grade_for(85), "Distinction")
        self.assertEqual(grade_for(55), "Merit")

    def test_boundaries(self):
        self.assertEqual(grade_for(70), "Distinction")
        self.assertEqual(grade_for(69), "Merit")
        self.assertEqual(grade_for(49), "Pass")
        self.assertEqual(grade_for(39), "Fail")
        self.assertEqual(grade_for(0), "Fail")
        self.assertEqual(grade_for(100), "Distinction")

    def test_invalid(self):
        with self.assertRaises(ValueError):
            grade_for(101)
        with self.assertRaises(ValueError):
            grade_for(-1)

def run(case):
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(case)
    result = unittest.TestResult()
    suite.run(result)
    print(result.testsRun, "tests,", len(result.failures), "failed,", len(result.errors), "errors")
    for test, trace in result.failures:
        print("  FAILED", test.id().split(".")[-1], "-", [line for line in trace.split("\n") if line.startswith("AssertionError")][0])

run(TestGradeFor)

Run it: three tests, none failed. The run function is only there to give a short, readable summary. In a normal program you would end the file with unittest.main() and let the framework print its own report.

A test that finds a bug

Change one character in the function, the classic boundary mistake of >= written as >, and run the same tests again.

Python
import unittest

def grade_for(mark):
    if mark >= 70:
        return "Distinction"
    if mark > 50:
        return "Merit"
    return "Pass"

class TestGradeFor(unittest.TestCase):
    def test_normal(self):
        self.assertEqual(grade_for(85), "Distinction")
        self.assertEqual(grade_for(55), "Merit")

    def test_boundaries(self):
        self.assertEqual(grade_for(70), "Distinction")
        self.assertEqual(grade_for(50), "Merit")

def run(case):
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(case)
    result = unittest.TestResult()
    suite.run(result)
    print(result.testsRun, "tests,", len(result.failures), "failed")
    for test, trace in result.failures:
        print("  FAILED", test.id().split(".")[-1], "-", [line for line in trace.split("\n") if line.startswith("AssertionError")][0])

run(TestGradeFor)

The normal test passes. The boundary test fails, and the report says exactly which assertion and what it received: 'Pass' != 'Merit'. A mark of 50 earned a Pass instead of a Merit. It is the boundary test that catches it, every time.

The assertions

MethodPasses when
assertEqual(a, b)a == b
assertTrue(x), assertFalse(x)x is true, or false
assertAlmostEqual(a, b)a and b are equal to 7 decimal places, for floats
assertIn(a, b)a is in b
assertIsNone(x)x is None
assertRaises(Error)the code inside the with block raises that error

A fresh start for every test

Tests must be independent: one must not depend on the leftovers of another, or the order they run in changes the result. setUp runs before each test and builds a fresh object.

Python
import unittest

class Stack:
    def __init__(self):
        self.items = []
    def push(self, item):
        self.items.append(item)
    def pop(self):
        if not self.items:
            raise IndexError("stack is empty")
        return self.items.pop()

class TestStack(unittest.TestCase):
    def setUp(self):
        self.stack = Stack()

    def test_last_in_first_out(self):
        self.stack.push(1)
        self.stack.push(2)
        self.assertEqual(self.stack.pop(), 2)
        self.assertEqual(self.stack.pop(), 1)

    def test_pop_from_empty_raises(self):
        with self.assertRaises(IndexError):
            self.stack.pop()

    def test_a_new_stack_is_empty(self):
        self.assertEqual(self.stack.items, [])

suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestStack)
result = unittest.TestResult()
suite.run(result)
print(result.testsRun, "tests,", len(result.failures) + len(result.errors), "problems")
A test is a specification you can run

Reading the test names above tells you what a Stack must do. That is the point of writing tests with the expected result decided first. If the test and the code disagree, the specification says which one is wrong.

A unittest file with three tests, the result of running it, and how to read itA test file using Python's unittest module. The class TestArea has three tests. test square checks that area of 4 and 4 equals 16, test zero checks that area of 0 and 5 equals 0, and test negative checks that area of minus 2 and 3 equals 0. Running the file prints a result: a dot for each test that passed and an F for each that failed, here dot, dot, F. The failure message names the test and shows the actual answer, minus 6, against the expected one, 0, then reports ran 3 tests, failed with one failure. A dot means a test passed, an F means a test failed because the answer was wrong, and an E means an error where the code crashed. The cycle is to run the tests, read the message, fix the code and run all the tests again.test_shapes.py1import unittest2from shapes import area34class TestArea(unittest.TestCase):5    def test_square(self):6        self.assertEqual(area(4, 4), 16)7    def test_zero(self):8        self.assertEqual(area(0, 5), 0)9    def test_negative(self):10        self.assertEqual(area(-2, 3), 0)1python -m unittest..FFAIL: test_negativeAssertionError: -6 != 0Ran 3 tests in 0.001sFAILED (failures=1)2Reading the result3.a test passedFa test failed: the answer was wrongEan error: the code crashedRed, then fix, then green4Run the testssee which failRead the messageactual -6, expected 0Fix the codearea must not go negativeRun them all again..., 3 passed
  1. Each test method checks one thing with assertEqual(actual, expected).
  2. The runner prints a dot for a pass and an F for a failure, then the message.
  3. Read the symbols: a dot passed, F is a wrong answer, E is a crash.
  4. Run, read, fix, run again. Re-run every test, so a fix cannot break something else.
A failing test is useful information. It tells you exactly what the code got wrong.

Test-driven development

Test-driven development (TDD) turns the order of work round. You write the test first, watch it fail, and only then write the code. It is a short cycle repeated many times:

  1. Red. Write one small test for a behaviour that does not exist yet. Run it. It fails, which proves that the test can fail.
  2. Green. Write the simplest code that makes the test pass. No more.
  3. Refactor. Tidy the code, with every test still passing, so that the tests protect the clean up.

A worked cycle: leap years

The rule: a year is a leap year if it divides by 4, except centuries, which must also divide by 400. So 2024 is, 1900 is not, 2000 is.

Red. The function does not do anything yet, so the very first test fails.

Python
import unittest

def is_leap_year(year):
    return False

class TestLeapYear(unittest.TestCase):
    def test_ordinary_leap_year(self):
        self.assertTrue(is_leap_year(2024))

suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestLeapYear)
result = unittest.TestResult()
suite.run(result)
print(result.testsRun, "tests,", len(result.failures), "failed")
for test, trace in result.failures:
    print("  FAILED", test.id().split(".")[-1], "-", [line for line in trace.split("\n") if line.startswith("AssertionError")][0])

Green. The simplest code that passes is to divide by 4. Add the next tests, which fail for 1900 and then pass for 2000, and each makes the function a little more right.

Python
import unittest

def is_leap_year(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

class TestLeapYear(unittest.TestCase):
    def test_ordinary_leap_year(self):
        self.assertTrue(is_leap_year(2024))

    def test_ordinary_year(self):
        self.assertFalse(is_leap_year(2023))

    def test_century_is_not_a_leap_year(self):
        self.assertFalse(is_leap_year(1900))

    def test_every_fourth_century_is(self):
        self.assertTrue(is_leap_year(2000))

suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestLeapYear)
result = unittest.TestResult()
suite.run(result)
print(result.testsRun, "tests,", len(result.failures), "failed")

Refactor. With four tests watching, the condition could be rewritten as clearer code, and the tests would say at once if the rewrite broke it.

Why do it this way round

BenefitBecause
Every feature has a testThere is no code that was written without one
Tests are written against the specificationNot against what the code happens to do
Small stepsA failure is always in the few lines just written
Fearless changeThe tests tell you at once if a clean up breaks anything, which is regression testing built in
Better designCode that is easy to test is made of small, independent units

The cost is time at the start, and the discipline to write a test when it is not wanted. TDD does not remove the need for the other levels of testing: unit tests do not show that the units fit together.

Exceptions, and failing loudly

A function that cannot do its job should say so, not return something plausible. Raise an exception with a message that explains what was wrong, and let the caller decide what to do. A custom exception gives the problem a name.

Python
class InsufficientFunds(Exception):
    pass

class Account:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("amount must be positive")
        if amount > self.balance:
            raise InsufficientFunds("balance is " + str(self.balance) + ", asked for " + str(amount))
        self.balance -= amount
        return self.balance

account = Account(100)
print(account.withdraw(30))

for amount in [500, -5, 20]:
    try:
        print("left:", account.withdraw(amount))
    except InsufficientFunds as error:
        print("refused:", error)
    except ValueError as error:
        print("invalid:", error)
    finally:
        print("   checked", amount)
PartDoes
raiseStops the function and hands an exception to the caller
tryThe code that may raise one
except Name as errorRuns only if that kind of exception was raised
finallyRuns whatever happened, which is where tidying up goes

Catching the specific exception matters. A bare except: hides every bug, including ones you did not know about, and is a common reason for a program that fails silently.

The red, green, refactor cycle of test driven development, with an exampleTest driven development repeats a three step cycle. Red: write a test first and watch it fail, here assert add of 2 and 3 equals 5, which fails with a NameError because add does not exist yet. Green: write just enough code to make the test pass, here def add of a and b returning a plus b, and the test result is 1 passed. Refactor: tidy up the code while the tests stay green, then run the tests again to check that they still pass. Then repeat in tiny steps.Test driven developmentREDa test that fails1GREENjust enough codeto pass2REFACTORtidy up, and thetests stay green3write the test first, then the code,and repeat in tiny stepstest_add.pyassert add(2, 3) == 5redNameError: add is not defined1 failedadd.pydef add(a, b):    return a + bgreen1 passedrefactor, then run the tests again: still 1 passed123
  1. Red: write a test that fails.
  2. Green: write just enough code to make it pass.
  3. Refactor: improve the code, and run the tests again to be sure they still pass.
The test comes first. It describes what the code must do before any of the code exists.

Key terms you must be able to define

Unit testing
Testing a single subprogram or class in isolation, with chosen inputs and expected outputs.
Integration testing
Testing that several units work correctly together.
System testing
Testing the whole program against its requirements.
Acceptance testing
Testing by the client, to decide whether the finished system meets what they asked for.
Alpha testing
Testing of software by people within the development organisation, before release to outsiders.
Beta testing
Testing of software by a selected group of real users, in their own environment, before general release.
Black box testing
Testing based only on the specification, without knowledge of the code.
White box testing
Testing designed using knowledge of the code, so that every path is exercised.
Regression testing
Repeating earlier tests after a change, to check that nothing has been broken.
Test-driven development
A way of working in which a failing test is written first, then the code to pass it, then the code is tidied.
Exception
An event that stops the normal flow of a program, raised when something cannot be done and handled elsewhere.

Flashcards

Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.

DefineUnit testing
AnswerTesting a single subprogram or class in isolation, with chosen inputs and expected outputs.
DefineIntegration testing
AnswerTesting that several units work correctly together.
DefineSystem testing
AnswerTesting the whole program against its requirements.
DefineAcceptance testing
AnswerTesting by the client, to decide whether the finished system meets what they asked for.
DefineAlpha testing
AnswerTesting of software by people within the development organisation, before release to outsiders.
DefineBeta testing
AnswerTesting of software by a selected group of real users, in their own environment, before general release.
DefineBlack box testing
AnswerTesting based only on the specification, without knowledge of the code.
DefineWhite box testing
AnswerTesting designed using knowledge of the code, so that every path is exercised.

Reach the top grade on this topic

Testing answers are marked on matching the term to the situation, and on tests that would really find a fault.

  • Name the level of testing from the scenario: unit, integration, system, acceptance, alpha or beta
  • Say what the tester knows in black box and in white box testing, and what each can and cannot find
  • In a test plan, include normal, boundary and erroneous data, and write the expected result first
  • Describe test driven development as red, green, refactor, with the test written before the code
  • Say that regression testing is repeating earlier tests after a change, and why that is needed

Mistakes that cost marks here

Avoid these
  • Mixing up alpha testing, done by staff inside the company, and beta testing, done by outside users.
  • Saying white box testing is done by the customer, or that black box testing needs the source code.
  • Writing the expected result after seeing the output, which tests nothing.
  • Testing only normal data, and never the boundary where a comparison is easily written wrongly.
  • Describing test driven development as writing the tests at the end of the project.
Fun fact

Software testing is expensive for a reason. In 1996 the first launch of the Ariane 5 rocket was destroyed 37 seconds after lift off by a conversion of a 64 bit number to a 16 bit integer that had been safe in the older Ariane 4 and was never tested against the new rocket's flight path.

Knowledge check

0 of 10 answered
1Which level of testing checks one subprogram on its own?
2Which testing is carried out by the customer to decide whether to accept the finished system?
3What is the difference between alpha and beta testing?
4What does a black box tester know?
5What is the aim of white box testing?
6What is regression testing?
7In test driven development, what is written first?
8What are the three stages of the test driven development cycle, in order?
9Which unittest method checks that a piece of code raises a particular error?
10Why is a bare except: that catches every error a bad idea?
Score 0 / 10

Exam-style questions

Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.

19 marks
1Describe the difference between black box testing and white box testing.4 marks
Describe

Mark scheme0 / 4
  • LIn black box testing the tester does not know, or does not use, the code
  • LTests are chosen from the specification, checking that the right output is given for an input
  • LIn white box testing the tester knows the code
  • LTests are chosen so that every path, branch or statement in the code is executed
Model answer

In black box testing the tester has no knowledge of the code and chooses tests from the specification, checking that each input gives the output that the requirements say it should. In white box testing the tester can see the code, and chooses tests so that every path, branch or statement in it is executed at least once.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
2A company develops a new mobile app. State, with a reason, whether each of the following is an example of alpha testing, beta testing or acceptance testing. (a) Employees of the company who did not write the app use it for a week and report faults. (b) A group of 500 members of the public are given the app before its public release and report faults. (c) The client who ordered the app uses it in their own business and then signs to confirm that it does what they asked.3 marks
State

Mark scheme0 / 3
  • L(a) Alpha testing, because it is carried out within the organisation, by staff
  • L(b) Beta testing, because it is carried out by real users before release
  • L(c) Acceptance testing, because the client confirms that it meets the requirements they ordered
Model answer

(a) is alpha testing, because it is carried out by staff inside the company, who are not the programmers, before anyone outside sees the app. (b) is beta testing, because a group of real users outside the company try the app in their own environment before the general release. (c) is acceptance testing, because the client checks that the app does what they asked for, and accepts it.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
3A function is_palindrome(text) returns True if the text reads the same forwards and backwards, ignoring case, and False otherwise. Write a unittest test case class with at least three tests that use normal, boundary and erroneous data. Include the function as well.5 marks
Write

Mark scheme0 / 5
  • LImports unittest, and a class that inherits from unittest.TestCase
  • LTest methods whose names start with test
  • LA normal data test, such as racecar or Level, with assertTrue or assertFalse
  • LA boundary test, such as an empty string or one character
  • LA test for erroneous or unusual data, using assertRaises or a check of the expected result
Model answer
Python
import unittest

def is_palindrome(text):
    cleaned = text.lower()
    return cleaned == cleaned[::-1]

class TestIsPalindrome(unittest.TestCase):
    def test_normal(self):
        self.assertTrue(is_palindrome("racecar"))
        self.assertTrue(is_palindrome("Level"))
        self.assertFalse(is_palindrome("python"))

    def test_boundary(self):
        self.assertTrue(is_palindrome(""))
        self.assertTrue(is_palindrome("a"))
        self.assertFalse(is_palindrome("ab"))

    def test_erroneous(self):
        with self.assertRaises(AttributeError):
            is_palindrome(12321)

suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestIsPalindrome)
result = unittest.TestResult()
suite.run(result)
print(result.testsRun, "tests,", len(result.failures) + len(result.errors), "problems")

The normal test uses words that are and are not palindromes, including a capital letter. The boundary test uses an empty string, one character and the shortest string that is not a palindrome. The erroneous test passes a number, which has no lower method, and checks that the function fails with an error rather than giving an answer. The three tests all pass.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
4Explain how test driven development works, and give one advantage of working in this way.4 marks
Explain

Mark scheme0 / 4
  • LA test is written first, for a feature that does not yet exist
  • LThe test is run and fails, which shows that it can detect the absence of the feature
  • LThe simplest code needed to make the test pass is then written, and the code is tidied up afterwards
  • LAn advantage, such as all of the code being covered by tests, or tests protecting later changes
Model answer

In test driven development a test for a small piece of behaviour is written first, before the code exists. The test is run and fails, which shows that the test can detect the missing feature. The simplest code that makes the test pass is then written, and once it passes the code is refactored, which means tidied up with the tests still passing. The cycle repeats for the next feature. An advantage is that all of the code is covered by tests, so later changes can be made with confidence, because the tests show straight away if anything has been broken.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
5A programmer fixes a fault in the calculation of delivery charges in an online shop. Explain why the programmer should carry out regression testing before the shop is updated.3 marks
Explain

Mark scheme0 / 3
  • LRegression testing repeats the earlier tests on the program after it has been changed
  • LA change in one part of a program can cause a fault in a part that was working
  • LIt confirms that the delivery charge fix has not affected, for example, the basket total or the payment
Model answer

Regression testing means repeating the earlier tests on the program after it has been changed. A change to one part of a program can cause a fault in another part that was working before, for example the fix to the delivery charge might alter the basket total or the payment. By repeating the tests, the programmer confirms that everything that worked before still works before the update is released.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
Machine mark 0 / 19