Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +50 XP

Тестирование fyne-приложений

Двадцать три урока строили интерфейс, и каждый раз проверка выглядела одинаково: собрать, запустить, подвигать мышкой. Так регрессии находят пользователи. Запрос «fyne тесты» — это пакет fyne.io/fyne/v2/test: test.NewApp поднимает приложение с тестовым драйвером без экрана, test.Tap и test.Type подсовывают виджетам клики и ввод настоящих событий, а AssertRendersToMarkup сверяет отрисовку с золотым файлом. Русский запрос «тестирование fyne» ведёт сюда же. Урок учится проверять три слоя: логику обработчиков, поведение виджетов под симулированным пользователем и структуру отрисовки — так что сломанный интерфейс останавливает сборку, а не доходит до монитора.

Содержание урока · 11 разделов

GUI тестируется без монитора#

Скепсис про «GUI не тестируется» держится на путанице двух вещей: событий экрана и событий приложения. В Fyne клик — это вызов метода Tapped у виджета, нажатие клавиши — вызов TypedRune, и настоящий монитор нужен только чтобы превратить движение мыши в этот вызов. Тестовый пакет делает последний шаг сам: события подсовываются виджетам напрямую, и вся логика — обработчики из шестого урока, валидаторы из восемнадцатого, привязки из семнадцатого — выполняется в обычном go test без единого окна. Запрос «fyne тесты» — это три слоя проверок. Первый — чистая логика: хранилища задач и подсчёты тестируются как обычный Go, им интерфейс не нужен вовсе. Второй — поведение: после test.Tap по кнопке состояние изменилось, после test.Type в поле валидатор выдал ошибку. Третий — отрисовка: снимок разметки ловит случай, когда кнопка осталась кликабельной, но исчезла с экрана из-за ошибки раскладки. Правило проекта: логику держи вне виджетов — тогда первый слой покрывает большую часть кода, а UI-тесты добивают стыки. Главный запрет урока: в тестах не должно быть app.New() — он просит у операционной системы настоящий драйвер, и на машине без экрана сборка упадёт. Для всего этого есть test.NewApp, о нём дальше.

Первый тест: кнопка и счётчик

GO
package main

import (
	"fmt"
	"testing"

	"fyne.io/fyne/v2/test"
	"fyne.io/fyne/v2/widget"
)

func TestCounterButton(t *testing.T) {
	test.NewApp()
	label := widget.NewLabel("0")
	count := 0

	btn := widget.NewButton("Плюс", func() {
		count++
		label.SetText(fmt.Sprint(count))
	})

	test.Tap(btn)
	test.Tap(btn)

	if count != 2 {
		t.Fatalf("нажали дважды, счётчик равен %d", count)
	}
	if label.Text != "2" {
		t.Fatalf("метка показывает %q", label.Text)
	}
}

Тест живёт в обычном _test.go файле и запускается go test. test.Tap вызывает у кнопки событие нажатия — обработчик исполняется синхронно, и утверждения проверяются сразу. Окно даже не создавалось: клик по виджету не требует холста.

Почему в тестах нужен test.NewApp(), а не обычный app.New()?

test.NewApp: изолированная среда#

Каждый тест начинается с test.NewApp() — и это не замена одной строчки, а смена среды. Приложение создаётся с тестовым драйвером: окна не открываются, рендер идёт на невидимый холст, а всё персистентное — настройки из девятнадцатого урока и хранилище из двадцать первого — живёт в памяти и умирает с тестом. Именно поэтому тесты с preferences не трогают твой настоящий файл настроек: тест изменил ключ, закончился, и мир чист. Из этого следует правило гигиены: один test.NewApp на одну тест-функцию. Общее состояние между тестами — источник самых загадочных сбоев: тест прошёл один, упал в паре с соседним, а по отдельности оба зелёные. Тестовое окно создаётся через test.NewWindow(содержимое): оно разворачивает виджеты на невидимом холсте, применяет раскладку и измеряет размеры — без него проверки, зависящие от MinSize и позиций, не имеют смысла. Окно нужно и для событий, требующих холста: прокрутка и перетаскивание идут через canvas. Для темы есть test.ApplyTheme — подменить тему на время теста и проверить, что тёмный вариант из пятнадцатого урока не сломал контраст. И тонкость порядка: создавай виджеты после test.NewApp — часть инициализации виджетов читает тему приложения, и без тестового приложения она падает или берёт мусор. Среда готова — теперь в неё можно пустить пользователя.

Пользователь, которого вызвали руками

GO
func TestEchoForm(t *testing.T) {
	test.NewApp()
	label := widget.NewLabel("")
	entry := widget.NewEntry()
	entry.OnChanged = func(s string) {
		label.SetText("Эхо: " + s)
	}

	test.Type(entry, "ок")

	if label.Text != "Эхо: ок" {
		t.Fatalf("метка показала %q", label.Text)
	}
}

test.Type даёт полю фокус и побуквенно вводит строку: на каждый символ срабатывает OnChanged из девятого урока, эхо-метка обновляется сама — через обработчик, а не через прямую запись. Тест проверяет связку «ввод — обработчик — интерфейс», а не только переменную.

Что окажется в поле после ввода

Прочитайте код и выберите, что он выведет:

field := widget.NewEntry()
field.SetPlaceHolder("задача")

test.Type(field, "ок")
test.Type(field, "!")

fmt.Println(field.Text)

Симуляция пользователя: полный арсенал#

Пакет test покрывает все виды взаимодействий, которые ты учил в шестом уроке. Клик: test.Tap(obj) для левой кнопки по любому Tappable — кнопки, элементы списка из десятого урока; test.TapSecondary(obj) для правого — так открываются контекстные меню из четырнадцатого; test.TapAt(obj, pos) целится в конкретную точку виджета; test.DoubleTap(obj) для двойного нажатия. Ввод: test.Type(obj, строка) для Focusable-объектов — полей ввода и всего, что принимает клавиатуру; функция сама даёт фокус, поэтому порядок «сначала Type, потом проверки» работает без ритуалов. Прокрутка и жесты: test.Scroll(canvas, pos, dx, dy) крутит Scrollable-контейнеры, test.Drag(canvas, pos, dx, dy) тянет перетаскиваемые элементы — так проверяются списки с drag-реордером. Движение мыши без клика — test.MoveMouse. Важно понимать механику: это не эмуляция операционной системы с курсором и задержками, а прямой вызов событий на объекте. Отсюда скорость — сотни тестов за секунды — и две границы. Первая: реальную раскладку окна и попадание в координаты тест не проверяет — если кнопка перекрыта другим виджетом, test.Tap по ней всё равно сработает, и это ловит снимок разметки, а не клик. Вторая: асинхронное поведение из двадцатого урока — код, запущенный через fyne.Do, исполняется не мгновенно, и тест должен дождаться завершения, а не спать вслепую. В остальном арсенал покрывает всё, что делает палец.

Что делает test.Type перед вводом символов?

Снимки: золотая разметка#

Клик и ввод проверяют поведение, но не внешний вид: кнопка может потерять иконку, метка — съехать, контейнер — схлопнуться, и все тесты останутся зелёными. Для этого слоя есть снимки. test.AssertRendersToMarkup(t, «greeting.xml», w.Canvas()) разворачивает дерево объектов в текстовую разметку — структуру, типы виджетов, тексты, — и сравнивает с золотым файлом в каталоге testdata; файл не совпал — тест красный, и diff показывает ровно ту строку, которая изменилась. Сестринский test.AssertRendersToImage сравнивает целую картинку отрисовки, но у изображения есть врождённая болезнь: сглаживание и шрифты чуть-чуть отличаются между платформами, и снимок с ноутбука не сходится со снимком с CI. Поэтому бытовое правило — по умолчанию markup-снимок, image-снимок только когда проверяется именно пиксельная графика из седьмого урока. Быстрая проверка без каталога testdata: test.RenderToMarkup(canvas) возвращает строку разметки, и её можно сравнить в тесте напрямую или напечатать при отладке. Когда снимок красный, у тебя два честных исхода: регрессия — чини код, и осознанное изменение дизайна — обнови золотой файл и упомяни это в коммите. Не обновляй снимки машинально: золотой файл — это договор о том, как выглядит приложение, и его правка должна быть решением, а не привычкой. Меняешь тему из пятнадцатого урока — снимки честно покраснеют и заставят перечитать, что именно поменялось.

Снимок экрана приветствия

GO
func TestGreetingMarkup(t *testing.T) {
	test.NewApp()
	w := test.NewWindow(
		widget.NewCard("Добро пожаловать", "", widget.NewLabel("Привет, Гость")),
	)

	test.AssertRendersToMarkup(t, "greeting.xml", w.Canvas())
}

Холст тестового окна разворачивается в разметку и сверяется с testdata/greeting.xml. Снимок ловит и структуру — карта внутри, метка в ней, — и тексты: замени заголовок карты, и diff укажет строку. Разметка текстовая, поэтому шрифты и сглаживание платформы ей безразличны.

Сломается ли снимок

Прочитайте код и выберите, что он выведет:

w := test.NewWindow(widget.NewLabel("Привет"))
old := test.RenderToMarkup(w.Canvas())

w.Content().(*widget.Label).SetText("Пока")

fmt.Println(old == test.RenderToMarkup(w.Canvas()))

Моки и подмены#

Тестирование fyne — это ещё и умение подменять мир вокруг интерфейса. Первый мок уже встроен: test.NewApp даёт настройки и хранилище в памяти, поэтому код, читающий preferences из девятнадцатого урока, тестируется без диска — установи ключи в начале теста и проверь, как экран их подхватывает. Второй слой — собственные фейки для данных: если список из десятого урока питается из интерфейса Store, тест подсовывает реализацию с фиксированным срезом — и проверяет, что List показал ровно их и вызвал шаблон нужное число раз. Отсюда и архитектурная награда разделения: чем тоньше слой виджетов, тем меньше UI-тестов нужно. Третий инструмент — рендерер кастомных виджетов из восьмого урока: test.WidgetRenderer(wid) возвращает WidgetRenderer, и тест может проверить объекты напрямую — сколько их, каких типов, с какими цветами. Это точечная проверка внутренностей без снимков: полезно для своих контролов, где разметка длинная и шумная. Четвёртая подмена — время и темы: test.ApplyTheme подставляет тему, а test.NewTheme собирает тестовую; если твой виджет берёт цвета из темы, подмена доказывает, что хардкода нет. Правило отбора моков простое: подменяй границы — диск, сеть, время, — но не подменяй виджеты, которые тестируешь: тест, в котором кнопка — мок, проверяет только твой оптимизм. Фальшивое хранилище — да, фальшивая кнопка — нет.

Проверка рендерера своего виджета

GO
func TestBadgeRenderer(t *testing.T) {
	test.NewApp()
	badge := NewBadge("новое")
	r := test.WidgetRenderer(badge)

	if len(r.Objects()) != 2 {
		t.Fatalf("у рендера %d объекта, ожидали два", len(r.Objects()))
	}
	rect, ok := r.Objects()[0].(*canvas.Rectangle)
	if !ok {
		t.Fatal("первый объект должен быть фоном")
	}
	want := theme.Color(theme.ColorNamePrimary)
	if rect.FillColor != want {
		t.Fatal("фон не акцентный цвет темы")
	}
}

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

Что уместно подменить моком в UI-тесте?

Каталог тестового пакета#

APIЧто делаетКогда звать
test.NewApp()приложение с тестовым драйвером без экранапервая строка каждой UI-тест-функции
test.NewWindow(content)окно с раскладкой на невидимом холстекогда нужны размеры, позиции, снимки
test.Tap(obj)клик по Tappableкнопки, пункты списков, действия
test.TapSecondary(obj)правый клик по SecondaryTappableконтекстные меню
test.Type(obj, chars)ввод строки посимвольно с автофокусомEntry и другие Focusable
test.DoubleTap(obj)двойной кликэлементы списков, открытие
test.Scroll(c, pos, dx, dy)прокрутка Scrollableдлинные списки и формы
test.Drag(c, pos, dx, dy)перетаскиваниереордер списков, слайдеры
test.RenderToMarkup(c)строка разметки холстабыстрые сравнения и отладка
test.AssertRendersToMarkupснимок разметки против золотого файлаосновной снимок по умолчанию
test.AssertRendersToImageснимок-картинка против золотого файлапроверка пиксельной графики
test.WidgetRenderer(wid)рендерер виджета и его объектытесты кастомных виджетов
test.ApplyTheme(t, theme)подмена темы на время тестапроверка тематических цветов
test.NewClipboard()чистый буфер обмена для тестапроверка копирования и вставки

Собери тест поля ввода

Восстанови четыре пропуска: тестовое приложение, окно, ввод текста и проверка эха.

func TestEcho(t *testing.T) { app := test.() entry := widget.NewEntry() window := test.(entry) test.(entry, "привет") if entry. != "привет" { t.Fatal("текст не попал в поле") } _ = app _ = window }

Путь одного go test

Расставь по порядку, что происходит от запуска go test до зелёной строки отчёта.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Практика: тестируй логику отдельно#

Соберём пирамиду для реального приложения. Основание — чистые структуры: хранилище задач финального проекта тестируется без единого виджета — добавил, отметил, удалил, сохранил, перечитал; таблица тестов Go покрывает граничные случаи дешевле любых UI-симуляций. Средний слой — валидация и связки: валидатор из восемнадцатого урока проверяется прямым вызовом Validate, привязки из семнадцатого — чтением источника после Set. Вершина — UI-тесты: по одному на пользовательский сценарий, не на каждый виджет — «ввёл текст, нажал добавить, задача появилась в списке и в хранилище», «правый клик по задаче открывает меню удаления», «закрытие с несохранённым спрашивает подтверждение» из двадцать третьего урока. На эту вершину вешаются markup-снимки ключевых экранов — по одному на состояние: пустой список, список с задачами, экран настроек. Итоговая пропорция зрелого проекта: десятки тестов логики, несколько поведенческих UI-тестов, пара снимков на экран. Если всё наоборот — тысячи кликов и ни одной проверки хранилища — тесты будут медленными и хрупкими. Прогони готовый проект по этой пирамиде в двадцать пятом уроке: там каждая функция итогового приложения уже расчищена под тест именно так.

Что дальше: итоговый проект#

Теперь у курса есть система контроля качества: логика проверяется чистыми тестами, поведение — симулированным пользователем, отрисовка — снимками, а моки закрывают доступ к настоящему диску. Опора урока — третий урок с виджетами, восьмой с рендерерами, десятый со списками, девятнадцатый с хранением и двадцать третий с поведением окна, которое тоже теперь под тестом. Финальная тема — итоговый проект: менеджер задач, который собирает все двадцать четыре урока в одно приложение, от модели данных до темы, и живёт на диске между запусками. Полный маршрут — на странице курса Fyne.

Термины урока

Термин

test.NewApp

нажмите, чтобы перевернуть
Определение

Приложение с тестовым драйвером: без экрана и GPU, настройки и хранилище — в памяти. Обязательная первая строка UI-теста вместо app.New.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Виртуальный экран

HTML-модель виджет-дерева fyne
Тестированиеfyne · мок

Ожидание: 0

Журнал событий

Экран собран: «Тестирование»

Коротко: что запомнить

  • GUI тестируется в go test: события подсовываются виджетам напрямую, монитор не нужен, а app.New() в тестах запрещён.
  • test.NewApp даёт изолированную среду: тестовый драйвер, настройки и хранилище в памяти, чистота между тест-функциями.
  • test.Tap, TapSecondary, DoubleTap, Type, Scroll и Drag покрывают весь арсенал взаимодействий из урока про события.
  • Markup-снимок — основной: текстовая разметка не зависит от шрифтов и платформы; image-снимок — только для пиксельной графики.
  • Моками подменяют границы — диск, сеть, время; виджеты под тестом остаются настоящими.
  • Пирамида: много чистых тестов логики, несколько UI-сценариев, пара снимков на экран.
  • Красный золотой файл — это развилка: регрессия чинится кодом, осознанный редизайн обновляет файл в отдельном коммите.

Домашнее задание

Возьми любой экран из предыдущих уроков — например, форму входа или список — и накрой его тестами трёх слоёв. Первый: чистая логика — вынеси обработку ввода в структуру и напиши табличный тест на граничные случаи. Второй: поведение — test.NewApp, test.NewWindow, test.Type с валидной и невалидной строкой из восемнадцатого урока, test.Tap по кнопке отправки; проверь, что состояние хранилища изменилось ровно один раз и метка ошибки появилась при невалидном вводе. Третий: снимок — AssertRendersToMarkup для двух состояний экрана, пустого и заполненного; золотые файлы положи в testdata и посмотри их глазами. Отдельно проверь гигиену: каждый тест начинается с test.NewApp, в коде нет ни одного app.New, ни одного time.Sleep. Затем сломай что-нибудь нарочно — удали у кнопки обработчик или переименуй заголовок — и убедись, что конкретный тест краснеет с понятным diff, а не весь пакет сразу. В финале добавь тест рендерера, если у тебя есть кастомный виджет из восьмого урока: проверь число объектов и тип первого. Собери отчёт go test -v и сохрани его рядом с проектом — он станет базой, с которой будешь сравнивать проект после рефакторинга.

Личные заметкиПусто
Хранятся только в вашем браузере (Fyne · Тестирование fyne-приложений)

Вопросы и ответы по уроку

Нужен ли для тестов Fyne настоящий экран или Xvfb?

Нет: test.NewApp использует тестовый драйвер, который рендерит на невидимый холст. Тесты идут в обычном go test на CI без дисплея; настоящий экран нужен только для ручной проверки и image-снимков, чувствительных к рендеру.

Почему markup-снимок предпочтительнее image-снимка?

Разметка текстовая: она фиксирует структуру виджетов, свойства и тексты, но не зависит от сглаживания, шрифтов и версии графической библиотеки. Image-снимок сравнивает пиксели и часто краснеет от различий между машинами, поэтому его берут точечно.

Как тестировать код, который запущен через fyne.Do из горутины?

Не спать вслепую, а синхронизировать событие: завершай фоновую работу отправкой в канал, который тест читает с таймаутом, либо проверяй состояние после явного ожидания готовности. Голый time.Sleep делает тесты медленными и мигающими.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Cobra25 уроков~10 чпрактика25 уроков уже доступны

Cobra: CLI-приложения на Go

Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.

  • Создавать CLI-приложения с деревом команд на Cobra
  • Работать с аргументами, флагами и их привязкой к конфигу
  • Управлять конфигурацией через Viper: файлы, env, приоритеты
  • Писать хуки PreRun/PostRun и корректно обрабатывать ошибки