ЁЯФМ рдзрдбрд╛ 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
тЭУ рдХрд╛рдп
- Integration test тАФ рддреБрдордЪрд╛ code рдПрдЦрд╛рджреНрдпрд╛ рдЦрд▒реНрдпрд╛ рдЧреЛрд╖реНрдЯреАрд╕реЛрдмрдд рдЪрд╛рд▓рддреЛ рдХрд╛ рддреЗ рддрдкрд╛рд╕рдгрд╛рд▒реНрдпрд╛ tests: database, file system, message queue, framework. Unit test рдкреЗрдХреНрд╖рд╛ рд╣рд│реВ, рдкрдг рдЬреЛрдбрдгреАрдЪреНрдпрд╛ рдЬрд╛рдЧрд╛рдВрдмрджреНрджрд▓ рдХрд┐рддреАрддрд░реА рдЬрд╛рд╕реНрдд рдкреНрд░рд╛рдорд╛рдгрд┐рдХ.
- Narrow рд╡рд┐рд░реБрджреНрдз broad integration тАФ narrow: рддреБрдордЪрд╛ code рдЖрдгрд┐ рдПрдХ рдЦрд░реА dependency (рд╣рд╛ рдзрдбрд╛). Broad: рдПрдХрд╛рдЪ рд╡реЗрд│реА рдЕрдиреЗрдХ рдЦрд▒реНрдпрд╛ services тАФ end-to-end рдЪреНрдпрд╛ рдЬрд╡рд│ (рдзрдбрд╛ 08).
- In-memory database тАФ SQLite рдЪреЗ
":memory:"RAM рдордзреНрдпреЗ рдкреВрд░реНрдг database рддрдпрд╛рд░ рдХрд░рддреЗ, рдЬреЛ connection рдмрдВрдж рдЭрд╛рд▓реНрдпрд╛рд╡рд░ рдирд╛рд╣реАрд╕рд╛ рд╣реЛрддреЛ. Laptop рд╡рд░ рдкреНрд░рддреНрдпреЗрдХ test рд╕рд╛рдареА рдирд╡рд╛ database рддрдпрд╛рд░ рдХрд░рд╛рдпрд▓рд╛ рдПрдХрд╛ millisecond рдкреЗрдХреНрд╖рд╛ рдЦреВрдкрдЪ рдХрдореА рд╡реЗрд│ рд▓рд╛рдЧрддреЛ, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ test рд▓рд╛ рд╕реНрд╡рдЪреНрдЫ data рдорд┐рд│рддреЛ. - SQL рдордзрд▓реЗ integer division тАФ SQLite рдордзреНрдпреЗ (рдЖрдгрд┐ PostgreSQL рдордзреНрдпреЗ),
INTEGER / INTEGERрдЪреЗ рдЙрддреНрддрд░ INTEGER рдпреЗрддреЗ:7/2рдореНрд╣рдгрдЬреЗ3.AVG()floating-point рдирд┐рдХрд╛рд▓ рджреЗрддреЗ;7.0/2рд╕реБрджреНрдзрд╛. - Constraint тАФ database рдкрд╛рд│рд╛рдпрд▓рд╛ рд▓рд╛рд╡рддреЛ рдЕрд╕рд╛ рдирд┐рдпрдо:
PRIMARY KEY (student, subject)рддреНрдпрд╛рдЪ рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдЖрдгрд┐ рд╡рд┐рд╖рдпрд╛рд╕рд╛рдареА рджреБрд╕рд░реА rowsqlite3.IntegrityErrorрдиреЗ рдирд╛рдХрд╛рд░рддреЛ. - Type affinity тАФ SQLite рдЪреЗ columns рдврд┐рд▓реЗ typed рдЕрд╕рддрд╛рдд: INTEGER column рдордзреНрдпреЗ рдШрд╛рддрд▓реЗрд▓рд╛ text
'71'integer 71 рдордзреНрдпреЗ рдмрджрд▓рд▓рд╛ рдЬрд╛рддреЛ, рдЖрдгрд┐ рдЖрдХрдбреНрдпрд╛рд╕рд╛рд░рдЦрд╛ рди рджрд┐рд╕рдгрд╛рд░рд╛ text ('absent') рд╕рд░рд│ text рдореНрд╣рдгреВрдирдЪ рд╕рд╛рдард╡рд▓рд╛ рдЬрд╛рддреЛ. PostgreSQL integer column рд╕рд╛рдареА'absent'error рджреЗрдКрди рдирд╛рдХрд╛рд░рддреЗ. SQLite рд╡рд░рдЪреА test рдореНрд╣рдгрдЬреЗ PostgreSQL рд╡рд░рдЪреА test рдирд╡реНрд╣реЗ. - Testcontainers тАФ in-memory рдкрд░реНрдпрд╛рдп рдкреБрд░реЗрд╕рд╛ рдЬрд╡рд│рдЪрд╛ рдирд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛, test run рдЪреНрдпрд╛ рдХрд╛рд▓рд╛рд╡рдзреАрдкреБрд░рддрд╛ Docker container рдордзреНрдпреЗ рдЦрд░рд╛ database (рдХрд┐рдВрд╡рд╛ queue) рд╕реБрд░реВ рдХрд░рдгрд╛рд░реА library.
ЁЯдФ рдХрд╛
рдХрд╛рд░рдг рдЕрдиреЗрдХ 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 рдХрд░рддрд╛ рддреНрдпрд╛рд╡рд░ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рд╕рд╛рдкрдбреВ рд╢рдХрддреЗ рддреЗ рдард░рддреЗ.
тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛
- SQL рдлрдХреНрдд fake рдХрд┐рдВрд╡рд╛ database driver рдЪреНрдпрд╛ mock рдордзреВрдирдЪ test рдХрд░рдгреЗ
- рд╕рдВрдкреВрд░реНрдг suite рд╕рд╛рдареА рдПрдХрдЪ shared database, рдореНрд╣рдгрдЬреЗ tests рдПрдХрдореЗрдХрд╛рдВрдЪреНрдпрд╛ rows рдкрд╛рд╣рддрд╛рдд
- SQLite рд╡рд░ test рдХрд░рдгреЗ рдЖрдгрд┐ PostgreSQL рд╡рд░ рдЪрд╛рд▓рд╡рдгреЗ тАФ рд╡реЗрдЧрд│реЗ types, рд╡реЗрдЧрд│рд╛ SQL
- logic рдЪреНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ рд╢рд╛рдЦреЗрд╕рд╛рдареА integration tests (рддреНрдпрд╛ рд╡реЗрдЧрд╡рд╛рди unit tests рдордзреНрдпреЗ рдареЗрд╡рд╛)
- рд░рд┐рдХрд╛рдорд╛ case, duplicate case рдЖрдгрд┐ constraint errors 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