Unit testing 在Go中对SSH客户机进行单元测试
我在Go中编写了一个SSH客户端,我想编写一些测试。问题是,我以前从未真正编写过正确的单元测试,而且大多数教程似乎都专注于为添加两个数字的函数编写测试或其他一些小问题。我读过关于模拟、使用接口和其他技术的书,但我在应用它们时遇到了困难。此外,我的客户机将同时使用,以允许一次快速配置多个设备。不确定这是否会改变我编写测试的方式,或者是否会添加额外的测试。感谢您的帮助 这是我的密码。基本上,Unit testing 在Go中对SSH客户机进行单元测试,unit-testing,testing,go,ssh,network-programming,Unit Testing,Testing,Go,Ssh,Network Programming,我在Go中编写了一个SSH客户端,我想编写一些测试。问题是,我以前从未真正编写过正确的单元测试,而且大多数教程似乎都专注于为添加两个数字的函数编写测试或其他一些小问题。我读过关于模拟、使用接口和其他技术的书,但我在应用它们时遇到了困难。此外,我的客户机将同时使用,以允许一次快速配置多个设备。不确定这是否会改变我编写测试的方式,或者是否会添加额外的测试。感谢您的帮助 这是我的密码。基本上,设备有4个主要功能:连接、发送、输出/错误和关闭,分别用于连接到设备、向其发送一组配置命令、捕获会话输出和关闭
设备
有4个主要功能:连接
、发送
、输出
/错误
和关闭
,分别用于连接到设备、向其发送一组配置命令、捕获会话输出和关闭客户端
package device
import (
"bufio"
"fmt"
"golang.org/x/crypto/ssh"
"io"
"net"
"time"
)
// A Device represents a remote network device.
type Device struct {
Host string // the device's hostname or IP address
client *ssh.Client // the client connection
session *ssh.Session // the connection to the remote shell
stdin io.WriteCloser // the remote shell's standard input
stdout io.Reader // the remote shell's standard output
stderr io.Reader // the remote shell's standard error
}
// Connect establishes an SSH connection to a device and sets up the session IO.
func (d *Device) Connect(user, password string) error {
// Create a client connection
client, err := ssh.Dial("tcp", net.JoinHostPort(d.Host, "22"), configureClient(user, password))
if err != nil {
return err
}
d.client = client
// Create a session
session, err := client.NewSession()
if err != nil {
return err
}
d.session = session
return nil
}
// configureClient sets up the client configuration for login
func configureClient(user, password string) *ssh.ClientConfig {
var sshConfig ssh.Config
sshConfig.SetDefaults()
sshConfig.Ciphers = append(sshConfig.Ciphers, "aes128-cbc", "aes256-cbc", "3des-cbc", "des-cbc", "aes192-cbc")
config := &ssh.ClientConfig{
Config: sshConfig,
User: user,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: time.Second * 5,
}
return config
}
// setupIO creates the pipes connected to the remote shell's standard input, output, and error
func (d *Device) setupIO() error {
// Setup standard input pipe
stdin, err := d.session.StdinPipe()
if err != nil {
return err
}
d.stdin = stdin
// Setup standard output pipe
stdout, err := d.session.StdoutPipe()
if err != nil {
return err
}
d.stdout = stdout
// Setup standard error pipe
stderr, err := d.session.StderrPipe()
if err != nil {
return err
}
d.stderr = stderr
return nil
}
// Send sends cmd(s) to the device's standard input. A device only accepts one call
// to Send, as it closes the session and its standard input pipe.
func (d *Device) Send(cmds ...string) error {
if d.session == nil {
return fmt.Errorf("device: session is closed")
}
defer d.session.Close()
// Start the shell
if err := d.startShell(); err != nil {
return err
}
// Send commands
for _, cmd := range cmds {
if _, err := d.stdin.Write([]byte(cmd + "\r")); err != nil {
return err
}
}
defer d.stdin.Close()
// Wait for the commands to exit
d.session.Wait()
return nil
}
// startShell requests a pseudo terminal (VT100) and starts the remote shell.
func (d *Device) startShell() error {
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.OCRNL: 0,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
err := d.session.RequestPty("vt100", 0, 0, modes)
if err != nil {
return err
}
if err := d.session.Shell(); err != nil {
return err
}
return nil
}
// Output returns the remote device's standard output output.
func (d *Device) Output() ([]string, error) {
return readPipe(d.stdout)
}
// Err returns the remote device's standard error output.
func (d *Device) Err() ([]string, error) {
return readPipe(d.stdout)
}
// reapPipe reads an io.Reader line by line
func readPipe(r io.Reader) ([]string, error) {
var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}
// Close closes the client connection.
func (d *Device) Close() error {
return d.client.Close()
}
// String returns the string representation of a `Device`.
func (d *Device) String() string {
return fmt.Sprintf("%s", d.Host)
}
当我们拥有的是数据库和http服务器时,单元测试教程几乎总是玩具问题(为什么总是斐波那契?)。帮助我的一个重要认识是,您只能在可以控制单元输入和输出的地方进行单元测试
configureClient
或readPipe
(给它一个strings.Reader
)是很好的选择。从那里开始
< >通过直接与磁盘、网络、STDUT等对话的程序,如<代码> Connect 方法,您将考虑程序的外部接口的一部分。你不需要对它们进行单元测试。你可以测试它们
将Device
更改为接口而不是结构,并制作一个实现它的MockDevice
。真正的设备现在可能是SSHDevice
。您可以通过插入模拟设备对程序的其余部分(使用设备接口)进行单元测试,以将自己与网络隔离
SSHDevice
将在集成测试中进行测试。启动一个真正的ssh服务器(可能是您使用crypto/ssh包在Go中编写的测试服务器,但任何sshd都可以工作)。用SSHDevice
启动您的程序,让它们相互对话,并检查输出。您将经常使用os/exec
包。编写集成测试比编写单元测试更有趣 如果你想在你公司的网络中配置生产设备,我真的建议你,不要使用一些自编程序来完成。很可能会出现bug,并且损坏可能会很严重。请使用一些配置管理工具,例如ansible(),来配置您的设备。这些工具将通过ssh连接并进行配置。这些工具已经投入生产,已经使用多年了。@mbuechmann:好建议,但与问题无关(正如整个序言与问题无关)@mbuechmann感谢您的建议。出于好奇,Ansible之类的东西和我的程序(一旦我的程序经过彻底测试并被证明是稳定的)有什么区别?除了Flimzy所说的之外,我还想学习如何使我的程序尽可能健壮,并继续为个人使用/实践而开发我的程序。要想实现与现有解决方案的功能对等,您需要花费许多人年的开发时间。这不是你想在一个单独的项目中尝试的东西。如果你的公司真的想把钱花在这上面,我想你可以做到,但是作为一个实际的管理员,如果没有几年的开发,我可能不想让它接近我的产品。。。