ЁЯПл The SchoolтА║ЁЯУЭ TestingтА║ЁЯФМ рдзрдбрд╛ 06 тАФ Integration tests: memory рдордзрд▓рд╛ рдЦрд░рд╛ database
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФМ рдзрдбрд╛ 06 тАФ Integration tests: memory рдордзрд▓рд╛ рдЦрд░рд╛ database

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-test-doubles ┬╖ рдкреБрдвреЗ: lesson-07-contract-tests


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ integration tests: рддреБрдордЪрд╛ code рдПрдХрд╛ рдЦрд▒реНрдпрд╛ dependency рд╕реЛрдмрдд тАФ рдЗрдереЗ рдкреНрд░рддреНрдпреЗрдХ test рд╕рд╛рдареА memory рдордзреНрдпреЗ рдирд╡реНрдпрд╛рдиреЗ рддрдпрд╛рд░ рдХреЗрд▓реЗрд▓рд╛ рдЦрд░рд╛ SQLite database. exam/grades.py рдордзрд▓рд╛ GradeStore рдЧреБрдг save рдХрд░рддреЛ рдЖрдгрд┐ SQL рдордзреНрдпреЗ class average рдореЛрдЬрддреЛ; рддреНрдпрд╛рдЪреНрдпрд╛ рдкрд╣рд┐рд▓реНрдпрд╛ version рдордзреНрдпреЗ рдЕрд╕рд╛ bug рдЖрд╣реЗ рдЬреЛ Python fake рдХрдзреАрдЪ рджрд╛рдЦрд╡реВ рд╢рдХрд▓рд╛ рдирд╕рддрд╛. exam/suites/test_store_sqlite.py рдЖрдгрд┐ exam/demo.py рдордзрд▓реЗ integration().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХрддрд░рд┐рдирд╛рд▓рд╛ maths рдордзреНрдпреЗ 68 рдорд┐рд│рд╛рд▓реЗ. рджреАрдкрд┐рдХрд╛рд▓рд╛ 67. Class average рдЖрд╣реЗ 67.5.

рдзрдбрд╛ 05 рдЪреНрдпрд╛ рдкрджреНрдзрддреАрдиреЗ, рджреАрдкрд┐рдХрд╛рдиреЗ рдЖрдкрд▓реЗ average рдПрдХрд╛ рдЦреЛрдЯреНрдпрд╛ marks book рд╡рд░ test рдХреЗрд▓реЗ: рдПрдХ Python dict. рддреНрдпрд╛рдиреЗ рд╕рд╛рдВрдЧрд┐рддрд▓реЗ 67.5. тЬЕ

рдордЧ рдРрд╢реНрд╡рд░реНрдпрд╛рдиреЗ рддреЛрдЪ рдкреНрд░рд╢реНрди рдЦрд▒реНрдпрд╛ marks book рд╡рд░ рдЪрд╛рд▓рд╡рд▓рд╛ тАФ SQL database. рддреНрдпрд╛рдиреЗ рд╕рд╛рдВрдЧрд┐рддрд▓реЗ 67. тЭМ

рдХрд╛? рджреАрдкрд┐рдХрд╛рдЪрд╛ SQL рдореНрд╣рдгрдд рд╣реЛрддрд╛ "рдЧреБрдгрд╛рдВрдЪреА рдмреЗрд░реАрдЬ рдХрд░рд╛, рдХрд┐рддреА рдЖрд╣реЗрдд рддреНрдпрд╛рдиреЗ рднрд╛рдЧрд╛". SQLite рдордзреНрдпреЗ, рдкреВрд░реНрдг рд╕рдВрдЦреНрдпреЗрд▓рд╛ рдкреВрд░реНрдг рд╕рдВрдЦреНрдпреЗрдиреЗ рднрд╛рдЧрд▓реЗ рдХреА рдкреВрд░реНрдг рд╕рдВрдЦреНрдпрд╛рдЪ рдорд┐рд│рддреЗ. 135 ├╖ 2 = 67. рдЕрд░реНрдзрд╛ рднрд╛рдЧ рдЯрд╛рдХреВрди рджрд┐рд▓рд╛ рдЬрд╛рддреЛ.

рдЦреЛрдЯреНрдпрд╛ marks book рдиреЗ рдмреЗрд░реАрдЬ Python рдордзреНрдпреЗ рдХреЗрд▓реА, рдЬрд┐рдереЗ 135 / 2 рдореНрд╣рдгрдЬреЗ 67.5. рдореНрд╣рдгреВрди рдЦреЛрдЯреНрдпрд╛ book рдордзреНрдпреЗ рд╣рд╛ bug рдпреЗрдКрдЪ рд╢рдХрдд рдирд╡реНрд╣рддрд╛. рдлрдХреНрдд рдЦрд▒реНрдпрд╛рдордзреНрдпреЗрдЪ рдпреЗрдК рд╢рдХрдд рд╣реЛрддрд╛.

рдЖрдгрд┐ рдЖрдгрдЦреА рдПрдХ: рдЦрд░реЗ book рд╕рд╛рдВрдЧрддреЗ рдХреА рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдЪрд╛ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рд╖рдпрд╛рдд рдПрдХрдЪ mark рдЕрд╕рддреЛ. рдХрддрд░рд┐рдирд╛рдЪреЗ maths рджреЛрдирджрд╛ save рдХрд░рдгреЗ рдирд╛рдХрд╛рд░рд▓реЗ рдЬрд╛рддреЗ. рдЦреЛрдЯреНрдпрд╛ book рдиреЗ рддреЗ рдЧреБрдкрдЪреВрдк overwrite рдХреЗрд▓реЗ. ЁЯУХ

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    t["ЁЯзк test: Katrina 68, Dipika 67<br/>expected average 67.5"]
    t --> fake["ЁЯз╕ Python fake store<br/>135 / 2 = 67.5 тЬЕ"]
    t --> real["ЁЯЧДя╕П real SQLite :memory:<br/>SUM/COUNT = 67 тЭМ"]
    real --> fix["AVG(marks) = 67.5 тЬЕ"]
    real --> pk["save Katrina twice<br/>IntegrityError"]
    fake --> ow["save twice<br/>silently overwritten"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/testing/lesson-diagrams.html#l06

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдЕрдиреЗрдХ bugs рддреБрдордЪрд╛ code рдЖрдгрд┐ рдЦрд░реА рдЧреЛрд╖реНрдЯ рдпрд╛рдВрдЪреНрдпрд╛ рдордзреНрдпреЗ рд░рд╛рд╣рддрд╛рдд: рддреБрдореНрд╣рд╛рд▓рд╛ рд╡рд╛рдЯрддреЗ рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рд╡реЗрдЧрд│рд╛ рдЕрд░реНрде рдЕрд╕рд▓реЗрд▓рд╛ SQL, fake рдордзреНрдпреЗ рдХрдзреАрдЪ рдирд╕рд▓реЗрд▓реА constraint, рдмрджрд▓рд▓рд╛ рдЬрд╛рдгрд╛рд░рд╛ type, commit рди рдЭрд╛рд▓реЗрд▓рд╛ transaction. Fake рдореНрд╣рдгрдЬреЗ dependency рдмрджреНрджрд▓рдЪрд╛ рддреБрдордЪрд╛ рд╡рд┐рд╢реНрд╡рд╛рд╕; integration test рддреЛ рд╡рд┐рд╢реНрд╡рд╛рд╕ рддрдкрд╛рд╕рддреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

exam/grades.py рдордзрд▓реЗ GradeStore(":memory:") standard library рдЪреНрдпрд╛ sqlite3 module рдиреЗ SQLite connection рдЙрдШрдбрддреЗ рдЖрдгрд┐ PRIMARY KEY (student, subject) рдЕрд╕рд▓реЗрд▓реЗ marks table рддрдпрд╛рд░ рдХрд░рддреЗ. class_average_v1() SUM(marks) / COUNT(*) рдЪрд╛рд▓рд╡рддреЗ; class_average() AVG(marks) рдЪрд╛рд▓рд╡рддреЗ. Unittest file setUp рдордзреНрдпреЗ рдирд╡рд╛ store рддрдпрд╛рд░ рдХрд░рддреЗ рдЖрдгрд┐ tearDown рдордзреНрдпреЗ рдмрдВрдж рдХрд░рддреЗ тАФ рдзрдбрд╛ 04 рдЪреА рдирд╡реА рдЙрддреНрддрд░рдкрддреНрд░рд┐рдХрд╛, рдЖрддрд╛ рдкреВрд░реНрдг database.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 exam/demo.py integration
python3 - <<'EOF'
import sqlite3
from exam.grades import GradeStore
s = GradeStore()
for who, m in (("Katrina", 68), ("Dipika", 67), ("Aishwarya", 70)): s.save(who, "maths", m)
print("3 pupils: v1", s.class_average_v1("maths"), "┬╖ fixed", round(s.class_average("maths"), 2))
print("SQLite says 7/2 =", s.db.execute("SELECT 7/2, 7.0/2, typeof(7/2)").fetchone())
print("empty subject: v1", s.class_average_v1("art"), "┬╖ fixed", s.class_average("art"))
s.db.execute("INSERT INTO marks VALUES ('Dipika', 'science', '71')")      # text that looks like a number
s.db.execute("INSERT INTO marks VALUES ('Katrina', 'science', 'absent')")  # text that does not
print("stored types:", s.db.execute("SELECT student, typeof(marks) FROM marks WHERE subject='science' ORDER BY student").fetchall())
EOF
python3 -m unittest exam.suites.test_store_sqlite 2>&1 | tail -1

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

integration рдЫрд╛рдкрддреЗ:

тФАтФА Katrina 68 and Dipika 67 in maths; the class average should be 67.5
   unit test with a Python fake store тЖТ 67.5 тЬЕ (Python's / keeps the half)
   integration test, REAL SQLite in memory, v1 SQL SUM/COUNT тЖТ 67 тЭМ (INTEGER / INTEGER drops it)
   fixed SQL AVG(marks) тЖТ 67.5 тЬЕ
   save Katrina's maths twice: fake silently overwrites ┬╖ SQLite тЖТ IntegrityError (the PRIMARY KEY the fake never had)
тФАтФА exam/suites/test_store_sqlite.py: 3 tests ┬╖ 0 failing ┬╖ a fresh :memory: database per test

рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:

3 pupils: v1 68 ┬╖ fixed 68.33
SQLite says 7/2 = (3, 3.5, 'integer')
empty subject: v1 None ┬╖ fixed None
stored types: [('Dipika', 'integer'), ('Katrina', 'text')]

рдЖрдгрд┐ unittest рд╢реЗрд╡рдЯреА рдЫрд╛рдкрддреЗ:

OK

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

V1 SQL рдмрд╣реБрддреЗрдХ classes рд╕рд╛рдареА рдЪреБрдХреАрдЪрд╛ рдЖрд╣реЗ (рддреАрди рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреАрдВрд╕рд╛рдареА 68.33 рдРрд╡рдЬреА 68) рдЖрдгрд┐ fake рд╡рд╛рдкрд░рдгрд╛рд▒реНрдпрд╛ unit test рд▓рд╛ рддреЛ рджрд┐рд╕реВ рд╢рдХрд▓рд╛ рдирд╛рд╣реА, рдХрд╛рд░рдг fake рдиреЗ рднрд╛рдЧрд╛рдХрд╛рд░ Python рдордзреНрдпреЗ рдХреЗрд▓рд╛. рдЦрд▒реНрдпрд╛ database рдиреЗ рдЖрдгрдЦреА рджреЛрди рд╡рд░реНрддрдиреЗ рджрд╛рдЦрд╡рд▓реА рдЬреНрдпрд╛рдВрдЪреА рд╕реНрд╡рддрдВрддреНрд░ test рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡реА: рд░рд┐рдХрд╛рдорд╛ рд╡рд┐рд╖рдп 0 рдирд╡реНрд╣реЗ рддрд░ None рджреЗрддреЛ (рдореНрд╣рдгреВрди рдмреЛрд▓рд╡рдгрд╛рд▒реНрдпрд╛рдиреЗ "рдЕрдЬреВрди рдЧреБрдг рдирд╛рд╣реАрдд" рд╣рд╛рддрд╛рд│рд╛рдпрд▓рд╛ рд╣рд╡реЗ), рдЖрдгрд┐ SQLite рдиреЗ text '71' рд▓рд╛ рдЖрдХрдбреНрдпрд╛рдд рдмрджрд▓рд▓реЗ рдкрдг INTEGER рдореНрд╣рдгреВрди declare рдХреЗрд▓реЗрд▓реНрдпрд╛ column рдордзреНрдпреЗ 'absent' рд╣рд╛ рд╢рдмреНрдж рддрд╕рд╛рдЪ рдареЗрд╡рд▓рд╛ тАФ PostgreSQL рдиреЗ рддреА row рдирд╛рдХрд╛рд░рд▓реА рдЕрд╕рддреА. рддреБрдореНрд╣реА рдХреЛрдгрддреНрдпрд╛ engine рд╡рд░ test рдХрд░рддрд╛ рддреНрдпрд╛рд╡рд░ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рд╕рд╛рдкрдбреВ рд╢рдХрддреЗ рддреЗ рдард░рддреЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ Testcontainers рд╕рд╣ рдкреНрд░рддреНрдпреЗрдХ test run рд╕рд╛рдареА рдЦрд░рд╛ PostgreSQL (pip install testcontainers[postgres] psycopg; Docker рд▓рд╛рдЧрддреЛ):

from testcontainers.postgres import PostgresContainer
import psycopg

def test_class_average_on_postgres():
    with PostgresContainer("postgres:16") as pg:
        url = pg.get_connection_url(driver=None)
        with psycopg.connect(url) as conn:
            conn.execute("CREATE TABLE marks (student text, subject text, marks int, PRIMARY KEY (student, subject))")
            conn.execute("INSERT INTO marks VALUES ('Katrina', 'maths', 68), ('Dipika', 'maths', 67)")
            assert conn.execute("SELECT SUM(marks) / COUNT(*) FROM marks").fetchone()[0] == 67      # int / int
            assert float(conn.execute("SELECT AVG(marks) FROM marks").fetchone()[0]) == 67.5

Container рд╕реБрд░реВ рд╡реНрд╣рд╛рдпрд▓рд╛ рдХрд╛рд╣реА рд╕реЗрдХрдВрдж рд▓рд╛рдЧрддрд╛рдд, рдореНрд╣рдгреВрди рдПрдХрд╛ session рд╕рд╛рдареА рдПрдХрдЪ share рдХрд░рд╛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ test рд▓рд╛ рд╢реЗрд╡рдЯреА rolled back рд╣реЛрдгрд╛рд░рд╛ transaction рджреНрдпрд╛ тАФ рдирд╡рд╛ server рди рд▓рд╛рд╡рддрд╛ рдирд╡рд╛ data. рд╣рд╛рдЪ pattern Java, Go, .NET рдЖрдгрд┐ Node.js рд╕рд╛рдареАрд╣реА рдЖрд╣реЗ (Testcontainers рдХрдбреЗ рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА libraries рдЖрд╣реЗрдд), рдЖрдгрд┐ GitHub Actions рд╕рд╛рд░рдЦреНрдпрд╛ CI services database service container рдореНрд╣рдгреВрди рдЪрд╛рд▓рд╡реВ рд╢рдХрддрд╛рдд.

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдХрд╛рд╣реАрддрд░реА рдореЛрдЬрдгрд╛рд▒реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ query рд╕рд╛рдареА (рдПрдХ average, рдПрдХреВрдг рдмреЗрд░реАрдЬ, rank), production рдордзреНрдпреЗ рддреБрдореНрд╣реА рдЪрд╛рд▓рд╡рддрд╛ рддреНрдпрд╛рдЪ database engine рдЖрдгрд┐ version рд╡рд░ рдХрд┐рдорд╛рди рдПрдХ integration test рдареЗрд╡рд╛.

тПня╕П рдкреБрдвреЗ

Attendance service рджреБрд╕рд▒реНрдпрд╛ team рдЪреА рдЖрд╣реЗ. рддреА рддреБрдореНрд╣реА рддреБрдордЪреНрдпрд╛ tests рдордзреНрдпреЗ рдЪрд╛рд▓рд╡реВ рд╢рдХрдд рдирд╛рд╣реА, рдЖрдгрд┐ рддреБрдордЪрд╛ stub рдЦреЛрдЯреЗ рдмреЛрд▓рдд рдЕрд╕реВ рд╢рдХрддреЛ. Contract tests рджреЛрдиреНрд╣реА рдмрд╛рдЬреВрдВрдирд╛ рдЙрддреНрддрд░ рдХрд╕реЗ рджрд┐рд╕рддреЗ рдпрд╛рд╡рд░ рд╕рд╣рдордд рдХрд░рддрд╛рдд.

git checkout lesson-07-contract-tests

ЁЯФМ Lesson 06 тАФ Integration tests: a real database in memory

ЁЯУН You are here: Lesson 06 of 12 ┬╖ Previous: lesson-05-test-doubles ┬╖ Next: lesson-07-contract-tests


ЁЯУж What's in this branch

Lessons 01тАУ05, plus integration tests: your code together with a real dependency тАФ here a real SQLite database, created fresh in memory for every test. GradeStore in exam/grades.py saves marks and computes a class average in SQL; its first version has a bug that a Python fake could never show. exam/suites/test_store_sqlite.py and integration() in exam/demo.py.

ЁЯзТ Explain like I'm 5

Katrina got 68 in maths. Dipika got 67. The class average is 67.5.

In lesson 05's style, Dipika tested her average with a pretend marks book: a Python dict. It said 67.5. тЬЕ

Then Aishwarya ran the same question against the real marks book тАФ the SQL database. It said 67. тЭМ

Why? Dipika's SQL said "add up the marks, divide by how many". In SQLite, a whole number divided by a whole number gives a whole number. 135 ├╖ 2 = 67. The half is thrown away.

The pretend marks book did the sum in Python, where 135 / 2 is 67.5. So the pretend book could not have this bug. Only the real one could.

And one more: the real book says each pupil has one mark per subject. Saving Katrina's maths twice is refused. The pretend book quietly overwrote it. ЁЯУХ

ЁЯЧ║я╕П Diagram

flowchart LR
    t["ЁЯзк test: Katrina 68, Dipika 67<br/>expected average 67.5"]
    t --> fake["ЁЯз╕ Python fake store<br/>135 / 2 = 67.5 тЬЕ"]
    t --> real["ЁЯЧДя╕П real SQLite :memory:<br/>SUM/COUNT = 67 тЭМ"]
    real --> fix["AVG(marks) = 67.5 тЬЕ"]
    real --> pk["save Katrina twice<br/>IntegrityError"]
    fake --> ow["save twice<br/>silently overwritten"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/testing/lesson-diagrams.html#l06

тЭУ What

ЁЯдФ Why

Because many bugs live between your code and the real thing: SQL that means something different from what you think, a constraint the fake never had, a type that is converted, a transaction that is not committed. A fake is your belief about the dependency; an integration test checks the belief.

ЁЯФз How (in this repo)

GradeStore(":memory:") in exam/grades.py opens a SQLite connection with the standard-library sqlite3 module and creates a marks table with a PRIMARY KEY (student, subject). class_average_v1() runs SUM(marks) / COUNT(*); class_average() runs AVG(marks). The unittest file creates a new store in setUp and closes it in tearDown тАФ lesson 04's fresh answer sheet, now a whole database.

ЁЯзк Try it

python3 exam/demo.py integration
python3 - <<'EOF'
import sqlite3
from exam.grades import GradeStore
s = GradeStore()
for who, m in (("Katrina", 68), ("Dipika", 67), ("Aishwarya", 70)): s.save(who, "maths", m)
print("3 pupils: v1", s.class_average_v1("maths"), "┬╖ fixed", round(s.class_average("maths"), 2))
print("SQLite says 7/2 =", s.db.execute("SELECT 7/2, 7.0/2, typeof(7/2)").fetchone())
print("empty subject: v1", s.class_average_v1("art"), "┬╖ fixed", s.class_average("art"))
s.db.execute("INSERT INTO marks VALUES ('Dipika', 'science', '71')")      # text that looks like a number
s.db.execute("INSERT INTO marks VALUES ('Katrina', 'science', 'absent')")  # text that does not
print("stored types:", s.db.execute("SELECT student, typeof(marks) FROM marks WHERE subject='science' ORDER BY student").fetchall())
EOF
python3 -m unittest exam.suites.test_store_sqlite 2>&1 | tail -1

тЬЕ Verify тАФ what you should see

integration prints:

тФАтФА Katrina 68 and Dipika 67 in maths; the class average should be 67.5
   unit test with a Python fake store тЖТ 67.5 тЬЕ (Python's / keeps the half)
   integration test, REAL SQLite in memory, v1 SQL SUM/COUNT тЖТ 67 тЭМ (INTEGER / INTEGER drops it)
   fixed SQL AVG(marks) тЖТ 67.5 тЬЕ
   save Katrina's maths twice: fake silently overwrites ┬╖ SQLite тЖТ IntegrityError (the PRIMARY KEY the fake never had)
тФАтФА exam/suites/test_store_sqlite.py: 3 tests ┬╖ 0 failing ┬╖ a fresh :memory: database per test

Your snippet prints:

3 pupils: v1 68 ┬╖ fixed 68.33
SQLite says 7/2 = (3, 3.5, 'integer')
empty subject: v1 None ┬╖ fixed None
stored types: [('Dipika', 'integer'), ('Katrina', 'text')]

and unittest ends with:

OK

ЁЯПБ What you just proved

The v1 SQL is wrong for most classes (68 instead of 68.33 for three pupils) and a unit test with a fake could not see it, because the fake did the division in Python. The real database also showed two behaviours worth a test of their own: an empty subject gives None, not 0 (so the caller must handle "no marks yet"), and SQLite turned the text '71' into a number but kept the word 'absent' in a column declared INTEGER тАФ PostgreSQL would have refused that row. The engine you test on shapes what you can find.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ a real PostgreSQL per test run with Testcontainers (pip install testcontainers[postgres] psycopg; needs Docker):

from testcontainers.postgres import PostgresContainer
import psycopg

def test_class_average_on_postgres():
    with PostgresContainer("postgres:16") as pg:
        url = pg.get_connection_url(driver=None)
        with psycopg.connect(url) as conn:
            conn.execute("CREATE TABLE marks (student text, subject text, marks int, PRIMARY KEY (student, subject))")
            conn.execute("INSERT INTO marks VALUES ('Katrina', 'maths', 68), ('Dipika', 'maths', 67)")
            assert conn.execute("SELECT SUM(marks) / COUNT(*) FROM marks").fetchone()[0] == 67      # int / int
            assert float(conn.execute("SELECT AVG(marks) FROM marks").fetchone()[0]) == 67.5

Starting a container takes seconds, so share one per session and give each test a transaction that is rolled back at the end тАФ fresh data without a fresh server. The same pattern exists for Java, Go, .NET and Node.js (Testcontainers has libraries for each), and CI services such as GitHub Actions can run a database as a service container.

ЁЯПн Why this matters in production: for every query that computes something (an average, a total, a rank), keep at least one integration test against the same database engine and version you run in production.

тПня╕П Next

The attendance service belongs to another team. You cannot run it in your tests, and your stub may be lying. Contract tests make both sides agree on what the answer looks like.

git checkout lesson-07-contract-tests
тЖР Previoustest doublesNext тЖТcontract tests

This page is the lesson's README from the lesson-06-integration branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.