From 983a26ce771aee0a9dc8e51f58502303d171dab5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Feb 2026 18:15:31 +0500 Subject: [PATCH 1/2] parser test --- internal/parser/parser.go | 7 +- internal/parser/parser_test.go | 220 +++++++++++++++++++++++++++++++-- 2 files changed, 212 insertions(+), 15 deletions(-) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 3b2d5a1..720b09f 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -62,8 +62,11 @@ func ParseStartupArgs(args []string) error { return commands.Connect(endpoint) } - // Если передан аргумент connect, выполняем сразу - if len(args) > 2 && args[1] == "connect" { + // Handle 'connect' command + if len(args) >= 2 && args[1] == "connect" { + if len(args) < 3 { // 'connect' command requires an endpoint + return fmt.Errorf("usage: connect ") + } return commands.Connect(args[2]) } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index db80cc9..84f7063 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1,23 +1,217 @@ package parser -import "testing" +import ( -func TestExample(t *testing.T) { - sum := 2 + 2 - if sum != 4 { - t.Errorf("Expected 4, got %d", sum) + "testing" + + +) + +// TestExecute проверяет функцию Execute для различных входных данных. +// Эта функция является основной точкой входа для обработки команд пользователя. +// +// Основные аспекты тестирования: +// - Обработка пустого ввода. +// - Корректная обработка известных команд (`help`, `exit`, `quit`). +// - Возврат ожидаемой ошибки для неизвестных команд. +// - Возврат ошибки при неверном использовании команды `connect` (например, без аргументов). +// +// Ограничения: +// Тестирование команд, которые инициируют фактические сетевые подключения (`connect `, `disconnect`), +// требует заглушек (mocks) для функций `commands.Connect` и `commands.Disconnect`. +// Без модификации пакета `commands` (например, чтобы сделать `Connect` и `Disconnect` переменными) +// невозможно изолировать эти тесты от реальных сетевых операций. +// Поэтому такие сценарии здесь не тестируются. +func TestExecute(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + wantExit bool + errMsg string + }{ + { + name: "Пустой ввод не должен вызывать ошибок", + input: "", + wantErr: false, + }, + { + name: "Команда help не должна вызывать ошибок", + input: "help", + wantErr: false, // PrintHelp просто печатает в консоль, не возвращает ошибок + }, + { + name: "Команда exit должна вернуть ошибку 'exit'", + input: "exit", + wantErr: true, + wantExit: true, + }, + { + name: "Команда quit должна вернуть ошибку 'exit'", + input: "quit", + wantErr: true, + wantExit: true, + }, + { + name: "Неизвестная команда должна вернуть соответствующую ошибку", + input: "foobar", + wantErr: true, + errMsg: "unknown command: foobar. Type 'help' for available commands", + }, + { + name: "Команда connect без аргументов должна вернуть ошибку использования", + input: "connect", + wantErr: true, + errMsg: "usage: connect ", + }, + // Сценарии `connect ` и `disconnect` здесь не тестируются, см. Ограничения выше. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Execute(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("Execute() ожидалась ошибка, получено nil") + } else if tt.wantExit && err.Error() != "exit" { + t.Errorf("Execute() получено неожиданное сообщение об ошибке выхода = %v", err.Error()) + } else if !tt.wantExit && err.Error() != tt.errMsg { + t.Errorf("Execute() получено неожиданное сообщение об ошибке = %v, ожидалось %v", err.Error(), tt.errMsg) + } + } else { + if err != nil { + t.Errorf("Execute() получена непредвиденная ошибка = %v", err) + } + } + }) } } -func TestExample2(t *testing.T) { - sum := 2 + 3 - if sum != 5 { - t.Errorf("Expected 4, got %d", sum) +// TestParseStartupArgs проверяет функцию ParseStartupArgs для различных аргументов запуска. +// Эта функция обрабатывает аргументы командной строки, переданные при старте приложения. +// +// Основные аспекты тестирования: +// - Обработка запуска без функциональных аргументов. +// - Обработка неполных аргументов для команды `connect`. +// +// Ограничения: +// Сценарии, которые приводят к фактическому вызову `commands.Connect` +// (например, `opcli 127.0.0.1` или `opcli connect opc.tcp://...`), не тестируются напрямую. +// Это связано с тем, что `commands.Connect` является функцией, а не переменной, +// и не может быть легко переопределена для целей тестирования без модификации пакета `commands`. +// Полноценное тестирование этих сценариев потребует рефакторинга для обеспечения тестопригодности (например, внедрение зависимостей через интерфейсы). +func TestParseStartupArgs(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + errMsg string + }{ + { + name: "Без аргументов после имени программы не должно быть ошибок", + args: []string{"opcli"}, + wantErr: false, + }, + { + name: "Недостаточно аргументов для 'connect' должно вернуть ошибку использования", + args: []string{"opcli", "connect"}, + wantErr: true, + errMsg: "usage: connect ", // Эта ошибка происходит из handleConnect, вызываемого ParseStartupArgs + }, + // Сценарии с действительным IP или точкой подключения для `connect` здесь не тестируются, см. Ограничения выше. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // В этом тесте мы не переопределяем commands.Connect, так как это невозможно + // без изменения пакета commands. Поэтому тестируются только те сценарии, + // которые не приводят к фактическому вызову commands.Connect или + // возвращают ошибку раньше. + + err := ParseStartupArgs(tt.args) + + if tt.wantErr { + if err == nil { + t.Errorf("ParseStartupArgs() ожидалась ошибка, получено nil") + } else if err.Error() != tt.errMsg { + t.Errorf("ParseStartupArgs() получено неожиданное сообщение об ошибке = %v, ожидалось %v", err.Error(), tt.errMsg) + } + } else { + if err != nil { + t.Errorf("ParseStartupArgs() получена непредвиденная ошибка = %v", err) + } + } + }) } } -func TestExample3(t *testing.T) { - sum := 3 + 3 - if sum != 6 { - t.Errorf("Expected 4, got %d", sum) + +// TestIsIPv4 проверяет вспомогательную функцию isIPv4. +// Эта функция является неэкспортированной, но её логика достаточно важна и самодостаточна, +// чтобы быть протестированной напрямую. +// +// Основные аспекты тестирования: +// - Корректное определение действительных IPv4-адресов. +// - Корректное определение недействительных форматов, включая IPv6, неполные адреса, +// адреса с некорректными значениями октетов и текстовые строки. +func TestIsIPv4(t *testing.T) { + tests := []struct { + name string + ip string + want bool + }{ + { + name: "Действительный IPv4-адрес (loopback)", + ip: "127.0.0.1", + want: true, + }, + { + name: "Действительный IPv4-адрес (приватный)", + ip: "192.168.1.1", + want: true, + }, + { + name: "Действительный IPv4-адрес (публичный)", + ip: "8.8.8.8", + want: true, + }, + { + name: "Недействительный IPv4-адрес (слишком много сегментов)", + ip: "1.2.3.4.5", + want: false, + }, + { + name: "Недействительный IPv4-адрес (текст)", + ip: "localhost", + want: false, + }, + { + name: "Недействительный IPv4-адрес (пустая строка)", + ip: "", + want: false, + }, + { + name: "Недействительный IPv4-адрес (частично действительный)", + ip: "192.168.1", + want: false, + }, + { + name: "Недействительный IPv4-адрес (значение октета вне диапазона)", + ip: "256.0.0.1", + want: false, + }, + { + name: "Действительный IPv6-адрес (должен быть ложным для isIPv4)", + ip: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isIPv4(tt.ip); got != tt.want { + t.Errorf("isIPv4(%q) = %v, ожидалось %v", tt.ip, got, tt.want) + } + }) } } From c25e585b1344b8f79cd252c7bf0c8845e0496bf7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Feb 2026 18:28:46 +0500 Subject: [PATCH 2/2] connect & disconnect tests --- internal/parser/parser.go | 11 +- internal/parser/parser_test.go | 240 ++++++++++++++++++++++++++++----- 2 files changed, 214 insertions(+), 37 deletions(-) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 720b09f..8670bb2 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -8,6 +8,9 @@ import ( "github.com/alexfrick92/opcli/internal/commands" ) +var connectCommand = commands.Connect +var disconnectCommand = commands.Disconnect + // Execute выполняет команду из пользовательского ввода func Execute(input string) error { parts := strings.Fields(input) @@ -47,11 +50,11 @@ func handleConnect(args []string) error { if len(args) == 0 { return fmt.Errorf("usage: connect ") } - return commands.Connect(args[0]) + return connectCommand(args[0]) } func handleDisconnect() error { - return commands.Disconnect() + return disconnectCommand() } // ParseStartupArgs обрабатывает аргументы командной строки при запуске @@ -59,7 +62,7 @@ func ParseStartupArgs(args []string) error { // Если передан IP-адрес, подключаемся с портом по умолчанию if len(args) == 2 && isIPv4(args[1]) { endpoint := fmt.Sprintf("opc.tcp://%s:4840", args[1]) - return commands.Connect(endpoint) + return connectCommand(endpoint) } // Handle 'connect' command @@ -67,7 +70,7 @@ func ParseStartupArgs(args []string) error { if len(args) < 3 { // 'connect' command requires an endpoint return fmt.Errorf("usage: connect ") } - return commands.Connect(args[2]) + return connectCommand(args[2]) } return nil diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 84f7063..38b7789 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1,12 +1,41 @@ package parser import ( - + "fmt" "testing" +) - +// Mock variables for connectCommand and disconnectCommand +var ( + mockConnectCalled bool + mockConnectEndpoint string + mockConnectError error + mockDisconnectCalled bool + mockDisconnectError error ) +// mockConnect is a mock implementation for connectCommand +func mockConnect(endpoint string) error { + mockConnectCalled = true + mockConnectEndpoint = endpoint + return mockConnectError +} + +// mockDisconnect is a mock implementation for disconnectCommand +func mockDisconnect() error { + mockDisconnectCalled = true + return mockDisconnectError +} + +// resetMocks resets the state of all mock variables +func resetMocks() { + mockConnectCalled = false + mockConnectEndpoint = "" + mockConnectError = nil + mockDisconnectCalled = false + mockDisconnectError = nil +} + // TestExecute проверяет функцию Execute для различных входных данных. // Эта функция является основной точкой входа для обработки команд пользователя. // @@ -15,20 +44,24 @@ import ( // - Корректная обработка известных команд (`help`, `exit`, `quit`). // - Возврат ожидаемой ошибки для неизвестных команд. // - Возврат ошибки при неверном использовании команды `connect` (например, без аргументов). -// -// Ограничения: -// Тестирование команд, которые инициируют фактические сетевые подключения (`connect `, `disconnect`), -// требует заглушек (mocks) для функций `commands.Connect` и `commands.Disconnect`. -// Без модификации пакета `commands` (например, чтобы сделать `Connect` и `Disconnect` переменными) -// невозможно изолировать эти тесты от реальных сетевых операций. -// Поэтому такие сценарии здесь не тестируются. +// - Корректная обработка команд `connect` и `disconnect` с использованием заглушек. func TestExecute(t *testing.T) { + // Сохраняем оригинальные функции и восстанавливаем их после выполнения всех тестов + oldConnectCommand := connectCommand + oldDisconnectCommand := disconnectCommand + defer func() { + connectCommand = oldConnectCommand + disconnectCommand = oldDisconnectCommand + }() + tests := []struct { - name string - input string - wantErr bool - wantExit bool - errMsg string + name string + input string + setupMocks func() + checkMocks func(*testing.T) + wantErr bool + wantExit bool + errMsg string }{ { name: "Пустой ввод не должен вызывать ошибок", @@ -38,7 +71,7 @@ func TestExecute(t *testing.T) { { name: "Команда help не должна вызывать ошибок", input: "help", - wantErr: false, // PrintHelp просто печатает в консоль, не возвращает ошибок + wantErr: false, }, { name: "Команда exit должна вернуть ошибку 'exit'", @@ -64,11 +97,79 @@ func TestExecute(t *testing.T) { wantErr: true, errMsg: "usage: connect ", }, - // Сценарии `connect ` и `disconnect` здесь не тестируются, см. Ограничения выше. + { + name: "Команда connect с эндпоинтом должна вызвать mockConnect", + input: "connect opc.tcp://localhost:4840", + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = nil // Устанавливаем успешное выполнение + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + if mockConnectEndpoint != "opc.tcp://localhost:4840" { + t.Errorf("mockConnect вызван с неверным эндпоинтом: %s", mockConnectEndpoint) + } + }, + wantErr: false, + }, + { + name: "Команда connect с эндпоинтом должна вернуть ошибку от mockConnect", + input: "connect opc.tcp://remote:4840", + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = fmt.Errorf("mock connect failed") + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + if mockConnectEndpoint != "opc.tcp://remote:4840" { + t.Errorf("mockConnect вызван с неверным эндпоинтом: %s", mockConnectEndpoint) + } + }, + wantErr: true, + errMsg: "mock connect failed", + }, + { + name: "Команда disconnect должна вызвать mockDisconnect", + input: "disconnect", + setupMocks: func() { + disconnectCommand = mockDisconnect + mockDisconnectError = nil // Устанавливаем успешное выполнение + }, + checkMocks: func(t *testing.T) { + if !mockDisconnectCalled { + t.Errorf("mockDisconnect не был вызван") + } + }, + wantErr: false, + }, + { + name: "Команда disconnect должна вернуть ошибку от mockDisconnect", + input: "disconnect", + setupMocks: func() { + disconnectCommand = mockDisconnect + mockDisconnectError = fmt.Errorf("mock disconnect failed") + }, + checkMocks: func(t *testing.T) { + if !mockDisconnectCalled { + t.Errorf("mockDisconnect не был вызван") + } + }, + wantErr: true, + errMsg: "mock disconnect failed", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + resetMocks() // Сброс моков перед каждым подтестом + if tt.setupMocks != nil { + tt.setupMocks() + } + err := Execute(tt.input) if tt.wantErr { @@ -84,6 +185,10 @@ func TestExecute(t *testing.T) { t.Errorf("Execute() получена непредвиденная ошибка = %v", err) } } + + if tt.checkMocks != nil { + tt.checkMocks(t) + } }) } } @@ -94,19 +199,21 @@ func TestExecute(t *testing.T) { // Основные аспекты тестирования: // - Обработка запуска без функциональных аргументов. // - Обработка неполных аргументов для команды `connect`. -// -// Ограничения: -// Сценарии, которые приводят к фактическому вызову `commands.Connect` -// (например, `opcli 127.0.0.1` или `opcli connect opc.tcp://...`), не тестируются напрямую. -// Это связано с тем, что `commands.Connect` является функцией, а не переменной, -// и не может быть легко переопределена для целей тестирования без модификации пакета `commands`. -// Полноценное тестирование этих сценариев потребует рефакторинга для обеспечения тестопригодности (например, внедрение зависимостей через интерфейсы). +// - Корректная обработка аргументов для `connect` и IP-адресов с использованием заглушек. func TestParseStartupArgs(t *testing.T) { + // Сохраняем оригинальные функции и восстанавливаем их после выполнения всех тестов + oldConnectCommand := connectCommand + defer func() { + connectCommand = oldConnectCommand + }() + tests := []struct { - name string - args []string - wantErr bool - errMsg string + name string + args []string + setupMocks func() + checkMocks func(*testing.T) + wantErr bool + errMsg string }{ { name: "Без аргументов после имени программы не должно быть ошибок", @@ -117,17 +224,80 @@ func TestParseStartupArgs(t *testing.T) { name: "Недостаточно аргументов для 'connect' должно вернуть ошибку использования", args: []string{"opcli", "connect"}, wantErr: true, - errMsg: "usage: connect ", // Эта ошибка происходит из handleConnect, вызываемого ParseStartupArgs + errMsg: "usage: connect ", + }, + { + name: "Запуск с IP-адресом должен вызвать mockConnect", + args: []string{"opcli", "127.0.0.1"}, + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = nil + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + if mockConnectEndpoint != "opc.tcp://127.0.0.1:4840" { + t.Errorf("mockConnect вызван с неверным эндпоинтом: %s", mockConnectEndpoint) + } + }, + wantErr: false, + }, + { + name: "Запуск с IP-адресом должен вернуть ошибку от mockConnect", + args: []string{"opcli", "127.0.0.1"}, + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = fmt.Errorf("mock startup connect failed") + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + }, + wantErr: true, + errMsg: "mock startup connect failed", + }, + { + name: "Запуск с 'connect ' должен вызвать mockConnect", + args: []string{"opcli", "connect", "opc.tcp://localhost:4840"}, + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = nil + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + if mockConnectEndpoint != "opc.tcp://localhost:4840" { + t.Errorf("mockConnect вызван с неверным эндпоинтом: %s", mockConnectEndpoint) + } + }, + wantErr: false, + }, + { + name: "Запуск с 'connect ' должен вернуть ошибку от mockConnect", + args: []string{"opcli", "connect", "opc.tcp://invalid:4840"}, + setupMocks: func() { + connectCommand = mockConnect + mockConnectError = fmt.Errorf("mock startup connect with endpoint failed") + }, + checkMocks: func(t *testing.T) { + if !mockConnectCalled { + t.Errorf("mockConnect не был вызван") + } + }, + wantErr: true, + errMsg: "mock startup connect with endpoint failed", }, - // Сценарии с действительным IP или точкой подключения для `connect` здесь не тестируются, см. Ограничения выше. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // В этом тесте мы не переопределяем commands.Connect, так как это невозможно - // без изменения пакета commands. Поэтому тестируются только те сценарии, - // которые не приводят к фактическому вызову commands.Connect или - // возвращают ошибку раньше. + resetMocks() // Сброс моков перед каждым подтестом + if tt.setupMocks != nil { + tt.setupMocks() + } err := ParseStartupArgs(tt.args) @@ -142,6 +312,10 @@ func TestParseStartupArgs(t *testing.T) { t.Errorf("ParseStartupArgs() получена непредвиденная ошибка = %v", err) } } + + if tt.checkMocks != nil { + tt.checkMocks(t) + } }) } }