🎓 A1Learner
🔍
0 of 102 lessons completed

Python Testing Patterns

A testing pattern is a shared habit for writing a test. Arrange-Act-Assert gives each test the same three-part shape.

The Arrange-Act-Assert shape

Three short parts, in the same order every time.

def word_count(text):
    return len(text.split())

def test_word_count():
    # Arrange
    text = "one two"
    # Act
    result = word_count(text)
    # Assert
    assert result == 2

Pure functions are the easiest tests

Same input, same output, no side effects to clean up.

def total_price(prices):
    return sum(prices)

print(total_price([10, 20, 12]))
print(total_price([]))

Read a failing test

This test fails on purpose. The count of failures is the useful news here.

import io
import unittest

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(2 + 2, 5)

suite = unittest.TestLoader().loadTestsFromTestCase(TestMath)
stream = io.StringIO()
result = unittest.TextTestRunner(stream=stream).run(suite)
print("Failures:", len(result.failures))

Example

import io
import unittest

# The messy version: one long function, repeated counting.
def analyse(text):
    words = text.split()
    total = 0
    for w in words:
        total = total + 1
    chars = 0
    for ch in text:
        chars = chars + 1
    longest = ""
    for w in words:
        if len(w) > len(longest):
            longest = w
    return {"words": total, "chars": chars, "longest": longest}

class TestAnalyse(unittest.TestCase):
    def test_counts_words(self):
        self.assertEqual(analyse("one two three")["words"], 3)

    def test_counts_chars(self):
        self.assertEqual(analyse("hello")["chars"], 5)

    def test_finds_longest(self):
        self.assertEqual(analyse("a bb ccc")["longest"], "ccc")

def run_suite(label):
    suite = unittest.TestLoader().loadTestsFromTestCase(TestAnalyse)
    stream = io.StringIO()
    result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)
    print(f"{label}: {result.testsRun} tests, all passed: {result.wasSuccessful()}")

run_suite("Before refactor")

# The clean version: small helpers, same behaviour.
def analyse(text):
    words = text.split()
    longest = max(words, key=len, default="")
    return {"words": len(words), "chars": len(text), "longest": longest}

run_suite("After refactor")

Key points

  • Arrange-Act-Assert: set up the data, run the code, then check the result.
  • Pure function: a function whose result depends only on its inputs.
  • Test pure functions first.
  • They need no fixtures and no mocks.
  • A failing test is information.
  • Read which value was expected and which value came out.
  • Coverage: a measure of how much of the code the tests run.
  • An external tool measures it.
  • Tests are a safety net.
  • They let you change code and learn at once if behaviour broke.
  • Refactoring changes how code looks, never what it does.
  • Testing patterns are shared habits that give tests a familiar shape.

In this example, a messy analyse function comes with three tests. The helper run_suite runs the tests quietly and prints one summary line. The first line shows the suite green before any change. Then analyse is written again in a clean way, with the same name and the same results. The tests look the name up fresh on each run, so they test the new version. The second line shows the suite still green. That is the safety net at work. The refactor changed the code, and the tests proved the behaviour stayed the same.

Note

If behaviour must change, write a new test first.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Python Playground

Exercise

In Arrange-Act-Assert, which step runs the code being tested?