Структура PythonGo-проектаГлава 2.6
02 / 06 Один проект · следующий шаг

Проект, который запустится у другого

Фиксируем зависимости, объясняем запуск и отделяем исходники от локального окружения.

Проверяем зависимости, пишем тесты и README, отделяем исходники от результата сборки.

Результат главыrequirements.txt + .gitignore + README.mdgo.mod + go.sum + .gitignore + README.md
Ваш терминал
Для работы вместе с преподавателем

Передаём рецепт окружения

Выполните в своём проекте
Где
Активная .venv; корень student-tools.
Действие
Выполните python -m pip freeze > requirements.txt. Откройте созданный файл.
Результат
В файле записаны строки с именами пакетов и версиями.
Проверка
Найдите rich; убедитесь, что файл не пуст. Если там посторонние библиотеки, проверьте выбранное окружение.

Ваша .venv зависит от компьютера и содержит локальные пути. Её не отправляют в Git или архив проекта. Другому разработчику нужны исходники, установленный Python и список зависимостей, по которому он создаст собственное окружение.

ТЕРМИНАЛ · student-tools/
python -m pip freeze > requirements.txt

Выполняйте команду в активном окружении этого проекта. freeze записывает установленные пакеты с версиями, в том числе транзитивные зависимости Rich. Символ > сохраняет вывод в файл и перезаписывает прежнее содержимое.

ПРИМЕР · student-tools/requirements.txtСкачать файл ↓
rich==13.9.4
markdown-it-py==3.0.0
mdurl==0.1.2
Pygments==2.18.0
Вывод pip freeze и содержимое созданного requirements.txt с четырьмя пакетами и версиями.
Свой список зависимостей · macOS / Python 3.12

Сначала freeze печатает список в терминал, затем та же команда со знаком > записывает его в файл. В списке четыре строки: сам Rich и три библиотеки, которые он тянет за собой. Версии в этом прогоне отличаются от учебного примера ниже — так и должно быть.

Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.

Это зафиксированный набор для скачиваемого примера, а не обещание самых новых версий. Ваш freeze после pip install rich может дать другие версии. В своей работе проверяйте именно свой файл зависимостей.

Что попадёт в Git

Выполните в своём проекте
Где
Explorer: корень student-tools.
Действие
Создайте файл .gitignore без расширения .txt. Вставьте правила ниже и сохраните.
Результат
Git игнорирует локальное окружение и кэш, если они ещё не отслеживались.
Проверка
Перед коммитом просмотрите git status: .venv и __pycache__ не должны входить в новые файлы.
student-tools/.gitignoreСкачать файл ↓
.venv/
__pycache__/
*.pyc
.env

__pycache__ и *.pyc — автоматически создаваемый кэш Python. .env может содержать локальные настройки и секреты. Исходники, тесты, README, requirements и сам .gitignore должны оставаться в репозитории.

Проверить правила можно командой git status: она перечисляет файлы, которые Git сейчас видит. Папки в коротком выводе показаны целиком, поэтому кеш внутри app удобно посмотреть отдельной командой с ключом --untracked-files=all.

ТЕРМИНАЛ · student-tools/
git status --short
git status --short --untracked-files=all -- app
Терминал: без .gitignore git status показывает .env и .venv, а внутри app — файл из __pycache__; после добавления .gitignore они пропадают из вывода.
Один и тот же проект до и после правил · macOS / Python 3.12

Сверху правил ещё нет: 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.

ТЕРМИНАЛ · student-tools/
python -m pip install pytest
python -m pip freeze > requirements.txt

Вторая команда обновляет список зависимостей: pytest тоже нужен тому, кто будет проверять вашу работу.

Файл с тестами

Каждый тест — обычная функция. Ни классов, ни наследования здесь не требуется: pytest сам находит функции, имя которых начинается с test_, и вызывает их по очереди.

student-tools/tests/test_calculator.pyСкачать файл ↓
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, среднее равно 4.4.
Почему для дробных нужен approx · macOS / Python 3.12

Сумма 0.1 и 0.2 печатается как 0.30000000000000004, поэтому точное сравнение с 0.3 даёт False. Наше среднее в этом примере получается ровным, но у другого списка результат может отличаться в последнем знаке. Для дробных чисел берут pytest.approx, а обычное == оставляют строкам и целым числам.

Скачать текст вывода ↓ · Нажмите на изображение, чтобы увеличить.

Команда запуска по частям

ТЕРМИНАЛ · student-tools/
python -m pytest -v

-m pytest — запускаем pytest тем же Python, что и программу. -v — печатать каждый тест отдельной строкой. Команда выполняется из корня проекта: оттуда виден пакет app, который тесты импортируют. Папку с тестами указывать не нужно — pytest сам обходит проект и берёт файлы test_*.py.

Создайте папку tests в корне и сохраните туда файл выше. Ожидаются три пройденных теста: средняя обычного списка, формат строки и ошибка для пустого списка.

Терминал: pytest собрал три теста, все помечены PASSED, итог 3 passed.
Результат реального прогона · macOS / Python 3.12

Строка collected 3 items говорит, сколько тестов найдено, дальше каждый помечен PASSED, в конце — 3 passed. Смотрите не только на слово passed, но и на число: тестов должно быть три.

Нажмите на изображение, чтобы увеличить.

Как выглядит непройденный тест

Пройденные тесты показывают только, что всё в порядке. Разберите три других вывода — они встретятся в практике 2.7 и в домашней работе.

Тест не прошёл: расчёт вернул не то

Терминал: тест test_average помечен FAILED, показана строка assert и значения 3.4 и 4.4, итог 1 failed, 2 passed.
Проверка не сошлась · macOS / Python 3.12

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

Нажмите на изображение, чтобы увеличить.

Ошибка до проверки

Терминал: в тесте вызвана несуществующая функция calculate_avg, pytest показывает NameError.
Код не дошёл до сравнения · macOS / Python 3.12

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

Нажмите на изображение, чтобы увеличить.

Ноль тестов: проверок не было

Терминал: pytest сообщает collected 0 items и no tests ran.
Файл назван не по правилу · macOS / Python 3.12

Файл переименован в calculator.py, и pytest его не взял: имя файла должно начинаться с test_, и имя каждой тестовой функции — тоже. В выводе стоит collected 0 items и no tests ran. Проверяйте это число: ноль выполненных тестов не подтверждает, что работа сделана.

Нажмите на изображение, чтобы увеличить.

Проверка с чистого листа

Скопируйте только передаваемые файлы в отдельную папку или скачайте свой репозиторий заново. Откройте папку, содержащую app и requirements.txt. Создайте там новое окружение и активируйте его командами главы 2.1.

ТЕРМИНАЛ · student-tools/
python -m pip install -r requirements.txt
python -m app.main
python -m pytest -v

Если видны Средний результат: 4.40 и 3 passed, вы проверили, что проект не зависит от старой локальной .venv. В README укажите также вывод python --version.

Терминал: список файлов без .venv, установка по requirements.txt, запуск программы и три пройденных теста.
Полный прогон с чистого листа · macOS / Python 3.12

В папке только исходники: 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 заполнялись сами, пока вы работали. Передать проект другому человеку — значит передать исходники вместе с этими двумя файлами.

ТЕРМИНАЛ · student-tools/
go mod tidy
go mod verify
go list -m all

go mod tidy приводит файлы в соответствие с импортами в коде. go mod verify пересчитывает контрольные суммы всех скачанных зависимостей и сравнивает их с go.sum: ответ all modules verified означает, что содержимое версий не менялось с момента загрузки. Обе команды выполняют перед отправкой работы.

Насколько это надёжнее requirements.txt

pip freeze делает снимок: он перечисляет версии, но не проверяет содержимое пакетов и не фиксирует версию Python. В Go go.mod задаёт минимальную версию языка, go.sum хранит контрольные суммы, а сборка останавливается при любом расхождении. В Python такую роль выполняет lock-файл (poetry.lock, uv.lock), а не requirements.txt.

ПРИМЕР · student-tools/go.mod
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/.gitignore
# Результат сборки
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 не делают. Зато тесту становятся доступны и неэкспортированные имена пакета — то, до чего внешний код не добрался бы.

student-tools/internal/services/calculator_test.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 читается как «отличается больше, чем на миллиардную». Точное сравнение == оставляют строкам и целым числам.

Команда запуска по частям

ТЕРМИНАЛ · student-tools/
go test ./...
go test ./... -v
go test ./internal/services -run TestCalculateAverageEmpty -v

go 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, чтобы зависимости скачались заново.

ТЕРМИНАЛ · student-tools/
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