ЁЯдЭ рдзрдбрд╛ 07 тАФ Contract tests: consumer рдЪреНрдпрд╛ рдЕрдкреЗрдХреНрд╖рд╛ рд╡рд┐рд░реБрджреНрдз provider рдЪреЗ response
ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 07 ┬╖ рдорд╛рдЧреЗ: lesson-06-integration ┬╖ рдкреБрдвреЗ: lesson-08-e2e-pyramid
ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ
рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ contract tests: report-card code (consumer) attendance service
(provider) рдХрдбреВрди рддреНрдпрд╛рд▓рд╛ рдиреЗрдордХреЗ рдХрд╛рдп рд▓рд╛рдЧрддреЗ рддреЗ рд▓рд┐рд╣реВрди рдареЗрд╡рддреЛ, рдЖрдгрд┐ provider рдкреНрд░рддреНрдпреЗрдХ release
ship рд╣реЛрдгреНрдпрд╛рдкреВрд░реНрд╡реА рддреНрдпрд╛ рдпрд╛рджреАрд╢реА рддрдкрд╛рд╕рддреЛ. exam/lab.py рдордзрд▓реЗ CONTRACT рдЖрдгрд┐
verify_contract(); exam/demo.py рдордзрд▓реЗ contract().
ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ
рдХрддрд░рд┐рдирд╛рдЪреНрдпрд╛ report cards рдирд╛ attendance office рдХрдбреВрди рджреЛрди рдЖрдХрдбреЗ рд▓рд╛рдЧрддрд╛рдд: рдЙрдкрд╕реНрдерд┐рдд рджрд┐рд╡рд╕ рдЖрдгрд┐ рдПрдХреВрдг рджрд┐рд╡рд╕. рддреА рддреЗ рдПрдХрд╛ рдХрд╛рд░реНрдбрд╡рд░ рд▓рд┐рд╣рд┐рддреЗ рдЖрдгрд┐ attendance office рдЪреНрдпрд╛ рднрд┐рдВрддреАрд╡рд░ рд▓рд╛рд╡рддреЗ: ЁЯдЭ
"рдореА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА 42 рдмрджреНрджрд▓ рд╡рд┐рдЪрд╛рд░рддреЗ рддреЗрд╡реНрд╣рд╛ рдорд▓рд╛ рдкрд░рдд рд╣рд╡реЗ:
days_present(рдкреВрд░реНрдг рд╕рдВрдЦреНрдпрд╛) рдЖрдгрд┐days_total(рдкреВрд░реНрдг рд╕рдВрдЦреНрдпрд╛)."
рддреЗ рдХрд╛рд░реНрдб рдореНрд╣рдгрдЬреЗ contract.
Attendance office рдЖрдкрд▓рд╛ form рдмрджрд▓рдгреНрдпрд╛рдкреВрд░реНрд╡реА, рддреЛ form рдХрддрд░рд┐рдирд╛рдЪреНрдпрд╛ рдХрд╛рд░реНрдбрд╢реА рддрдкрд╛рд╕рддреЗ.
- v2 рдПрдХ рдирд╡реА рдЪреМрдХрдЯ рдЬреЛрдбрддреЗ, "late days". рдХрддрд░рд┐рдирд╛рдиреЗ рддреА рдХрдзреАрдЪ рдорд╛рдЧрд┐рддрд▓реА рдирд╡реНрд╣рддреА, рдореНрд╣рдгреВрди рддрд┐рд▓рд╛ рд╣рд░рдХрдд рдирд╛рд╣реА. тЬЕ
- v3 "days_present" рдЪреЗ рдирд╛рд╡ рдмрджрд▓реВрди "present" рдХрд░рддреЗ. рдХрддрд░рд┐рдирд╛рдЪрд╛ code "days_present" рд╢реЛрдзреЗрд▓ рдЖрдгрд┐ crash рд╣реЛрдИрд▓. рдирд╡рд╛ form рдмрд╛рд╣реЗрд░ рдЬрд╛рдгреНрдпрд╛рдЪреНрдпрд╛ рдЖрдзреАрдЪ рдХрд╛рд░реНрдб рддреЗ рдкрдХрдбрддреЗ. тЭМ
- v4 "200" рдЖрдХрдбреНрдпрд╛рдРрд╡рдЬреА рд╢рдмреНрджрд╛рдВрдд рд▓рд┐рд╣рд┐рддреЗ. рдкрдХрдбрд▓реЗ. тЭМ
рджреЛрдиреНрд╣реА offices рдПрдХрддреНрд░ рдЪрд╛рд▓рд╡рдгреНрдпрд╛рдЪреА рдХреЛрдгрд╛рд▓рд╛рдЪ рдЧрд░рдЬ рдкрдбрд▓реА рдирд╛рд╣реА. рдкреНрд░рддреНрдпреЗрдХ рдмрд╛рдЬреВрдиреЗ рд╕реНрд╡рддрдГрд▓рд╛ рддреНрдпрд╛рдЪ рдХрд╛рд░реНрдбрд╢реА рддрдкрд╛рд╕рд▓реЗ.
ЁЯЧ║я╕П рдЖрдХреГрддреА
flowchart LR
c["ЁЯУЗ consumer: report cards<br/>needs days_present:int, days_total:int"] -->|"writes the contract"| k["ЁЯдЭ contract<br/>GET /attendance/42 тЖТ 200"]
k -->|"replayed in the provider's CI"| p["ЁЯПв provider: attendance"]
p --> v1["v1 as today тЬЕ"]
p --> v2["v2 adds late_days тЬЕ"]
p --> v3["v3 renames to present тЭМ<br/>missing field days_present"]
p --> v4["v4 days_total as text тЭМ<br/>str, expected int"]
ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/testing/lesson-diagrams.html#l07
тЭУ рдХрд╛рдп
- Consumer / provider тАФ API рд▓рд╛ call рдХрд░рдгрд╛рд░реА рдмрд╛рдЬреВ рдЖрдгрд┐ рддреА API рдкреБрд░рд╡рдгрд╛рд░реА рдмрд╛рдЬреВ.
- Contract тАФ consumer рдХрд░рдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ requests рдЖрдгрд┐ responses рдЪреЗ рддреЗ рднрд╛рдЧ рдЬреНрдпрд╛рдВрд╡рд░ рддреЛ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕рддреЛ: status, fields, types. рд╕рдВрдкреВрд░реНрдг API рдирд╛рд╣реА тАФ рдлрдХреНрдд рд╣рд╛ consumer рдЬреЗ рд╡рд╛рдкрд░рддреЛ рддреЗрд╡рдвреЗрдЪ.
- Consumer-driven contract testing тАФ consumers рдЖрдкрд▓реЗ contracts рдкреНрд░рдХрд╛рд╢рд┐рдд рдХрд░рддрд╛рдд; provider рдЪрд╛ build рдкреНрд░рддреНрдпреЗрдХ contract рдЦрд▒реНрдпрд╛ provider code рд╡рд░ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддреЛ рдЖрдгрд┐ рдХреЛрдгрддрд╛рд╣реА consumer рдореЛрдбрдгрд╛рд░ рдЕрд╕реЗрд▓ рддрд░ fail рд╣реЛрддреЛ. Pact рд╣реЗ рд╕рд░реНрд╡рд╛рдд рдкреНрд░рд╕рд┐рджреНрдз рд╕рд╛рдзрди рдЖрд╣реЗ; рддреНрдпрд╛рдЪреНрдпрд╛ contract files рдирд╛ pacts рдореНрд╣рдгрддрд╛рдд.
- Provider verification тАФ рддрдкрд╛рд╕рдгреАрдЪреА provider рдмрд╛рдЬреВ: provider рд╕реБрд░реВ рдХрд░рд╛ (рдкреНрд░рддреНрдпреЗрдХ case рд╕рд╛рдареА test data рддрдпрд╛рд░ рдХрд░реВрди тАФ Pact рдпрд╛рдВрдирд╛ provider states рдореНрд╣рдгрддреЗ), рдиреЛрдВрджрд╡рд▓реЗрд▓реА рдкреНрд░рддреНрдпреЗрдХ request рдкрд╛рдард╡рд╛, response рдЪреА рддреБрд▓рдирд╛ рдХрд░рд╛.
- Tolerant reader тАФ consumer рдиреЗ рди рдорд╛рдЧрд┐рддрд▓реЗрд▓реНрдпрд╛ fields рдХрдбреЗ рддреЛ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░рддреЛ. рдореНрд╣рдгреВрдирдЪ field рдЬреЛрдбрдгреЗ (v2) рд╕реБрд░рдХреНрд╖рд┐рдд рдЖрд╣реЗ рдЖрдгрд┐ рдирд╛рд╡ рдмрджрд▓рдгреЗ (v3) рдирд╛рд╣реА.
- Matching by type тАФ consumer рд▓рд╛ рдЦрд░реЛрдЦрд░ рдХрд╛рдп рд▓рд╛рдЧрддреЗ рддреЗрдЪ contract рдиреЗ рдкрдХреНрдХреЗ рдХрд░рд╛рд╡реЗ: рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛ "рдПрдХ integer", "рдиреЗрдордХреЗ 172" рдирд╛рд╣реА. рдЬрд╛рд╕реНрдд рдХрдбрдХ contract рдирд┐рд░реБрдкрджреНрд░рд╡реА рдмрджрд▓рд╣реА рдЕрдбрд╡рддреЛ.
- Schema-first рдкрд░реНрдпрд╛рдп тАФ рджреЛрдиреНрд╣реА рдмрд╛рдЬреВ рдПрдХрд╛рдЪ shared schema рд╡рд░ test рдХрд░рддрд╛рдд (OpenAPI, JSON Schema, protobuf). Schemathesis рд╕рд╛рд░рдЦреА рд╕рд╛рдзрдиреЗ OpenAPI file рд╡рд░реВрди requests рддрдпрд╛рд░ рдХрд░рддрд╛рдд.
ЁЯдФ рдХрд╛
рдХрд╛рд░рдг рдзрдбрд╛ 05 рдЪреЗ stubs рд╣реЗ рджреБрд╕рд▒реНрдпрд╛ team рдЪреНрдпрд╛ service рдмрджреНрджрд▓рдЪреЗ рджрд╛рд╡реЗ рдЖрд╣реЗрдд, рдЖрдгрд┐ рджреЛрдиреНрд╣реА services рдПрдХрддреНрд░ рдЪрд╛рд▓рд╡рдгрд╛рд▒реНрдпрд╛ end-to-end tests рд╣рд│реВ рдЖрдгрд┐ flaky рдЕрд╕рддрд╛рдд (рдзрдбрд╛ 08). Contract рддреНрдпрд╛ рджрд╛рд╡реНрдпрд╛рдЪреЗ рд░реВрдкрд╛рдВрддрд░ рджреЛрдиреНрд╣реА teams рдЬреНрдпрд╛рд╡рд░ test рдХрд░рддрд╛рдд рдЕрд╢рд╛ file рдордзреНрдпреЗ рдХрд░рддреЗ: consumer рдЪреНрдпрд╛ unit tests рддреА stub рдореНрд╣рдгреВрди рд╡рд╛рдкрд░рддрд╛рдд, рдЖрдгрд┐ provider рдЪрд╛ build рд╕рд┐рджреНрдз рдХрд░рддреЛ рдХреА рддреЛ рдЕрдЬреВрдирд╣реА рддрд╕реЗрдЪ рдЙрддреНрддрд░ рджреЗрддреЛ. рдореЛрдбрдгрд╛рд░рд╛ рдмрджрд▓ deployment рдЖрдзреА, provider рдЪреНрдпрд╛ pipeline рдордзреНрдпреЗ, рддреЛ рдмрджрд▓ рдХрд░рдгрд╛рд▒реНрдпрд╛ team рдХрдбреВрдирдЪ рдкрдХрдбрд▓рд╛ рдЬрд╛рддреЛ.
ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)
exam/lab.py рдордзрд▓реЗ CONTRACT рдПрдХ dict рдЖрд╣реЗ: consumer рдЖрдгрд┐ provider рдЪреА рдирд╛рд╡реЗ,
request (GET /attendance/42) рдЖрдгрд┐ рдЕрдкреЗрдХреНрд╖рд┐рдд response (status 200, int type рдЪреЗ
days_present рдЖрдгрд┐ days_total рдЕрд╕рд▓реЗрд▓реА body). verify_contract(contract, provider) request path
рджреЗрдКрди provider рд▓рд╛ рдмреЛрд▓рд╡рддреЗ рдЖрдгрд┐ рдЕрдбрдЪрдгреАрдВрдЪреА рдпрд╛рджреА рдкрд░рдд рджреЗрддреЗ: рдЪреБрдХреАрдЪрд╛ status, рд╣рд░рд╡рд▓реЗрд▓реЗ
field, рдХрд┐рдВрд╡рд╛ рдЪреБрдХреАрдЪреНрдпрд╛ type рдЪреЗ field. рд░рд┐рдХрд╛рдореА рдпрд╛рджреА рдореНрд╣рдгрдЬреЗ provider contract
рдкрд╛рд│рддреЛ. рдЬрд╛рд╕реНрддреАрдЪреНрдпрд╛ fields рдХрдбреЗ рдореБрджреНрджрд╛рдо рджреБрд░реНрд▓рдХреНрд╖ рдХреЗрд▓реЗ рдЬрд╛рддреЗ.
ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛
python3 exam/demo.py contract
python3 - <<'EOF'
from exam.lab import CONTRACT, verify_contract
from exam.grades import AttendanceClient
v5 = lambda p: (200, {"days_present": 172})
v6 = lambda p: (404, {"error": "moved"})
v7 = lambda p: (200, {"days_present": 172.0, "days_total": 200})
for name, prov in (("v5 drops days_total", v5), ("v6 moved the path", v6), ("v7 sends a float", v7)):
print(f"{name:<20} тЖТ {verify_contract(CONTRACT, prov)}")
try: AttendanceClient(v5).attendance_percent("42")
except KeyError as e: print("without the contract, production sees: KeyError", e)
print("contract request:", CONTRACT["request"])
EOF
тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ
contract рдЫрд╛рдкрддреЗ:
provider v1 as today тЖТ тЬЕ honours the contract
provider v2 adds 'late_days' тЖТ тЬЕ honours the contract
provider v3 renames to 'present' тЖТ тЭМ missing field 'days_present'
provider v4 sends days_total as text тЖТ тЭМ 'days_total' is str, expected int
рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:
v5 drops days_total тЖТ ["missing field 'days_total'"]
v6 moved the path тЖТ ['status 404, expected 200', "missing field 'days_present'", "missing field 'days_total'"]
v7 sends a float тЖТ ["'days_present' is float, expected int"]
without the contract, production sees: KeyError 'days_total'
contract request: {'method': 'GET', 'path': '/attendance/42'}
ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ
рдкреНрд░рддреНрдпреЗрдХ рдореЛрдбрдгрд╛рд▒реНрдпрд╛ рдмрджрд▓рд╛рдЪреЗ рдиреЗрдордХреЗ рдирд╛рд╡ рд╕рд╛рдВрдЧрд┐рддрд▓реЗ рдЧреЗрд▓реЗ тАФ рдХреЛрдгрддреЗ field, рдХреЛрдгрддрд╛ type тАФ consumer рдЕрдЬрд┐рдмрд╛рдд
рди рдЪрд╛рд▓рд╡рддрд╛. Contract рдирд╕рддрд╛ рддрд░ v5 production рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрд▓рд╛ рдЕрд╕рддрд╛ рдЖрдгрд┐ report-card code рдордзреНрдпреЗ
KeyError рдореНрд╣рдгреВрди fail рдЭрд╛рд▓рд╛ рдЕрд╕рддрд╛, рддреЛ рдХрд╛рд░рдгреАрднреВрдд рдмрджрд▓рд╛рдкрд╛рд╕реВрди рдЦреВрдк рджреВрд░. рдЖрдгрд┐ v7 рд╣рд╛ рдХрдбрдХрдкрдгрд╛рдЪрд╛
рдзрдбрд╛ рдЖрд╣реЗ: AttendanceClient 172.0 рдЕрдЧрджреА рд╡реНрдпрд╡рд╕реНрдерд┐рдд рд╣рд╛рддрд╛рд│реВ рд╢рдХрд▓рд╛ рдЕрд╕рддрд╛. рдЬрд░ рдПрдЦрд╛рджрд╛
contract consumer рд▓рд╛ рдкрд░реНрд╡рд╛ рдирд╕рд▓реЗрд▓реЗ рдмрджрд▓ рдирд╛рдХрд╛рд░рдд рдЕрд╕реЗрд▓, рддрд░ teams рддреНрдпрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░реВ рд▓рд╛рдЧрддрд╛рдд; рддреБрдореНрд╣рд╛рд▓рд╛
рд▓рд╛рдЧрддреЗ рддреЗрд╡рдвреЗрдЪ рдкрдХреНрдХреЗ рдХрд░рд╛, рдЬрд╛рд╕реНрдд рдирд╛рд╣реА.
тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛
- consumer рдЬреЗ рд╡рд╛рдкрд░рддреЛ рддреНрдпрд╛рдРрд╡рдЬреА provider рдЪреЗ рд╕рдВрдкреВрд░реНрдг response copy рдХрд░рдгрд╛рд░рд╛ contract
- type рдкреБрд░реЗрд╕рд╛ рдЕрд╕рддрд╛рдирд╛ рдиреЗрдордХреНрдпрд╛ рдХрд┐рдорддреА ("рдПрдХ integer" рдРрд╡рдЬреА "172")
- provider team рдиреЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реЗ contracts тАФ рдордЧ рддреЗ рдЧрд░рдЬрд╛ рдирд╡реНрд╣реЗ, provider рдЪреЗ рд╡рд░реНрдгрди рдХрд░рддрд╛рдд
- contracts рдлрдХреНрдд consumer рдЪреНрдпрд╛ build рдордзреНрдпреЗ рддрдкрд╛рд╕рдгреЗ, рдЬрд┐рдереЗ provider рдЪрд╛ рдмрджрд▓ рдХрдзреАрдЪ рдЪрд╛рд▓рдд рдирд╛рд╣реА
- contract tests рдирд╛ provider рдЪреНрдпрд╛ business logic рдЪреНрдпрд╛ functional tests рд╕рд╛рд░рдЦреЗ рд╡рд╛рдЧрд╡рдгреЗ
ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд
рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ consumer рдЪреНрдпрд╛ tests рд▓рд┐рд╣рд┐рддрд╛рдд рддрд╢реА рдПрдХ pact file (Pact specification v3). Matching rules "рдиреЗрдордХреЗ 172" рдРрд╡рдЬреА "рдХреЛрдгрддрд╛рд╣реА integer" рд╕рд╛рдВрдЧрддрд╛рдд:
{
"consumer": {"name": "report-cards"},
"provider": {"name": "attendance"},
"interactions": [{
"description": "attendance for pupil 42",
"providerStates": [{"name": "pupil 42 exists"}],
"request": {"method": "GET", "path": "/attendance/42"},
"response": {
"status": 200,
"body": {"days_present": 172, "days_total": 200},
"matchingRules": {"body": {
"$.days_present": {"matchers": [{"match": "integer"}]},
"$.days_total": {"matchers": [{"match": "integer"}]}
}}
}
}],
"metadata": {"pactSpecification": {"version": "3.0.0"}}
}
Pact рдХрдбреЗ Python, JavaScript, Java, Go, .NET рдЖрдгрд┐ рдЗрддрд░ рднрд╛рд╖рд╛рдВрд╕рд╛рдареА libraries рдЖрд╣реЗрдд. Pacts рд╕рд╣рд╕рд╛ Pact Broker (рдХрд┐рдВрд╡рд╛ PactFlow) рдордзреВрди share рдХреЗрд▓реЗ рдЬрд╛рддрд╛рдд, рдЬреЛ "рд╣реЗ version deploy рдХрд░рдгреЗ рд╕реБрд░рдХреНрд╖рд┐рдд рдЖрд╣реЗ рдХрд╛?" рдпрд╛рдЪреЗрд╣реА рдЙрддреНрддрд░ рджреЗрддреЛ:
pact-broker can-i-deploy --pacticipant attendance --version 2.3.0 --to-environment production
рдЗрддрд░ рдкрджреНрдзрддреА: Spring Cloud Contract (JVM), рдЖрдгрд┐ OpenAPI рд╡рд░ рдЖрдзрд╛рд░рд┐рдд checks рдЬрд╕реЗ Schemathesis рдХрд┐рдВрд╡рд╛ Dredd, рдЬрд┐рдереЗ shared schema contract рдЪреА рднреВрдорд┐рдХрд╛ рдмрдЬрд╛рд╡рддреЗ.
ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рддреБрдордЪреА service рдЬреНрдпрд╛ рдЬреНрдпрд╛ API рдирд╛ call рдХрд░рддреЗ рддреНрдпрд╛рдВрдЪреА рдпрд╛рджреА рдХрд░рд╛. рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рд╡рд┐рдЪрд╛рд░рд╛ "рддреНрдпрд╛рдВрдиреА рдЙрджреНрдпрд╛ рдПрдЦрд╛рджреНрдпрд╛ field рдЪреЗ рдирд╛рд╡ рдмрджрд▓рд▓реЗ, рддрд░ рдХреЛрдгрддрд╛ build рд▓рд╛рд▓ рд╣реЛрдИрд▓?" рдЙрддреНрддрд░ "рдЖрдордЪрд╛, production рдордзреНрдпреЗ" рдЕрд╕реЗрд▓, рддрд░ рддреБрдореНрд╣рд╛рд▓рд╛ contract рд╣рд╡рд╛.
тПня╕П рдкреБрдвреЗ
Unit, integration, contract тАФ рдЖрдгрд┐ рд╕рд░реНрд╡рд╛рдд рд╡рд░, рд╕рдЧрд│реНрдпрд╛рдд рд╣рд│реВ рдЖрдгрд┐ рд╕рдЧрд│реНрдпрд╛рдд рдкреВрд░реНрдг test: end-to-end. Suite рдордзреНрдпреЗ рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рдХрд╛рд░рдЪреНрдпрд╛ рдХрд┐рддреА рдЕрд╕рд╛рд╡реНрдпрд╛рдд? Pyramid рдЖрдгрд┐ trophy.
git checkout lesson-08-e2e-pyramid