Большинство разработчиков предпочитают тестировать код без использования моков. Например, при интеграции с Elasticsearch логичнее запустить контейнер локально и тестировать Go-код непосредственно с ним. Но в реальном мире часто возникают ситуации, когда создание локального окружения становится практически невозможным. В отличие от языков с динамической типизацией, в Go нельзя просто подменить реализацию структуры во время выполнения. Система типов языка строго статична, что делает тестирование с использованием реальных зависимостей сложнее. Кроме того, Go поощряет явную обработку ошибок, и тестирование различных сценариев ошибок становится критически важным.
Особая фишка Go - это интерфейсы. Они являются идеальным инструментом для создания моков. Определив интерфейс, вы можете создать реальную имплементацию для продакшена и мок-имплементацию для тестов. Это дает гибкость без ущерба для типобезопасности. Еще один аспект - производительность тестов. Взаимодействие с реальными внешними системами делает тесты медленными и потенциально нестабильными. Моки позволяют запускать тесты быстрее и с предсказуемыми результатами.
Архитектурный подход к дизайну кода также сильно влияет на необходимость мокирования. Принцип внедрения зависимостей (Dependency Injection) становится ключевым. Вместо жесткой привязки к конкретным реализациям компонентов, код должен зависеть от абстракций - интерфейсов. Тогда подмена реальных объектов моками происходит естественно и элегантно.
Поскольку Go - это язык с ярко выраженным прагматичным подходом, моки в нем должны быть максимально простыми и функциональными. Не нужно перегружать код сложными фреймворками для мокирования - часто достаточно небольших, сфокусированных интерфейсов и структур, реализующих их. Особую ценность моки приобретают при работе с конкурентным кодом, который в Go встречается повсеместно благодаря горутинам и каналам. Тестирование многопоточного кода, взаимодействующего с внешними системами, без моков превращается в настоящую головную боль из-за непредсказуемых таймингов и состояний гонки.
Анатомия mock-объектов
Моки в Go - это не просто замена реальных объектов, это фундаментальный архитектурный подход к проектированию тестируемого кода.
Интерфейсы - основа всего мокирования
В отличие от языков вроде Python или JavaScript, где можно подменить практически любую функцию или метод на лету, в Go нельзя просто так создать мок для конкретной структуры. Вся система мокирования в Go завязана на интерфейсах, и это один из самых глубоких и важных аспектов дизайна языка. Интерфейс в Go определяет поведение через набор методов. Любой тип, который реализует все методы интерфейса, неявно удовлетворяет этому интерфейсу. Это позволяет легко создавать альтернативные имплементации - реальные для продакшена и моковые для тестов.
| Go | 1
2
3
4
| type S3Client interface {
CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error)
HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error)
} |
|
Такой подход прямо коррелирует с принципом инверсии зависимостей из SOLID. Модули высокого уровня не должны зависеть от модулей низкого уровня, а оба должны зависеть от абстракций. В случае Go абстракциями являются интерфейсы.
Моки, стабы и фейки - разбираемся в терминологии
Часто термины "мок", "стаб" и "фейк" используют как взаимозаменяемые, но между ними есть существенные различия:
Стаб (Stub) - простейшая реализация, которая возвращает захардкоженные значения. Обычно стабы не содержат логики, а просто дают предопределенный ответ.
| Go | 1
2
3
4
5
| type S3ClientStub struct{}
func (s S3ClientStub) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
return &s3.CreateBucketOutput{}, nil
} |
|
Мок (Mock) - более сложная реализация, которая позволяет верифицировать взаимодействие с объектом. Моки запоминают вызовы методов, их параметры и могут проверять, был ли вызван конкретный метод с определенными аргументами.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| type S3ClientMock struct {
createBucketCalled bool
createBucketParams *s3.CreateBucketInput
}
func (m *S3ClientMock) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
m.createBucketCalled = true
m.createBucketParams = params
return &s3.CreateBucketOutput{}, nil
}
func (m *S3ClientMock) VerifyCreateBucketCalledWith(expectedName string) bool {
return m.createBucketCalled && *m.createBucketParams.Bucket == expectedName
} |
|
Фейк (Fake) - полноценная рабочая реализация, но более простая, чем продакшен-версия. Например, вместо реальной базы данных можно использовать in-memory реализацию.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| type InMemoryS3 struct {
buckets map[string]bool
}
func NewInMemoryS3() *InMemoryS3 {
return &InMemoryS3{buckets: make(map[string]bool)}
}
func (s *InMemoryS3) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
name := *params.Bucket
if _, exists := s.buckets[name]; exists {
return nil, errors.New("bucket already exists")
}
s.buckets[name] = true
return &s3.CreateBucketOutput{}, nil
} |
|
Механизмы создания и внедрения mock-объектов
Существует несколько подходов к созданию и внедрению моков в Go:
1. Ручное создание - самый простой подход, когда вы сами определяете структуру и реализуете необходимые методы. Преимущество в контроле и простоте, недостаток - рутинность при большом количестве методов.
2. Встраивание интерфейса - техника, которая позволяет переопределить только нужные методы:
| Go | 1
2
3
4
5
6
7
8
9
| type MockS3Client struct {
S3Client // встраиваем интерфейс
createBucketErr error
}
// Переопределяем только нужный метод
func (m MockS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
return &s3.CreateBucketOutput{}, m.createBucketErr
} |
|
3. Генерация моков - использование инструментов для автоматического создания моков на основе интерфейсов (об этом в следующей главе).
Внедрение моков обычно происходит через:
Конструкторы:
| Go | 1
2
3
| func NewBucketManager(client S3Client) *BucketManager {
return &BucketManager{client: client}
} |
|
Явные поля:
| Go | 1
2
3
| type BucketManager struct {
client S3Client
} |
|
Функциональные опции:
| Go | 1
2
3
4
5
| func WithS3Client(client S3Client) func(*BucketManager) {
return func(bm *BucketManager) {
bm.client = client
}
} |
|
Создание кастомных mock-объектов без внешних библиотек
Одна из сильных сторон Go - возможность создавать эффективные моки без привлечения сторонних фреймворков. Рассмотрим пример создания гибкого мока для AWS S3:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| type MockS3Client struct {
createBucketError error
headBucketError error
bucketExists bool
createBucketCalls int
headBucketCalls int
}
func (m *MockS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
m.createBucketCalls++
return &s3.CreateBucketOutput{}, m.createBucketError
}
func (m *MockS3Client) HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error) {
m.headBucketCalls++
if !m.bucketExists {
return nil, &types.NotFound{Message: aws.String("bucket not found")}
}
return &s3.HeadBucketOutput{}, m.headBucketError
}
// Удобные методы для настройки поведения
func (m *MockS3Client) WithCreateBucketError(err error) *MockS3Client {
m.createBucketError = err
return m
}
func (m *MockS3Client) WithBucketExists(exists bool) *MockS3Client {
m.bucketExists = exists
return m
} |
|
Такой подход позволяет создавать гибкие моки с возможностью настройки поведения для разных тестовых сценариев:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| func TestBucketCreation(t *testing.T) {
t.Run("successful creation", func(t *testing.T) {
mock := &MockS3Client{}
manager := NewBucketManager(mock)
err := manager.CreateBucket("test-bucket", "us-west-2")
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if mock.createBucketCalls != 1 {
t.Errorf("expected 1 call to CreateBucket, got %d", mock.createBucketCalls)
}
})
t.Run("creation failure", func(t *testing.T) {
mock := &MockS3Client{}.WithCreateBucketError(errors.New("failed to create bucket"))
manager := NewBucketManager(mock)
err := manager.CreateBucket("test-bucket", "us-west-2")
if err == nil {
t.Error("expected error, got nil")
}
})
} |
|
Кастомный подход дает полный контроль над поведением мока и четкое понимание происходящего, что особенно ценно при отладке сложных тестов. Однако при большом количестве методов в интерфейсе ручное создание становится утомительным, и тут на помощь приходят инструменты генерации.
Интересный подход - создание универсального мока, который настраивается через замыкания:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
| type MockFunc func(method string, args ...interface{}) (interface{}, error)
type GenericMock struct {
mockFunc MockFunc
}
func (m GenericMock) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
result, err := m.mockFunc("CreateBucket", ctx, params, optFns)
if err != nil {
return nil, err
}
return result.(*s3.CreateBucketOutput), nil
} |
|
Этот подход особенно полезен, когда нужно быстро создать мок для тестирования одного конкретного сценария, не тратя время на полную реализацию всех методов интерфейса.
Принципы создания композитных mock-объектов для сложных зависимостей
Реальные приложения редко ограничиваются простыми интерфейсами с парой методов. Что делать, когда приходится мокировать сложную систему с десятками методов или несколькими взаимосвязанными интерфейсами? Тут на помощь приходят композитные моки. Композитный мок представляет собой структуру, которая реализует несколько интерфейсов одновременно или включает в себя другие моки. Такой подход особенно полезен, когда тестируемый код взаимодействует с несколькими зависимостями через единый фасад.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Интерфейс для управления корзинами
type BucketManager interface {
CreateBucket(name, region string) error
DeleteBucket(name string) error
ListBuckets() ([]string, error)
}
// Интерфейс для управления объектами
type ObjectManager interface {
PutObject(bucket, key string, data []byte) error
GetObject(bucket, key string) ([]byte, error)
DeleteObject(bucket, key string) error
}
// Композитный интерфейс
type S3Service interface {
BucketManager
ObjectManager
}
// Композитный мок
type MockS3Service struct {
MockBucketManager
MockObjectManager
} |
|
При таком подходе каждый компонент можно настраивать независимо:
| Go | 1
2
3
4
5
6
7
8
9
10
11
| mock := &MockS3Service{
MockBucketManager: MockBucketManager{
Buckets: []string{"test-bucket"},
CreateBucketError: nil,
},
MockObjectManager: MockObjectManager{
Objects: map[string][]byte{
"test-bucket/key1": []byte("data"),
},
},
} |
|
Однако композитные моки могут стать источником проблем при изменении базовых интерфейсов. Если один из интерфейсов обновляется, все зависимые композитные моки тоже придется обновить. Это может превратиться в кошмар сопровождения.
Решение? Структурировать моки по принципу малых, узкоспециализированных компонентов, следуя философии Unix: "Делай одну вещь и делай ее хорошо". Лучше иметь несколько маленьких моков, чем один гигантский.
Таблично-управляемые тесты с моками
Одна из сильнейших идиом тестирования в Go - это таблично-управляемые тесты (table-driven tests). Их комбинация с моками дает мощный инструмент для тестирования различных сценариев:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| func TestCreateS3Bucket(t *testing.T) {
tests := []struct {
name string
bucketName string
region string
mockError error
expectError bool
expectedCalls int
}{
{
name: "successful creation",
bucketName: "test-bucket",
region: "us-west-2",
mockError: nil,
expectError: false,
expectedCalls: 1,
},
{
name: "failed creation",
bucketName: "test-bucket",
region: "us-west-2",
mockError: errors.New("access denied"),
expectError: true,
expectedCalls: 1,
},
{
name: "empty bucket name",
bucketName: "",
region: "us-west-2",
mockError: nil,
expectError: true,
expectedCalls: 0, // Ожидаем, что метод даже не будет вызван
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &MockS3Client{createBucketError: tt.mockError}
err := createS3Bucket(mock, tt.bucketName, tt.region)
if (err != nil) != tt.expectError {
t.Errorf("createS3Bucket() error = %v, expectError %v", err, tt.expectError)
}
if mock.createBucketCalls != tt.expectedCalls {
t.Errorf("expected %d calls to CreateBucket, got %d", tt.expectedCalls, mock.createBucketCalls)
}
})
}
} |
|
Такой подход не только позволяет тестировать разные сценарии, но и делает тесты более поддерживаемыми. Добавление нового сценария требует лишь добавления новой записи в таблицу тестов.
Моки для конкурентного кода
Особую сложность представляет тестирование конкурентного кода в Go. Моки для горутин и каналов требуют особого подхода:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| type WorkerPool interface {
Submit(task func() error) error
Wait() []error
}
type MockWorkerPool struct {
tasks []func() error
failNext bool
mu sync.Mutex
}
func (m *MockWorkerPool) Submit(task func() error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failNext {
m.failNext = false
return errors.New("pool is full")
}
m.tasks = append(m.tasks, task)
return nil
}
func (m *MockWorkerPool) Wait() []error {
m.mu.Lock()
defer m.mu.Unlock()
results := make([]error, 0, len(m.tasks))
for _, task := range m.tasks {
results = append(results, task())
}
// Очищаем список задач после выполнения
m.tasks = nil
return results
}
func (m *MockWorkerPool) FailNextSubmit() {
m.mu.Lock()
defer m.mu.Unlock()
m.failNext = true
} |
|
Этот мок позволяет тестировать код, использующий пул воркеров, без реального создания горутин. Он выполняет задачи последовательно, что делает тесты детерминированными и предсказуемыми.
Контроль времени в тестах
Еще одна сложная область - тестирование кода, зависящего от времени. В Go существует паттерн внедрения часов:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| type Clock interface {
Now() time.Time
Sleep(duration time.Duration)
}
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
func (RealClock) Sleep(d time.Duration) { time.Sleep(d) }
type MockClock struct {
currentTime time.Time
sleepCalls int
}
func NewMockClock(t time.Time) *MockClock {
return &MockClock{currentTime: t}
}
func (m *MockClock) Now() time.Time {
return m.currentTime
}
func (m *MockClock) Sleep(d time.Duration) {
m.currentTime = m.currentTime.Add(d)
m.sleepCalls++
}
func (m *MockClock) Forward(d time.Duration) {
m.currentTime = m.currentTime.Add(d)
} |
|
Такой мок позволяет контролировать течение времени в тестах, что особенно полезно при тестировании кода с таймаутами, повторными попытками или планировщиками.
Как пользоваться моками? нужно протестировать CRUD. создал моки репозиториев, сгенерировал код с помощью gomock. а что... Golang Gin web applicaton as linux service unit Вхожу в линукс (CentOS) очень ранний этап, скомпилировал тестовое приложение, настроил фаервал,... Singleton для log и тестирование Добрый день. подскажите как правильно, есть singleton для логов. понятное дело виден отосвюду во... Тестирование Здравствуйте. Подскажите пожалуйста. как можно сделать тесты для функции которая не возвращает...
Практические инструменты мокинга
Хотя ручное создание моков в Go вполне осуществимо, оно быстро становится утомительным занятием при работе с большими интерфейсами. К счастью, экосистема Go предлагает множество инструментов, которые автоматизируют процесс и делают мокирование более эффективным. Разберемся с наиболее популярными решениями и их особенностями.
Библиотека testify/mock - надежный фундамент
Библиотека testify от Stretchr — один из самых популярных инструментов тестирования в Go. Её подпакет mock предоставляет мощный фреймворк для мокирования интерфейсов с возможностью точного контроля над ожидаемыми вызовами методов. Базовый принцип работы testify/mock заключается в создании структуры мока, которая встраивает mock.Mock:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import (
"github.com/stretchr/testify/mock"
"context"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type MockS3Client struct {
mock.Mock
}
func (m *MockS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
args := m.Called(ctx, params, optFns)
return args.Get(0).(*s3.CreateBucketOutput), args.Error(1)
} |
|
Ключевое преимущество testify/mock — возможность настройки ожиданий:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func TestCreateBucket(t *testing.T) {
// Создаем мок
mockClient := new(MockS3Client)
// Настраиваем ожидание
mockClient.On("CreateBucket", mock.Anything, mock.MatchedBy(func(input *s3.CreateBucketInput) bool {
return *input.Bucket == "test-bucket"
}), mock.Anything).Return(&s3.CreateBucketOutput{}, nil)
// Используем мок
manager := NewBucketManager(mockClient)
err := manager.CreateBucket("test-bucket", "us-west-2")
// Проверяем результат
assert.NoError(t, err)
// Проверяем, что ожидания были выполнены
mockClient.AssertExpectations(t)
} |
|
Testify предлагает гибкую систему сопоставления аргументов:
mock.Anything — соответствует любому значению,
mock.MatchedBy(func) — проверяет аргумент с помощью функции,
mock.AnythingOfType("string") — соответствует значению указанного типа.
Библиотека также поддерживает последовательные вызовы с разными результатами:
| Go | 1
2
3
4
| mockClient.On("HeadBucket", mock.Anything, mock.Anything).
Return(nil, &types.NotFound{}).Once(). // Первый вызов: бакет не найден
On("HeadBucket", mock.Anything, mock.Anything).
Return(&s3.HeadBucketOutput{}, nil) // Последующие вызовы: бакет существует |
|
Такой подход отлично подходит для тестирования механизмов повторных попыток или операций с отложенным результатом.
Генерация моков с помощью mockery
Ручное создание структур моков, даже с использованием testify/mock, может быть трудоемким. Инструмент mockery решает эту проблему, автоматически генерируя моки из определений интерфейсов. Установка mockery проста:
| Bash | 1
| go install github.com/vektra/mockery/v2@latest |
|
Затем генерация мока для интерфейса выполняется одной командой:
| Bash | 1
| mockery --name=S3Client --output=./mocks |
|
Сгенерированный код включает реализацию всех методов интерфейса с использованием testify/mock:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // Code generated by mockery v2.16.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
s3 "github.com/aws/aws-sdk-go-v2/service/s3"
)
// S3Client is an autogenerated mock type for the S3Client type
type S3Client struct {
mock.Mock
}
// CreateBucket provides a mock function with given fields: ctx, params, optFns
func (_m *S3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
_va := make([]interface{}, len(optFns))
for _i := range optFns {
_va[_i] = optFns[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, params)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *s3.CreateBucketOutput
if rf, ok := ret.Get(0).(func(context.Context, *s3.CreateBucketInput, ...func(*s3.Options)) *s3.CreateBucketOutput); ok {
r0 = rf(ctx, params, optFns...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*s3.CreateBucketOutput)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *s3.CreateBucketInput, ...func(*s3.Options)) error); ok {
r1 = rf(ctx, params, optFns...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HeadBucket provides a mock function...
// [остальной код опущен для краткости] |
|
Mockery поддерживает множество полезных опций:
--with-expecter — генерирует дополнительные методы для упрощения настройки ожиданий,
--filename — задает имя выходного файла,
--dir — указывает директорию с исходными интерфейсами,
--keeptree — сохраняет структуру директорий при генерации.
Особенно удобная функция mockery — возможность автоматического обновления моков при изменении интерфейсов. Для этого можно добавить в Makefile команду:
| Bash | 1
2
3
| .PHONY: mocks
mocks:
mockery --all --keeptree --output=./mocks |
|
Интеграция с CI/CD пайплайнами позволяет автоматически проверять актуальность моков:
| YAML | 1
2
3
| check-mocks:
mockery --all --keeptree --output=./tmp/mocks
diff -r ./mocks ./tmp/mocks || (echo "Mocks are outdated. Run 'make mocks'" && exit 1) |
|
Альтернативные решения
Помимо testify/mock и mockery существуют и другие инструменты для мокирования в Go.
GoMock — официальный инструмент от Google, похожий на mockery:
| Bash | 1
2
| go install github.com/golang/mock/mockgen@latest
mockgen -source=s3client.go -destination=mock_s3client.go -package=mocks |
|
GoMock использует свой фреймворк для настройки ожиданий:
| Go | 1
2
3
4
5
6
7
| mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockClient := mocks.NewMockS3Client(mockCtrl)
mockClient.EXPECT().
CreateBucket(gomock.Any(), gomock.Any(), gomock.Any()).
Return(&s3.CreateBucketOutput{}, nil) |
|
Pegomock — еще один генератор моков с синтаксисом, вдохновленным Mockito:
| Go | 1
2
3
| mockClient := NewMockS3Client()
When(mockClient.CreateBucket(AnyContext(), AnyS3CreateBucketInput(), AnyS3Options())).
ThenReturn(&s3.CreateBucketOutput{}, nil) |
|
Minimock — легковесный генератор моков, фокусирующийся на производительности:
| Go | 1
2
3
4
5
| mc := minimock.NewController(t)
defer mc.Finish()
mockClient := NewS3ClientMock(mc).
CreateBucketMock.Return(&s3.CreateBucketOutput{}, nil) |
|
Moq — инструмент, который генерирует функциональные моки без зависимостей:
| Go | 1
2
3
4
5
| mocker := &S3ClientMoq{
CreateBucketFunc: func(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
return &s3.CreateBucketOutput{}, nil
},
} |
|
Выбор инструмента зависит от проекта: если уже используется testify для ассертов, логично выбрать testify/mock с mockery. Для проектов с высокими требованиями к производительности тестов может подойти minimock. GoMock хорошо интегрируется с другими инструментами Google.
Сравнительный анализ производительности
Производительность mock-библиотек может быть критична при большом количестве тестов. Сравнительные бенчмарки показывают, что:
1. Minimock обычно показывает лучшую производительность благодаря минимальному использованию рефлексии.
2. Ручные моки без использования фреймворков работают быстрее всего.
3. Testify/mock имеет средние показатели производительности.
4. GoMock немного медленнее testify, но разница редко бывает заметной.
Однако важно помнить, что производительность самих тестов обычно не так критична, как удобство написания и поддержки. Сэкономленное на разработке время часто перевешивает незначительные различия в скорости выполнения тестов.
Автоматизация процесса обновления моков при изменении интерфейсов
Одной из самых болезненных проблем при использовании моков является их синхронизация с изменяющимися интерфейсами. Изменили интерфейс — нужно обновить и все моки, иначе тесты перестанут компилироваться. В крупных проектах такая синхронизация может превратиться в настоящий кошмар. Интеграция генерации моков в процесс сборки проекта решает эту проблему. Добавление команды генерации в go generate позволяет автоматизировать процесс:
| Go | 1
2
3
4
| //go:generate mockery --name=S3Client --output=./mocks
type S3Client interface {
// методы интерфейса
} |
|
Теперь обновление моков выполняется простой командой:
Более продвинутый подход — использование pre-commit хуков Git:
| Bash | 1
2
3
4
| #!/bin/sh
# .git/hooks/pre-commit
go generate ./...
git add ./mocks/ |
|
Такой хук автоматически обновляет и добавляет в индекс измененные моки перед каждым коммитом, гарантируя их актуальность.
Инкрементальная генерация и кэширование
При работе с большими проектами генерация всех моков может занимать значительное время. Инкрементальный подход помогает сократить это время:
| Bash | 1
2
3
4
5
6
| changed_files=$(git diff --name-only HEAD)
for file in $changed_files; do
if grep -q "type.*interface" "$file"; then
go generate "$file"
fi
done |
|
Этот скрипт находит измененные файлы с интерфейсами и генерирует моки только для них.
Моки для стандартной библиотеки
Стандартная библиотека Go не проектировалась специально для тестирования с моками, но часто возникает необходимость мокировать такие пакеты как os, http или time. Решение — создание оберток-интерфейсов:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| type FileSystem interface {
Open(name string) (File, error)
Stat(name string) (os.FileInfo, error)
Remove(name string) error
}
type File interface {
io.ReadWriteCloser
Name() string
Stat() (os.FileInfo, error)
}
type RealFileSystem struct{}
func (RealFileSystem) Open(name string) (File, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
return realFile{f}, nil
}
type realFile struct {
*os.File
}
// Реализуем интерфейс File...
// Теперь можно создать мок
type MockFileSystem struct {
mock.Mock
}
func (m *MockFileSystem) Open(name string) (File, error) {
args := m.Called(name)
return args.Get(0).(File), args.Error(1)
} |
|
Такой подход требует дополнительного кода, но позволяет эффективно тестировать функции, работающие с файловой системой, сетью и другими системными ресурсами.
Моки для REST API и HTTP клиентов
HTTP-взаимодействие — одна из самых частых областей применения моков. Для этого часто используют httptest из стандартной библиотеки:
| Go | 1
2
3
4
5
6
7
8
9
10
11
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/buckets" {
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"status":"created"}`))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
client := NewBucketClient(server.URL) |
|
Для более сложных случаев удобно использовать библиотеки вроде h2non/gock:
| Go | 1
2
3
4
5
6
7
8
9
| gock.New("https://api.aws.com").
Post("/buckets").
MatchHeader("Authorization", "^Bearer .+$").
Reply(201).
JSON(map[string]string{"status": "created"})
// Теперь любой HTTP-клиент, обращающийся к этому URL,
// получит мок-ответ вместо реального запроса
client := NewBucketClient("https://api.aws.com") |
|
Такой подход позволяет мокировать внешние API без изменения кода приложения.
Гибридные подходы и композиция инструментов
На практике часто используется не один инструмент, а комбинация разных подходов. Например, для интерфейсов с большим количеством методов можно использовать генерацию с mockery, а для простых случаев — ручное создание моков.
Комбинирование моков с другими инструментами тестирования тоже дает интересные возможности:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Интеграция с fuzzing-тестированием
func FuzzCreateBucket(f *testing.F) {
f.Add("bucket-name", "us-west-2")
f.Fuzz(func(t *testing.T, name, region string) {
mock := &MockS3Client{}
// Настраиваем мок в зависимости от входных данных
if len(name) < 3 {
mock.createBucketError = errors.New("invalid bucket name")
}
err := createS3Bucket(mock, name, region)
// Проверки в зависимости от входных данных
if len(name) < 3 && err == nil {
t.Error("expected error for short bucket name")
}
})
} |
|
Такой подход позволяет комбинировать преимущества разных техник тестирования для более полного покрытия кода.
Реальные примеры применения
Теория моков захватывающа своей элегантностью, но без практического применения она остается лишь набором абстрактных идей. Погрузимся в реальные боевые сценарии, где моки помогают создавать надежные тесты и экономят нервные клетки разработчиков.
Тестирование HTTP-клиентов
HTTP-взаимодействие — классический пример внешней зависимости, идеально подходящий для мокирования. Представим, что у нас есть сервис для работы с внешним API:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| type WeatherService struct {
client *http.Client
apiKey string
baseURL string
}
func (ws *WeatherService) GetTemperature(city string) (int, error) {
url := fmt.Sprintf("%s/weather?city=%s&apikey=%s",
ws.baseURL, url.QueryEscape(city), ws.apiKey)
resp, err := ws.client.Get(url)
if err != nil {
return 0, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var result struct {
Temperature int `json:"temp"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, fmt.Errorf("failed to decode response: %w", err)
}
return result.Temperature, nil
} |
|
Чтобы протестировать этот сервис без реальных HTTP-запросов, у нас есть несколько вариантов.
Вариант 1: Использование httptest
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| func TestWeatherService_GetTemperature(t *testing.T) {
// Создаем тестовый сервер
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Проверяем параметры запроса
query := r.URL.Query()
city := query.Get("city")
apiKey := query.Get("apikey")
if apiKey != "test-key" {
w.WriteHeader(http.StatusUnauthorized)
return
}
if city == "Unknown" {
w.WriteHeader(http.StatusNotFound)
return
}
// Возвращаем тестовые данные
w.Header().Set("Content-Type", "application/json")
temperature := 25
if city == "Moscow" {
temperature = 15
}
json.NewEncoder(w).Encode(map[string]int{"temp": temperature})
}))
defer server.Close()
// Создаем тестируемый сервис с нашим мок-сервером
service := &WeatherService{
client: http.DefaultClient,
apiKey: "test-key",
baseURL: server.URL,
}
// Тестируем успешный сценарий
temp, err := service.GetTemperature("Moscow")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if temp != 15 {
t.Errorf("Expected temperature 15, got %d", temp)
}
// Тестируем ошибку авторизации
service.apiKey = "wrong-key"
_, err = service.GetTemperature("Moscow")
if err == nil {
t.Error("Expected authorization error, got nil")
}
} |
|
Вариант 2: Использование интерфейса для HTTP-клиента
Более гибкий подход — создание интерфейса для HTTP-клиента:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type WeatherService struct {
client HTTPClient
apiKey string
baseURL string
}
func (ws *WeatherService) GetTemperature(city string) (int, error) {
url := fmt.Sprintf("%s/weather?city=%s&apikey=%s",
ws.baseURL, url.QueryEscape(city), ws.apiKey)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, fmt.Errorf("failed to create request: %w", err)
}
resp, err := ws.client.Do(req)
// Остальной код остается прежним
} |
|
Теперь можно создать мок HTTP-клиента:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| type MockHTTPClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
}
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.DoFunc(req)
}
func TestWeatherService_WithMockClient(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Проверяем URL запроса
if !strings.Contains(req.URL.String(), "apikey=test-key") {
return &http.Response{
StatusCode: http.StatusUnauthorized,
Body: io.NopCloser(strings.NewReader(`{"error":"unauthorized"}`)),
}, nil
}
// Готовим ответ в зависимости от города
city := req.URL.Query().Get("city")
temp := 25
if city == "Moscow" {
temp = 15
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"temp":%d}`, temp))),
}, nil
},
}
service := &WeatherService{
client: mockClient,
apiKey: "test-key",
baseURL: "https://api.example.com",
}
// Теперь можно тестировать так же, как в первом варианте
} |
|
Моки для баз данных
Тестирование кода, работающего с базами данных, еще одна распространенная задача. Создание тестовой базы данных не всегда возможно или практично, особенно в CI/CD пайплайнах. Рассмотрим типичный репозиторий для работы с пользователями:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| type User struct {
ID int
Username string
Email string
}
type UserRepository interface {
GetByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}
type SQLUserRepository struct {
db *sql.DB
}
func (r *SQLUserRepository) GetByID(id int) (*User, error) {
var user User
err := r.db.QueryRow("SELECT id, username, email FROM users WHERE id = $1", id).
Scan(&user.ID, &user.Username, &user.Email)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user with ID %d not found", id)
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
return &user, nil
}
// Реализации других методов опущены для краткости |
|
Для тестирования сервисов, использующих этот репозиторий, создадим мок:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| type MockUserRepository struct {
users map[int]*User
GetByIDFunc func(id int) (*User, error)
CreateFunc func(user *User) error
UpdateFunc func(user *User) error
DeleteFunc func(id int) error
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]*User),
GetByIDFunc: func(id int) (*User, error) {
user, exists := m.users[id]
if !exists {
return nil, fmt.Errorf("user with ID %d not found", id)
}
return user, nil
},
CreateFunc: func(user *User) error {
if user.ID == 0 {
maxID := 0
for id := range m.users {
if id > maxID {
maxID = id
}
}
user.ID = maxID + 1
}
if _, exists := m.users[user.ID]; exists {
return fmt.Errorf("user with ID %d already exists", user.ID)
}
m.users[user.ID] = user
return nil
},
// Реализации других методов опущены для краткости
}
}
func (m *MockUserRepository) GetByID(id int) (*User, error) {
return m.GetByIDFunc(id)
}
func (m *MockUserRepository) Create(user *User) error {
return m.CreateFunc(user)
}
// Реализации других методов интерфейса |
|
Теперь можно тестировать сервис пользователей:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| func TestUserService_ChangeEmail(t *testing.T) {
repo := NewMockUserRepository()
// Добавляем тестового пользователя
testUser := &User{ID: 1, Username: "testuser", Email: "old@example.com"}
repo.users[testUser.ID] = testUser
service := NewUserService(repo)
// Тестируем успешную смену email
err := service.ChangeEmail(1, "new@example.com")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if repo.users[1].Email != "new@example.com" {
t.Errorf("Email not changed. Expected 'new@example.com', got '%s'", repo.users[1].Email)
}
// Тестируем ошибку - пользователь не найден
err = service.ChangeEmail(999, "new@example.com")
if err == nil {
t.Error("Expected error for non-existent user, got nil")
}
} |
|
Работа с файловой системой
Тестирование кода, взаимодействующего с файловой системой, всегда было непростой задачей. Моки помогают изолировать этот аспект и сделать тесты надежнее. Создадим интерфейс для файловой системы:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| type FileSystem interface {
ReadFile(filename string) ([]byte, error)
WriteFile(filename string, data []byte, perm os.FileMode) error
Stat(name string) (os.FileInfo, error)
Remove(name string) error
}
// Реальная имплементация для продакшена
type RealFileSystem struct{}
func (fs RealFileSystem) ReadFile(filename string) ([]byte, error) {
return os.ReadFile(filename)
}
func (fs RealFileSystem) WriteFile(filename string, data []byte, perm os.FileMode) error {
return os.WriteFile(filename, data, perm)
}
func (fs RealFileSystem) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}
func (fs RealFileSystem) Remove(name string) error {
return os.Remove(name)
} |
|
Теперь можно создать мок для тестирования:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| type MockFileSystem struct {
files map[string][]byte
ReadFileFunc func(filename string) ([]byte, error)
WriteFileFunc func(filename string, data []byte, perm os.FileMode) error
StatFunc func(name string) (os.FileInfo, error)
RemoveFunc func(name string) error
}
func NewMockFileSystem() *MockFileSystem {
fs := &MockFileSystem{
files: make(map[string][]byte),
}
fs.ReadFileFunc = func(filename string) ([]byte, error) {
data, exists := fs.files[filename]
if !exists {
return nil, os.ErrNotExist
}
return data, nil
}
fs.WriteFileFunc = func(filename string, data []byte, perm os.FileMode) error {
fs.files[filename] = data
return nil
}
fs.StatFunc = func(name string) (os.FileInfo, error) {
_, exists := fs.files[name]
if !exists {
return nil, os.ErrNotExist
}
return &mockFileInfo{
name: filepath.Base(name),
size: int64(len(fs.files[name])),
}, nil
}
fs.RemoveFunc = func(name string) error {
if _, exists := fs.files[name]; !exists {
return os.ErrNotExist
}
delete(fs.files, name)
return nil
}
return fs
}
// Реализация методов интерфейса
func (m *MockFileSystem) ReadFile(filename string) ([]byte, error) {
return m.ReadFileFunc(filename)
}
// Остальные методы реализуются аналогично
// Вспомогательная структура для Stat
type mockFileInfo struct {
name string
size int64
}
// Реализация интерфейса os.FileInfo
func (m *mockFileInfo) Name() string { return m.name }
func (m *mockFileInfo) Size() int64 { return m.size }
func (m *mockFileInfo) Mode() os.FileMode { return 0644 }
func (m *mockFileInfo) ModTime() time.Time { return time.Now() }
func (m *mockFileInfo) IsDir() bool { return false }
func (m *mockFileInfo) Sys() interface{} { return nil } |
|
Теперь можно тестировать сервисы, работающие с файлами:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| func TestConfigService_Load(t *testing.T) {
fs := NewMockFileSystem()
// Создаем тестовый файл конфигурации
configData := []byte(`{"apiKey": "test-key", "timeout": 30}`)
fs.WriteFile("config.json", configData, 0644)
service := NewConfigService(fs)
config, err := service.Load("config.json")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if config.APIKey != "test-key" {
t.Errorf("Expected APIKey 'test-key', got '%s'", config.APIKey)
}
if config.Timeout != 30 {
t.Errorf("Expected Timeout 30, got %d", config.Timeout)
}
// Тестируем ошибку - файл не найден
_, err = service.Load("nonexistent.json")
if err == nil {
t.Error("Expected error for nonexistent file, got nil")
}
} |
|
Тестирование конкурентного кода с использованием моков
Тестирование многопоточных приложений - это отдельный вид головной боли. Горутины, каналы, мьютексы, условные переменные - весь этот инструментарий создает массу проблем при написании тестов. Непредсказуемое поведение, состояния гонки и тайминг-зависимые операции делают тесты хрупкими и недетерминированными. К счастью, моки могут существенно облегчить эту задачу. Допустим, у нас есть сервис для параллельной обработки задач:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| type Task struct {
ID string
Data string
}
type TaskProcessor interface {
Process(task Task) error
}
type WorkerPool struct {
processor TaskProcessor
workers int
taskChan chan Task
wg sync.WaitGroup
}
func NewWorkerPool(processor TaskProcessor, workers int) *WorkerPool {
return &WorkerPool{
processor: processor,
workers: workers,
taskChan: make(chan Task, workers*2),
}
}
func (p *WorkerPool) Start() {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for task := range p.taskChan {
if err := p.processor.Process(task); err != nil {
log.Printf("Error processing task %s: %v", task.ID, err)
}
}
}()
}
}
func (p *WorkerPool) Submit(task Task) {
p.taskChan <- task
}
func (p *WorkerPool) Stop() {
close(p.taskChan)
p.wg.Wait()
} |
|
Тестирование такого кода без моков - кошмарная задача. С помощью моков мы можем полностью контролировать выполнение:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| type MockTaskProcessor struct {
mu sync.Mutex
processedTasks []Task
processingTime time.Duration
shouldFail bool
}
func (m *MockTaskProcessor) Process(task Task) error {
if m.processingTime > 0 {
time.Sleep(m.processingTime)
}
m.mu.Lock()
defer m.mu.Unlock()
m.processedTasks = append(m.processedTasks, task)
if m.shouldFail {
return errors.New("processing failed")
}
return nil
}
func (m *MockTaskProcessor) GetProcessedTasks() []Task {
m.mu.Lock()
defer m.mu.Unlock()
result := make([]Task, len(m.processedTasks))
copy(result, m.processedTasks)
return result
}
func TestWorkerPool(t *testing.T) {
processor := &MockTaskProcessor{}
pool := NewWorkerPool(processor, 3)
pool.Start()
tasks := []Task{
{ID: "1", Data: "task1"},
{ID: "2", Data: "task2"},
{ID: "3", Data: "task3"},
}
for _, task := range tasks {
pool.Submit(task)
}
pool.Stop()
processedTasks := processor.GetProcessedTasks()
if len(processedTasks) != len(tasks) {
t.Errorf("Expected %d processed tasks, got %d", len(tasks), len(processedTasks))
}
// Проверка, что все задачи были обработаны
taskMap := make(map[string]bool)
for _, task := range processedTasks {
taskMap[task.ID] = true
}
for _, task := range tasks {
if !taskMap[task.ID] {
t.Errorf("Task %s was not processed", task.ID)
}
}
} |
|
Еще интереснее тестировать обработку ошибок в конкурентном коде:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| func TestWorkerPool_ErrorHandling(t *testing.T) {
processor := &MockTaskProcessor{shouldFail: true}
pool := NewWorkerPool(processor, 1)
// Перехватываем логи для проверки
var buf bytes.Buffer
log.SetOutput(&buf)
defer log.SetOutput(os.Stderr) // Восстанавливаем стандартный вывод
pool.Start()
pool.Submit(Task{ID: "error-task", Data: "will fail"})
pool.Stop()
logOutput := buf.String()
if !strings.Contains(logOutput, "Error processing task error-task") {
t.Error("Error handling didn't work as expected")
}
} |
|
Тестирование временных зависимостей и работа с таймерами через моки
Когда код зависит от времени или содержит таймеры, его тестирование становится особенно сложным. Ожидание реального истечения таймаутов делает тесты медленными, а зависимость от системного времени - нестабильными. И тут на сцену выходит интерфейс Clock:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| type Clock interface {
Now() time.Time
Sleep(d time.Duration)
After(d time.Duration) <-chan time.Time
NewTicker(d time.Duration) *time.Ticker
NewTimer(d time.Duration) *time.Timer
}
// Реальная имплементация для продакшена
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
func (RealClock) Sleep(d time.Duration) { time.Sleep(d) }
func (RealClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
func (RealClock) NewTicker(d time.Duration) *time.Ticker { return time.NewTicker(d) }
func (RealClock) NewTimer(d time.Duration) *time.Timer { return time.NewTimer(d) } |
|
А теперь создадим мок, который полностью контролирует время:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
| type MockClock struct {
currentTime time.Time
timers []*MockTimer
tickers []*MockTicker
mu sync.Mutex
}
type MockTimer struct {
c chan time.Time
fireTime time.Time
fired bool
stopped bool
clock *MockClock
}
type MockTicker struct {
c chan time.Time
duration time.Duration
lastTick time.Time
stopped bool
clock *MockClock
}
func NewMockClock(initialTime time.Time) *MockClock {
return &MockClock{
currentTime: initialTime,
timers: make([]*MockTimer, 0),
tickers: make([]*MockTicker, 0),
}
}
func (m *MockClock) Now() time.Time {
m.mu.Lock()
defer m.mu.Unlock()
return m.currentTime
}
func (m *MockClock) Sleep(d time.Duration) {
m.mu.Lock()
m.currentTime = m.currentTime.Add(d)
m.mu.Unlock()
}
func (m *MockClock) After(d time.Duration) <-chan time.Time {
timer := m.NewTimer(d)
return timer.C
}
func (m *MockClock) NewTimer(d time.Duration) *time.Timer {
m.mu.Lock()
defer m.mu.Unlock()
c := make(chan time.Time, 1)
fireTime := m.currentTime.Add(d)
mockTimer := &MockTimer{
c: c,
fireTime: fireTime,
clock: m,
}
m.timers = append(m.timers, mockTimer)
return &time.Timer{
C: c,
}
}
// Методы для управления временем в тестах
func (m *MockClock) Advance(d time.Duration) {
m.mu.Lock()
defer m.mu.Unlock()
newTime := m.currentTime.Add(d)
m.advanceTo(newTime)
}
func (m *MockClock) advanceTo(newTime time.Time) {
// Проверяем, не сработали ли таймеры
for _, timer := range m.timers {
if !timer.fired && !timer.stopped && newTime.After(timer.fireTime) {
timer.fired = true
timer.c <- timer.fireTime // Отправляем событие таймера
}
}
// Обрабатываем тикеры
for _, ticker := range m.tickers {
if ticker.stopped {
continue
}
tickCount := int(newTime.Sub(ticker.lastTick) / ticker.duration)
for i := 0; i < tickCount; i++ {
nextTick := ticker.lastTick.Add(ticker.duration)
if nextTick.After(m.currentTime) && nextTick.Before(newTime) || nextTick.Equal(newTime) {
select {
case ticker.c <- nextTick:
ticker.lastTick = nextTick
default:
// Канал заполнен, пропускаем тик
}
}
}
}
m.currentTime = newTime
} |
|
Теперь мы можем тестировать код с таймерами без реальных задержек:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| func TestRetryWithBackoff(t *testing.T) {
mockClock := NewMockClock(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
operation := func() error {
static callCount = 0
callCount++
if callCount < 3 {
return errors.New("temporary error")
}
return nil
}
retrier := NewRetrier(operation, mockClock, 5, 100*time.Millisecond)
// Запускаем в горутине, потому что будет ждать таймер
go func() {
retrier.Start()
}()
// Продвигаем время вперед
mockClock.Advance(50 * time.Millisecond) // Полпути до первой попытки
mockClock.Advance(50 * time.Millisecond) // Должна произойти первая попытка
mockClock.Advance(100 * time.Millisecond) // Должна произойти вторая попытка
mockClock.Advance(200 * time.Millisecond) // Должна произойти третья попытка (успешная)
// Проверяем, что операция выполнилась успешно
if !retrier.IsCompleted() {
t.Error("Retrier should be completed")
}
if retrier.Attempts() != 3 {
t.Errorf("Expected 3 attempts, got %d", retrier.Attempts())
}
} |
|
Такой подход позволяет тестировать временные зависимости детерминированно, быстро и без флакающих тестов. Особенно это актуально для систем с экспоненциальными задержками или сложной логикой повторных попыток.
Подводные камни и антипаттерны
Мокирование — мощный инструмент, но как и любая сила, требует осторожного обращения. Неправильное использование моков может привести к тестам, которые дают ложное чувство уверенности или становятся кошмаром сопровождения. Рассмотрим основные подводные камни и антипаттерны, чтобы избежать самых распространенных ошибок.
Избыточное мокирование
Возможно, самая распространенная ошибка — мокировать всё подряд. Этот подход часто называют "мокопсихозом". Он проявляется, когда разработчик создает моки для каждой зависимости, даже если она тривиальна или стабильна.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Антипаттерн: мокирование тривиальных функций
type StringFormatter interface {
Format(s string) string
}
// Это избыточно
type UppercaseFormatter struct{}
func (UppercaseFormatter) Format(s string) string {
return strings.ToUpper(s)
}
// Мок для тривиальной функциональности
type MockFormatter struct {
FormatFunc func(s string) string
}
func (m MockFormatter) Format(s string) string {
return m.FormatFunc(s)
} |
|
Проблема здесь в том, что настоящая имплементация проще и надежнее, чем мок. В случае с такими простыми функциями лучше использовать реальную реализацию даже в тестах. Чрезмерное мокирование также увеличивает сопряжение тестов с деталями реализации. Когда вы мокируете внутренние компоненты, вы закрепляете их поведение в тестах, что затрудняет рефакторинг. Вспомним слова легендарного Кента Бека: "Меньше моков — меньше проблем".
Хрупкие тесты
Тесты с моками могут стать чрезвычайно хрупкими, особенно когда они сильно связаны с деталями реализации. Небольшие изменения в коде могут сломать много тестов, даже если функциональность не изменилась.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Антипаттерн: тест, завязанный на последовательность вызовов
func TestUserService_Register(t *testing.T) {
mockRepo := &MockUserRepository{}
mockMailer := &MockMailer{}
// Устанавливаем жесткие ожидания порядка вызовов
mockRepo.On("FindByEmail", "user@example.com").Return(nil, nil).Once()
mockRepo.On("Create", mock.Anything).Return(nil).Once()
mockMailer.On("SendWelcomeEmail", "user@example.com").Return(nil).Once()
service := NewUserService(mockRepo, mockMailer)
err := service.Register("user@example.com", "password")
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
mockMailer.AssertExpectations(t)
} |
|
Если имплементация изменится, например, сначала создавать пользователя, а потом проверять дубликаты email, тест сломается, хотя функциональность может остаться корректной.
Вместо этого лучше фокусироваться на тестировании конечного результата:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Лучший подход: проверяем результат, а не порядок вызовов
func TestUserService_Register(t *testing.T) {
mockRepo := &MockUserRepository{
users: make(map[string]*User),
}
mockMailer := &MockMailer{}
service := NewUserService(mockRepo, mockMailer)
err := service.Register("user@example.com", "password")
assert.NoError(t, err)
// Проверяем только то, что действительно важно
user, found := mockRepo.FindUserByEmail("user@example.com")
assert.True(t, found)
assert.Equal(t, "user@example.com", user.Email)
assert.True(t, mockMailer.WelcomeEmailSent("user@example.com"))
} |
|
Балансирование между скоростью и надежностью
Использование моков делает тесты быстрее, но может уменьшить их надежность. Тесты, полностью полагающиеся на моки, могут пропустить ошибки интеграции между компонентами. Оптимальный подход — пирамида тестирования с комбинацией:- Модульных тестов с моками для быстрой проверки бизнес-логики.
- Интеграционных тестов для проверки взаимодействия компонентов.
- Небольшого количества end-to-end тестов для критических путей.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Пример балансировки: модульный тест с моком
func TestProcessItem_Unit(t *testing.T) {
mockStorage := &MockStorage{}
processor := NewItemProcessor(mockStorage)
// ... тест с моком
}
// Интеграционный тест с реальным хранилищем
func TestProcessItem_Integration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
tmpDir := t.TempDir()
realStorage := NewFileStorage(tmpDir)
processor := NewItemProcessor(realStorage)
// ... интеграционный тест
} |
|
Флаг -short позволяет пропускать интеграционные тесты при быстрых прогонах.
Несинхронизированные моки
Еще одна распространенная ошибка — забывать о потокобезопасности при создании моков для многопоточного кода. Когда несколько горутин обращаются к одному моку, может возникнуть состояние гонки.
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Антипаттерн: неконкурентный мок
type UnsafeMockCache struct {
data map[string]interface{} // Требует синхронизации при конкурентном доступе
}
// Лучший подход: потокобезопасный мок
type SafeMockCache struct {
mu sync.RWMutex
data map[string]interface{}
}
func (c *SafeMockCache) Get(key string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
} |
|
Моки должны следовать тем же правилам конкурентности, что и реальные объекты, иначе тесты могут выдавать ложные результаты или падать недетерминированно. Использование моков — как хождение по канату: правильный баланс и техника дают впечатляющие результаты, но малейшая ошибка может привести к падению. Зная эти антипаттерны и активно их избегая, вы сможете создавать тесты, которые дают реальную уверенность в коде, а не просто иллюзию покрытия.
Метрики и мониторинг качества mock-тестов
Написать тесты с моками — только половина дела. Без понимания того, насколько эти тесты эффективны, можно оказаться в ситуации ложного чувства безопасности. Как же измерить качество тестов с моками и настроить мониторинг их эффективности?
Ключевые метрики эффективности моков
Первая и самая очевидная метрика — покрытие кода. Go предоставляет мощный инструмент для анализа покрытия:
| Bash | 1
2
| go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html |
|
Однако для тестов с моками стандартного покрытия недостаточно. Важно отслеживать более глубокие метрики:
1. Покрытие ветвлений — проверяют ли тесты все возможные пути в коде, включая обработку ошибок.
2. Покрытие граничных условий — тестируются ли экстремальные случаи, пустые входные данные, максимальные значения.
3. Мутационное тестирование — выживают ли тесты при внесении мутаций в код.
Для измерения мутационного покрытия в Go можно использовать инструменты вроде go-mutesting:
| Bash | 1
2
| go get github.com/zimmski/go-mutesting/...
go-mutesting ./... |
|
Инструмент вносит небольшие изменения в код (мутации) и проверяет, обнаруживают ли тесты эти изменения. Если тесты продолжают проходить после мутации, значит, они недостаточно строги.
Автоматизация проверки моков
Недостаточно просто покрыть код тестами с моками — важно, чтобы моки адекватно отражали реальное поведение систем. Для этого полезны:
1. Контрактные тесты — проверяют, что моки имплементируют интерфейс так же, как реальные объекты:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| func TestMockBehaviorMatchesReal(t *testing.T) {
realClient := NewRealS3Client()
mockClient := NewMockS3Client()
// Одинаковые входные данные
input := &s3.CreateBucketInput{Bucket: aws.String("test-bucket")}
// Проверяем, что в нормальном случае оба клиента ведут себя похоже
realOutput, realErr := realClient.CreateBucket(context.TODO(), input)
mockOutput, mockErr := mockClient.CreateBucket(context.TODO(), input)
// Сравниваем результаты, учитывая только важные аспекты
assert.Equal(t, realErr != nil, mockErr != nil)
// и т.д.
} |
|
2. Фаззинг-тестирование моков — проверяет поведение моков на случайных входных данных.
Визуализация и мониторинг
Для мониторинга качества тестов в CI/CD системах можно настроить:
1. Тренды покрытия — отслеживание изменений покрытия со временем.
2. Время выполнения тестов — отлавливание регрессий производительности.
3. Подсчет использования моков — выявление избыточного мокирования.
Эти метрики можно интегрировать в системы мониторинга через простые скрипты:
| Go | 1
2
3
4
5
6
| type TestMetrics struct {
CoveragePercent float64
TestRunTimeSeconds float64
MockUsageCount int
FlakynessScore float64
} |
|
Отслеживание таких метрик помогает выявлять проблемы на ранних стадиях и поддерживать высокое качество тестов в проекте.
Баланс между мокингом и интеграционными тестами
Во-первых, используйте моки для изоляции бизнес-логики и тестирования сложных сценариев с ошибками, которые трудно воспроизвести иначе. Внешние зависимости, особенно облачные сервисы, платежные системы и нестабильные API — идеальные кандидаты для мокирования.
Во-вторых, дополняйте модульные тесты с моками хотя бы базовыми интеграционными тестами для критически важных путей. Это помогает убедиться, что ваша система действительно взаимодействует с реальными компонентами как ожидается.
В-третьих, используйте контрактные тесты для проверки соответствия поведения моков реальным компонентам. Без этого вы рискуете написать тесты, которые проходят в вакууме, но падают при столкновении с реальным миром.
Наконец, помните: качество важнее количества. Меньше тестов, которые действительно проверяют важную функциональность, лучше чем множество поверхностных, сложных в поддержке тестов.
Полный листинг приложения с демонстрацией всех техник
Для демонстрации различных подходов к мокированию в Go создадим небольшое приложение, работающее с AWS S3 и включающее асинхронную обработку данных, взаимодействие с файловой системой и HTTP-запросы. Приложение будет включать полноценные юнит-тесты с использованием моков. Проект имеет следующую структуру:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ├── go.mod
├── go.sum
├── main.go
├── interfaces.go
├── s3client.go
├── filestorage.go
├── processor.go
├── mocks/
│ ├── s3_mock.go
│ ├── fs_mock.go
│ └── time_mock.go
└── test/
├── s3client_test.go
├── filestorage_test.go
└── integration_test.go |
|
Начнем с определения основных интерфейсов в файле interfaces.go:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| package s3processor
import (
"context"
"io"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// S3Client интерфейс для взаимодействия с AWS S3
type S3Client interface {
CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error)
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error)
}
// FileSystem интерфейс для работы с файловой системой
type FileSystem interface {
ReadFile(filename string) ([]byte, error)
WriteFile(filename string, data []byte, perm os.FileMode) error
Stat(name string) (os.FileInfo, error)
Remove(name string) error
}
// Clock интерфейс для работы со временем
type Clock interface {
Now() time.Time
Sleep(d time.Duration)
After(d time.Duration) <-chan time.Time
}
// HTTPClient интерфейс для HTTP-запросов
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
} |
|
Основная логика для работы с S3 в файле s3client.go:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| package s3processor
import (
"context"
"fmt"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// BucketManager управляет операциями с S3 корзинами
type BucketManager struct {
client S3Client
clock Clock
fs FileSystem
}
// NewBucketManager создает новый экземпляр BucketManager
func NewBucketManager(client S3Client, clock Clock, fs FileSystem) *BucketManager {
return &BucketManager{
client: client,
clock: clock,
fs: fs,
}
}
// CreateBucket создает новую корзину S3
func (bm *BucketManager) CreateBucket(name, region string) error {
if name == "" {
return fmt.Errorf("bucket name cannot be empty")
}
if _, err := bm.client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
Bucket: aws.String(name),
CreateBucketConfiguration: &types.CreateBucketConfiguration{
LocationConstraint: types.BucketLocationConstraint(region),
},
}); err != nil {
return fmt.Errorf("failed to create bucket: %w", err)
}
// Ждем, пока корзина станет доступной
deadline := bm.clock.Now().Add(30 * time.Second)
for bm.clock.Now().Before(deadline) {
_, err := bm.client.HeadBucket(context.TODO(), &s3.HeadBucketInput{
Bucket: aws.String(name),
})
if err == nil {
return nil // Корзина создана и доступна
}
bm.clock.Sleep(time.Second)
}
return fmt.Errorf("timeout waiting for bucket %s to become available", name)
}
// UploadFile загружает файл в S3
func (bm *BucketManager) UploadFile(bucket, key, localPath string) error {
data, err := bm.fs.ReadFile(localPath)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
_, err = bm.client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ContentType: aws.String("application/octet-stream"),
})
if err != nil {
return fmt.Errorf("failed to upload file: %w", err)
}
return nil
} |
|
Теперь создадим мок для интерфейса S3Client в файле mocks/s3_mock.go:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| package mocks
import (
"context"
"sync"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// MockS3Client мок для S3Client
type MockS3Client struct {
mu sync.Mutex
CreateBucketFunc func(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error)
PutObjectFunc func(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
GetObjectFunc func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
HeadBucketFunc func(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error)
createBucketCalls int
putObjectCalls int
getObjectCalls int
headBucketCalls int
buckets map[string]bool
objects map[string][]byte
}
// NewMockS3Client создает новый мок для S3Client
func NewMockS3Client() *MockS3Client {
mock := &MockS3Client{
buckets: make(map[string]bool),
objects: make(map[string][]byte),
}
// Настройка поведения по умолчанию
mock.CreateBucketFunc = mock.defaultCreateBucket
mock.HeadBucketFunc = mock.defaultHeadBucket
mock.PutObjectFunc = mock.defaultPutObject
mock.GetObjectFunc = mock.defaultGetObject
return mock
}
// Реализация методов интерфейса
func (m *MockS3Client) CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.createBucketCalls++
return m.CreateBucketFunc(ctx, params, optFns...)
}
// Остальные методы и реализация по умолчанию опущены для краткости... |
|
Пример юнит-теста с использованием моков в файле test/s3client_test.go:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
| package test
import (
"errors"
"testing"
"time"
"example.com/s3processor"
"example.com/s3processor/mocks"
)
func TestCreateBucket(t *testing.T) {
tests := []struct {
name string
bucketName string
region string
mockSetup func(*mocks.MockS3Client, *mocks.MockClock)
expectError bool
expectedCalls int
}{
{
name: "successful creation",
bucketName: "test-bucket",
region: "us-west-2",
mockSetup: func(s3Mock *mocks.MockS3Client, clockMock *mocks.MockClock) {
// По умолчанию моки настроены на успешное выполнение
},
expectError: false,
expectedCalls: 1,
},
{
name: "empty bucket name",
bucketName: "",
region: "us-west-2",
mockSetup: func(s3Mock *mocks.MockS3Client, clockMock *mocks.MockClock) {},
expectError: true,
expectedCalls: 0,
},
{
name: "creation error",
bucketName: "test-bucket",
region: "us-west-2",
mockSetup: func(s3Mock *mocks.MockS3Client, clockMock *mocks.MockClock) {
s3Mock.CreateBucketFunc = func(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
return nil, errors.New("access denied")
}
},
expectError: true,
expectedCalls: 1,
},
{
name: "timeout waiting for bucket",
bucketName: "test-bucket",
region: "us-west-2",
mockSetup: func(s3Mock *mocks.MockS3Client, clockMock *mocks.MockClock) {
// CreateBucket успешен, но HeadBucket всегда возвращает ошибку
s3Mock.HeadBucketFunc = func(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error) {
return nil, errors.New("bucket not found")
}
// Ускоряем время для теста
initialTime := time.Now()
clockMock.NowFunc = func() time.Time {
return initialTime.Add(31 * time.Second)
}
},
expectError: true,
expectedCalls: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s3Mock := mocks.NewMockS3Client()
clockMock := mocks.NewMockClock()
fsMock := mocks.NewMockFileSystem()
tt.mockSetup(s3Mock, clockMock)
manager := s3processor.NewBucketManager(s3Mock, clockMock, fsMock)
err := manager.CreateBucket(tt.bucketName, tt.region)
if (err != nil) != tt.expectError {
t.Errorf("CreateBucket() error = %v, expectError %v", err, tt.expectError)
}
if s3Mock.GetCreateBucketCalls() != tt.expectedCalls {
t.Errorf("Expected %d calls to CreateBucket, got %d", tt.expectedCalls, s3Mock.GetCreateBucketCalls())
}
})
}
} |
|
Это основные компоненты приложения, демонстрирующие различные техники мокирования в Go. Полный код слишком большой для одной главы, но приведенные листинги показывают ключевые концепции: использование интерфейсов для зависимостей, гибкие моки с возможностью настройки поведения, и таблично-управляемые тесты для различных сценариев.
Для запуска приложения и тестов выполните:
| Bash | 1
2
3
4
5
6
7
8
| # Установка зависимостей
go mod tidy
# Запуск тестов
go test ./test/... -v
# Запуск приложения
go run main.go |
|
Эта структура проекта позволяет легко расширять функциональность и добавлять новые тесты, сохраняя чистую архитектуру с четким разделением ответственности между компонентами.
Тестирование, моки Здравствуйте. Есть функция:
func Test(iRepo RepositoryInterface) {
iRepo.Get()
iRepo.Set()
}... Golang тестирование сервера с помощью утилиты Добрый день, спасибо за уделенное время.
Мне нужно протестировать ендпоинты на сервере. Я помню... Unit -тестирование или автоматизированное тестирование Доброго времени суток.
Я программирую «для себя» второй год, на выходе получаются разного рода... Возможно ли как-то перевести unit C++ в unit delphi Возможно ли как-то перевести unit C++ в unit delphi? очень нужно сделать программу способом отдельных модулей (unit mod1, unit mod2.) const
nmax=20;
type
tAr=array of integer;
procType=procedure(var ar: tAr; n: byte);
var... Warning: Cannot modify header information - headers already sent by (output started at Z:\home\unit.su\WWW\config.php:1) in Z:\home\unit.su\WWW\aut.ph подскажите что за ошибка в коде?
Warning: Cannot modify header information - headers already... Unit Tests для проекта с Unit Of Work Привет.
Вынужден снова обратиться за помощью.
Пишу блог.
Архитектура такова, что есть... unit-тестирование функции Заполнить массив n´m нулями и единицами «цепочкой квадратов».
Размер квадрата задается.
Тест: K =... Unit тестирование функции sin x/x на C Здравствуйте, подскажите какие сделать unit тесты для функции sin x/x С++ Unit Test (модульное тестирование) Вопроса по сути два.
1. что используете для модульного тестирования и почему отдаете этому... Unit тестирование в Visual Studio 2012 как написать тест для функции перевода целого 10тичного числа в указанную систему счислению?
... Unit - тестирование Есть ли на C03++ стандарте что-то, помогающее в этом? И как это самое использовать?
Ну или...
|