Зарегистрируйтесь, чтобы продолжить обучение

Работа с временными файлами и каталогами Go: Автоматическое тестирование

В реальном коде мы всё время крутимся вокруг файлов. Читаем конфиги, пишем логи, сохраняем результаты работы. В тестах это тоже нужно проверять. Но если тесты начнут писать настоящие файлы прямо в проект — начнётся полный бардак. Представь: один тест перезаписал файл, другой не смог его удалить, третий случайно затёр что-то важное. В итоге проект после прогона тестов похож на свалку.

Поэтому в тестах всегда работают с временными файлами и директориями. Они создаются в специальной системной директории tmp, имеют уникальные имена и исчезают после завершения теста. Это как одноразовая посуда: использовал, проверил — выкинул.

Директория tmp

У каждой операционной системы есть место под временные данные. На Linux и macOS это обычно /tmp, на Windows — C:\Users<имя>\AppData\Local\Temp. Туда можно писать что угодно, и система рано или поздно сама почистит.

Go знает, где этот каталог, через функцию os.TempDir(). Если её вызвать, она вернёт путь к текущему «времяному».

fmt.Println(os.TempDir())
// Linux: /tmp
// Windows: C:\Users\Artur\AppData\Local\Temp

Когда мы вызываем os.CreateTemp("", "pattern") или os.MkdirTemp("", "pattern") и передаём пустую строку первым аргументом, Go автоматически создаёт файл или директорию именно там.

Временный файл: создаём, пишем, читаем

Допустим, у нас есть функция, которая должна записывать данные в файл. Мы хотим протестировать её, но писать в проект нельзя. Тогда используем временный файл.

func TestTempFile(t *testing.T) {
    // Создаём временный файл в системной директории tmp.
    // Второй аргумент — шаблон имени. Звёздочка заменится на случайные символы,
    // так что два теста никогда не получат один и тот же файл.
    f, err := os.CreateTemp("", "example-*.txt")
    if err != nil {
        t.Fatal(err)
    }

    // Файл создаётся реально на диске. Чтобы он не остался после теста,
    // регистрируем удаление. Remove сработает в конце функции.
    defer os.Remove(f.Name())

    // Записываем строку в файл. Здесь всё как обычно.
    _, err = f.WriteString("hello, world")
    if err != nil {
        t.Fatal(err)
    }

    // Теперь читаем содержимое обратно через имя файла.
    data, err := os.ReadFile(f.Name())
    if err != nil {
        t.Fatal(err)
    }

    // Сравниваем результат.
    if string(data) != "hello, world" {
        t.Errorf("получили %q, хотели %q", string(data), "hello, world")
    }
}

Такой тест работает в изоляции: каждый запуск создаёт уникальный файл, тест проверяет его содержимое и удаляет. После тестов в проекте чисто, будто ничего и не было.

Временная директория: создаём и работаем внутри

Иногда одного файла мало. Например, если нужно проверить функцию, которая перебирает файлы в директории. Для этого в Go есть os.MkdirTemp.

func TestTempDir(t *testing.T) {
    // Создаём временный каталог. Имя будет вроде /tmp/exampledir-12345.
    dir, err := os.MkdirTemp("", "exampledir-*")
    if err != nil {
        t.Fatal(err)
    }
    // Удаляем директорию и всё содержимое после теста.
    defer os.RemoveAll(dir)

    // Внутри этой директории создаём файл.
    path := filepath.Join(dir, "data.txt")
    err = os.WriteFile(path, []byte("test data"), 0644)
    if err != nil {
        t.Fatal(err)
    }

    // Читаем обратно и проверяем.
    data, err := os.ReadFile(path)
    if err != nil {
        t.Fatal(err)
    }

    if string(data) != "test data" {
        t.Errorf("получили %q, хотели %q", string(data), "test data")
    }
}

Теперь тест создаёт целую «песочницу»: в ней можно создавать файлы, директории, проверять логику работы с файловой системой. В конце всё это удаляется одной строкой os.RemoveAll.

Современный способ: t.TempDir()

Писать вручную os.MkdirTemp и defer os.RemoveAll немного утомительно. Поэтому начиная с Go 1.15 у *testing.T есть метод TempDir(). Он делает всё то же самое, но за вас.

func TestWithTempDir(t *testing.T) {
    // Создаём временную директорию. Go сам удалит её после теста.
    dir := t.TempDir()

    // Кладём туда файл.
    path := filepath.Join(dir, "config.json")
    os.WriteFile(path, []byte(`{"ok": true}`), 0644)

    // Читаем обратно.
    data, err := os.ReadFile(path)
    if err != nil {
        t.Fatal(err)
    }

    if string(data) != `{"ok": true}` {
        t.Errorf("получили %q, хотели %q", string(data), `{"ok": true}`)
    }
}

С t.TempDir тесты получаются чище: не нужно вручную писать RemoveAll, не нужно думать про порядок defer. Go всё делает за вас.

Зачем всё это

Временные файлы и директории нужны везде, где код взаимодействует с файловой системой:

  • сохранить результат работы и проверить, что он записан правильно;
  • проверить алгоритм, который читает директорию с файлами;
  • временно создать лог или отчёт и убедиться, что он имеет правильный формат.

Тесты должны быть изолированными, и tmp — идеальный способ эту изоляцию обеспечить.

Итог

Вместо того чтобы засорять проект настоящими файлами, тесты работают с временными. Функция os.CreateTemp создаёт файл, os.MkdirTemp — директорию, t.TempDir делает то же самое, но сам убирает за собой. Все они создают уникальные имена внутри системного tmp, так что тесты не мешают друг другу.

Благодаря этому после прогонов тестов на диске не остаётся следов, проект чистый, а тесты надёжные.


Самостоятельная работа

Протестируем маленькую утилиту: она складывает строки лога во временный файл, чтобы потом его прикрепить к письму/репорту.

package tempfiledemo

import (
    "os"
    "strings"
)

// WriteLinesToTemp пишет строки в новый временный файл и возвращает путь к нему.
func WriteLinesToTemp(prefix string, lines []string) (string, error) {
    f, err := os.CreateTemp("", prefix)
    if err != nil {
        return "", err
    }
    defer f.Close()

    content := strings.Join(lines, "\n")
    if _, err := f.WriteString(content); err != nil {
        return "", err
    }
    return f.Name(), nil
}

Напишите тесты, которые убеждаются, что файл создан, данные на месте, и всё корректно удаляется.

Что тестируем

  • Что функция создаёт файл в системной temp‑директории и записывает все строки в правильном порядке (через \n).
  • Что файл закрывается и его можно безопасно удалить.

Что покрыть в тестах

  • Базовый кейс: []string{"hello"} → файл содержит hello.
  • Несколько строк: проверяем разделитель \n и порядок.
  • Пустой список: создаётся пустой файл.

Подсказка

  • После os.Remove(path) можно проверить os.Stat на os.ErrNotExist.
Показать решение
package tempfiledemo

import (
"errors"
"os"
"testing"
)

func TestWriteLinesToTemp(t *testing.T) {
t.Run("single line", func(t *testing.T) {
    path, err := WriteLinesToTemp("log-", []string{"hello"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    defer os.Remove(path)

    data, err := os.ReadFile(path)
    if err != nil {
        t.Fatalf("read: %v", err)
    }
    if string(data) != "hello" {
        t.Fatalf("got %q, want %q", data, "hello")
    }
})

t.Run("multi line and empty", func(t *testing.T) {
    cases := []struct {
        lines []string
        want  string
    }{
        {[]string{"a", "b", "c"}, "a\nb\nc"},
        {nil, ""},
    }
    for _, c := range cases {
        path, err := WriteLinesToTemp("log-", c.lines)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        data, err := os.ReadFile(path)
        if err != nil {
            t.Fatalf("read: %v", err)
        }
        if string(data) != c.want {
            t.Fatalf("got %q, want %q", data, c.want)
        }
        if err := os.Remove(path); err != nil {
            t.Fatalf("remove: %v", err)
        }
        if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
            t.Fatalf("expected not exists, got: %v", err)
        }
    }
})
}

Дополнительные материалы

  1. os.CreateTemp — документация
  2. testing.T.TempDir — временный каталог в тестах
  3. os.Remove — удаление файлов

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff