Проект, который запустится у другого
Фиксируем зависимости, объясняем запуск и отделяем исходники от локального окружения.
Проверяем зависимости, пишем тесты и README, отделяем исходники от результата сборки.
Передаём рецепт окружения
- Где
- Активная .venv; корень student-tools.
- Действие
- Выполните python -m pip freeze > requirements.txt. Откройте созданный файл.
- Результат
- В файле записаны строки с именами пакетов и версиями.
- Проверка
- Найдите rich; убедитесь, что файл не пуст. Если там посторонние библиотеки, проверьте выбранное окружение.
Ваша .venv зависит от компьютера и содержит локальные пути. Её не отправляют в Git или архив проекта. Другому разработчику нужны исходники, установленный Python и список зависимостей, по которому он создаст собственное окружение.
python -m pip freeze > requirements.txtВыполняйте команду в активном окружении этого проекта. freeze записывает установленные пакеты с версиями, в том числе транзитивные зависимости Rich. Символ > сохраняет вывод в файл и перезаписывает прежнее содержимое.
rich==13.9.4
markdown-it-py==3.0.0
mdurl==0.1.2
Pygments==2.18.0
Сначала freeze печатает список в терминал, затем та же команда со знаком > записывает его в файл. В списке четыре строки: сам Rich и три библиотеки, которые он тянет за собой. Версии в этом прогоне отличаются от учебного примера ниже — так и должно быть.
Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.Это зафиксированный набор для скачиваемого примера, а не обещание самых новых версий. Ваш freeze после pip install rich может дать другие версии. В своей работе проверяйте именно свой файл зависимостей.
Что попадёт в Git
- Где
- Explorer: корень student-tools.
- Действие
- Создайте файл .gitignore без расширения .txt. Вставьте правила ниже и сохраните.
- Результат
- Git игнорирует локальное окружение и кэш, если они ещё не отслеживались.
- Проверка
- Перед коммитом просмотрите git status: .venv и __pycache__ не должны входить в новые файлы.
.venv/
__pycache__/
*.pyc
.env__pycache__ и *.pyc — автоматически создаваемый кэш Python. .env может содержать локальные настройки и секреты. Исходники, тесты, README, requirements и сам .gitignore должны оставаться в репозитории.
Проверить правила можно командой git status: она перечисляет файлы, которые Git сейчас видит. Папки в коротком выводе показаны целиком, поэтому кеш внутри app удобно посмотреть отдельной командой с ключом --untracked-files=all.
git status --short
git status --short --untracked-files=all -- app
Сверху правил ещё нет: Git предлагает добавить .env, .venv/ и кеш app/__pycache__/main.cpython-312.pyc. Снизу тот же проект с .gitignore: в списке остались только исходники, тесты, README, requirements и сам файл правил. Знак ?? в начале строки означает «файл ещё не отслеживается Git».
app · tests · инструкции≠На компьютере
.venv · кэш · .env
README — инструкция следующему человеку
- Где
- Explorer: корень student-tools и новая папка tests.
- Действие
- Установите pytest, создайте README.md и tests/test_calculator.py. Сохраните код тестов ниже.
- Результат
- В проекте есть инструкция и три проверки результата.
- Проверка
- Выполните python -m pytest -v. Нужны три теста и строка 3 passed.
Читатель не присутствовал на занятии. Напишите, что делает программа, какая версия Python нужна, откуда выполнять команды, как создать окружение, установить зависимости, запустить приложение и что должно появиться. Названия папок объясните через обязанности.
↓Скачать пример READMEMarkdown · команды для Windows и macOS/LinuxЧто такое тест и зачем он в этой теме
Тест — это обычный код, который вызывает вашу функцию с известными данными и проверяет, что она вернула ожидаемое. Проверяет не человек глазами, а сама программа: вы один раз описываете «на списке [5, 4, 5, 3, 5] среднее равно 4.4» — и дальше эта проверка повторяется по команде за доли секунды.
В этой теме тесты выполняют ещё одну работу: они проверяют структуру проекта. Тестовый файл лежит в отдельной папке tests и добирается до функций теми же импортами, что и main.py. Если структура собрана правильно, тесты запускаются сразу. Если раскладка файлов не совпадает с импортами, появятся те же ошибки, которые разобраны в главах 2.4 и 2.5.
Тесты ваши модули импортируют, а не запускают. Поэтому main() и должен вызываться только под условием __name__ == "__main__" — иначе при каждом прогоне тестов программа будет печатать свой отчёт.
Ставим pytest
Тесты запускает отдельная программа — pytest. Она устанавливается в окружение проекта так же, как Rich в главе 2.3.
python -m pip install pytest
python -m pip freeze > requirements.txtВторая команда обновляет список зависимостей: pytest тоже нужен тому, кто будет проверять вашу работу.
Файл с тестами
Каждый тест — обычная функция. Ни классов, ни наследования здесь не требуется: pytest сам находит функции, имя которых начинается с test_, и вызывает их по очереди.
import pytest
from app.services.calculator import calculate_average
from app.utils.formatter import format_average
def test_average():
assert calculate_average([5, 4, 5, 3, 5]) == pytest.approx(4.4)
def test_format():
assert format_average(4.4) == "Средний результат: 4.40"
def test_empty_list():
with pytest.raises(ValueError):
calculate_average([])Как работает assert
assert — это встроенная в Python проверка условия. Читается она так: «дальше идём, только если условие истинно».
- Условие истинно —
assertничего не делает, функция доходит до конца, тест считается пройденным. - Условие ложно — Python останавливает функцию и сообщает об ошибке
AssertionError. pytest перехватывает её и печатает, какие значения сравнивались.
Поэтому строка assert format_average(4.4) == "Средний результат: 4.40" означает: вызови функцию и сравни результат со строкой справа. Никаких специальных слов для сравнения не нужно — работают обычные операторы ==, !=, >, in.
Две конструкции в файле пришли из pytest:
pytest.approx(4.4)— «примерно 4.4». Нужно для дробных чисел: они хранятся приближённо, и точное равенство иногда не выполняется.with pytest.raises(ValueError):— «внутри этого блока обязана возникнуть такая ошибка». Здесь проверяется не результат, а поведение при неверных данных: на пустом спискеcalculate_averageдолжна вызыватьValueError. Если функция промолчит, тест не пройдёт.

Сумма 0.1 и 0.2 печатается как 0.30000000000000004, поэтому точное сравнение с 0.3 даёт False. Наше среднее в этом примере получается ровным, но у другого списка результат может отличаться в последнем знаке. Для дробных чисел берут pytest.approx, а обычное == оставляют строкам и целым числам.
Команда запуска по частям
python -m pytest -v-m pytest — запускаем pytest тем же Python, что и программу. -v — печатать каждый тест отдельной строкой. Команда выполняется из корня проекта: оттуда виден пакет app, который тесты импортируют. Папку с тестами указывать не нужно — pytest сам обходит проект и берёт файлы test_*.py.
Создайте папку tests в корне и сохраните туда файл выше. Ожидаются три пройденных теста: средняя обычного списка, формат строки и ошибка для пустого списка.

Строка collected 3 items говорит, сколько тестов найдено, дальше каждый помечен PASSED, в конце — 3 passed. Смотрите не только на слово passed, но и на число: тестов должно быть три.
Как выглядит непройденный тест
Пройденные тесты показывают только, что всё в порядке. Разберите три других вывода — они встретятся в практике 2.7 и в домашней работе.
Тест не прошёл: расчёт вернул не то

В формулу намеренно внесена ошибка: деление на len(values) - 1. Тест test_average помечен FAILED, ниже pytest показывает саму строку с assert и значения по обе стороны сравнения: получено 3.4000000000000004, ожидалось 4.4. Итог — 1 failed, 2 passed. Читать такой вывод удобно снизу вверх: сначала строка итога, затем значения, затем строка теста.
Ошибка до проверки

Здесь ошибка в самом тесте: вызвана функция calculate_avg, которой нет. До сравнения дело не дошло — Python сообщил NameError. Разница важна: расхождение значений означает ошибку в расчёте, а NameError, TypeError или ImportError — что сломался сам код, и чинить нужно другое место.
Ноль тестов: проверок не было

Файл переименован в calculator.py, и pytest его не взял: имя файла должно начинаться с test_, и имя каждой тестовой функции — тоже. В выводе стоит collected 0 items и no tests ran. Проверяйте это число: ноль выполненных тестов не подтверждает, что работа сделана.
Проверка с чистого листа
Скопируйте только передаваемые файлы в отдельную папку или скачайте свой репозиторий заново. Откройте папку, содержащую app и requirements.txt. Создайте там новое окружение и активируйте его командами главы 2.1.
python -m pip install -r requirements.txt
python -m app.main
python -m pytest -vЕсли видны Средний результат: 4.40 и 3 passed, вы проверили, что проект не зависит от старой локальной .venv. В README укажите также вывод python --version.

В папке только исходники: app, tests, requirements.txt и README — окружения нет. По списку зависимостей pip ставит те же версии, включая pytest, после чего программа печатает 4.40, а три теста проходят. Так проверяется, что проект запустится у другого человека.
Нажмите на изображение, чтобы увеличить.Рецепт уже лежит в проекте
- Где
- Корень student-tools.
- Действие
- Выполните go mod tidy, затем go mod verify. Откройте go.mod.
- Результат
- tidy молчит или правит файл, verify печатает all modules verified.
- Проверка
- В go.mod есть строка с github.com/fatih/color и номером версии; рядом лежит go.sum.
В Python список зависимостей создавала отдельная команда pip freeze. В Go отдельного шага нет: go.mod и go.sum заполнялись сами, пока вы работали. Передать проект другому человеку — значит передать исходники вместе с этими двумя файлами.
go mod tidy
go mod verify
go list -m allgo mod tidy приводит файлы в соответствие с импортами в коде. go mod verify пересчитывает контрольные суммы всех скачанных зависимостей и сравнивает их с go.sum: ответ all modules verified означает, что содержимое версий не менялось с момента загрузки. Обе команды выполняют перед отправкой работы.
pip freeze делает снимок: он перечисляет версии, но не проверяет содержимое пакетов и не фиксирует версию Python. В Go go.mod задаёт минимальную версию языка, go.sum хранит контрольные суммы, а сборка останавливается при любом расхождении. В Python такую роль выполняет lock-файл (poetry.lock, uv.lock), а не requirements.txt.
module example.com/student-tools
go 1.21
require github.com/fatih/color v1.17.0
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.18.0 // indirect
)Это зафиксированный набор для скачиваемого примера, а не обещание самых новых версий. У вас версии будут другими — так и должно быть. В своей работе проверяйте именно свой go.mod.
Что попадёт в Git
- Где
- Explorer: корень student-tools.
- Действие
- Создайте файл .gitignore без расширения .txt. Вставьте правила ниже и сохраните.
- Результат
- Git игнорирует собранный бинарник и локальные файлы.
- Проверка
- Выполните go build, затем git status: собранного файла в списке новых быть не должно.
# Результат сборки
student-tools
student-tools.exe
# Локальные настройки и покрытие
.env
coverage.outОтличие от Python: исключать зависимости не нужно. Их в проекте нет — код библиотек лежит в общем кэше за пределами папки. Исключаем то, что создаёт сборка: исполняемый файл (его имя совпадает с именем папки проекта) и служебные файлы вроде отчёта о покрытии тестами.
go.mod и go.sum в репозиторий попадают обязательно. Без них другой человек не сможет получить те же версии, и go build у него соберёт неизвестно что — или не соберёт вовсе.
main.go · internal · go.mod · go.sum≠Только локально
бинарник · .env · кэш модулей
README и тесты рядом с кодом
- Где
- Explorer: корень student-tools и папка internal/services.
- Действие
- Создайте README.md и internal/services/calculator_test.go. Сохраните код тестов ниже.
- Результат
- В проекте есть инструкция и три проверки результата.
- Проверка
- Выполните go test ./... -v. Нужны три строки PASS и итог ok.
Читатель не присутствовал на занятии. Напишите, что делает программа, какая версия Go нужна, откуда выполнять команды, как получить зависимости, как собрать и запустить проект и что должно появиться. Названия папок объясните через обязанности.
Что такое тест и зачем он в этой теме
Тест — это обычный код, который вызывает вашу функцию с известными данными и проверяет, что она вернула ожидаемое. Проверяет не человек глазами, а сама программа: вы один раз описываете «на срезе [5, 4, 5, 3, 5] среднее равно 4.4» — и дальше эта проверка повторяется по команде за доли секунды.
В Go тестирование встроено в язык и в команду go: ставить ничего не нужно, сторонние библиотеки для трёх проверок не требуются. Правила всего три, и все они про имена. Файл обязан заканчиваться на _test.go. Функция обязана начинаться с Test и принимать один аргумент *testing.T. И такой файл не попадает в обычную сборку: go build его игнорирует, поэтому тесты не увеличивают программу.
Ещё одно отличие от Python: тест лежит в той же папке, что и проверяемый код, и объявляет тот же пакет. Отдельной папки tests в Go не делают. Зато тесту становятся доступны и неэкспортированные имена пакета — то, до чего внешний код не добрался бы.
package services
import (
"math"
"testing"
)
func TestCalculateAverage(t *testing.T) {
got, err := CalculateAverage([]int{5, 4, 5, 3, 5})
if err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if math.Abs(got-4.4) > 1e-9 {
t.Errorf("среднее = %v, ожидалось 4.4", got)
}
}
func TestCalculateAverageEmpty(t *testing.T) {
_, err := CalculateAverage([]int{})
if err == nil {
t.Error("для пустого среза ожидалась ошибка, её нет")
}
}Как читается файл теста
package services— тот же пакет, что и у проверяемого файла. ПоэтомуCalculateAverageвызывается напрямую, без импорта: имена внутри пакета видны без квалификации.func TestCalculateAverage(t *testing.T)— один тест. Имя обязано начинаться сTest: по этому признаку Go и находит, что запускать. Функция без такого имени просто не выполнится.t.Errorf(...)— «запиши расхождение и продолжай»;t.Fatalf(...)— «запиши и прекрати этот тест немедленно». Второе нужно там, где дальше проверять нечего: если функция вернула ошибку, сравнивать её результат бессмысленно.- Готовых
assertEqualв Go нет: сравнение пишется обычнымif, и тест остаётся обычным кодом.
Почему сравнение через math.Abs
Компьютер хранит дробные числа приближённо, и привычные равенства иногда не выполняются: 0.1 + 0.2 даёт 0.30000000000000004. Так устроено двоичное представление дробных чисел, и это одинаково работает во всех языках. Поэтому дробные результаты сравнивают с допуском: math.Abs(got-4.4) > 1e-9 читается как «отличается больше, чем на миллиардную». Точное сравнение == оставляют строкам и целым числам.
Команда запуска по частям
go test ./...
go test ./... -v
go test ./internal/services -run TestCalculateAverageEmpty -vgo test собирает и запускает тесты; ./... — «все пакеты модуля»; -v печатает каждый тест отдельной строкой, а не сводку. Третья команда показывает, как запустить один тест: -run принимает шаблон имени. Команды работают из любой папки внутри модуля.
Ожидается две строки --- PASS и итог ok example.com/student-tools/internal/services. Пакеты без тестов Go отметит как no test files: так он сообщает, что проверять там нечего.
Как выглядит непройденный тест
Зелёный прогон полезен, но учит мало. Разберите три вида вывода — с ними вы столкнётесь в практике 2.7 и в домашней работе.
FAIL — код отработал, но посчитал не то
--- FAIL: TestCalculateAverage (0.00s)
calculator_test.go:15: среднее = 5.5, ожидалось 4.4
FAIL
FAIL example.com/student-tools/internal/services 0.002s
FAILЗдесь в формулу внесена ошибка — деление на len(values) - 1. Go называет тест, файл и строку, где сработала проверка, а текст расхождения вы написали сами в t.Errorf. Отсюда практический вывод: пишите в сообщении и полученное, и ожидаемое значение — иначе через месяц вывод FAIL вам ничего не скажет.
Ошибка сборки — до тестов дело не дошло
# example.com/student-tools/internal/services [build failed]
./calculator_test.go:10:17: not enough arguments in call to CalculateAverage
FAIL example.com/student-tools/internal/services [build failed]Аналог питоновского ERROR, только раньше: в Go такие ошибки находит компилятор, а не выполнение теста. Пометка [build failed] означает, что ни один тест пакета не запускался. Разница важна: FAIL — «проверка не сошлась», build failed — «код не собрался», и чинить нужно разные места.
Строки no test files: проверок не было
? example.com/student-tools [no test files]
? example.com/student-tools/internal/services [no test files]
? example.com/student-tools/internal/utils [no test files]Так выглядит прогон, в котором проверять оказалось нечего: файл назван calculatortest.go вместо calculator_test.go, или функция — CheckAverage вместо TestAverage. Ошибки нет, красного цвета нет, но и проверок не было. Смотрите не на отсутствие слова FAIL, а на наличие строк ok и PASS.
Проверка с чистого листа
Скопируйте только передаваемые файлы в отдельную папку или скачайте свой репозиторий заново. Откройте папку, содержащую main.go и go.mod. Кэш модулей можно даже очистить командой go clean -modcache, чтобы зависимости скачались заново.
go mod download
go build ./...
go run .
go test ./... -vЕсли видны Средний результат: 4.40 и строки PASS, вы проверили, что проект собирается не только у вас. В README укажите также вывод go version: он показывает, на какой версии работа проверялась.
Проверьте себя · Почему go.sum нужно отправлять в репозиторий?
В нём записаны контрольные суммы тех версий, на которых работа проверялась. Без go.sum Go скачает зависимости заново и запишет суммы с нуля — подмену содержимого версии в этом случае никто не заметит. Файл служебный, править его руками не нужно, но и удалять из репозитория нельзя.
Проверьте себя · Почему requirements.txt нужно получать в окружении проекта?
freeze перечисляет установленные пакеты выбранного Python. В общем окружении он захватит библиотеки других программ, а в неверно выбранном — может не содержать Rich.
Документация к теме
Go: добавляем тест · Пакет testing · Go: файлы go.sum · Go: управление зависимостями
Python: venv · Python: модули и пакеты · VS Code: Python environments · pip freeze