草庐IT

unit-testing - 在 Go 中对 SSH 客户端进行单元测试

coder 2023-06-28 原文

我用 Go 写了一个 SSH 客户端,我想写一些测试。问题是我以前从未真正编写过正确的单元测试,而且大多数教程似乎都侧重于为添加两个数字或其他玩具问题的函数编写测试。我读过有关模拟、使用接口(interface)和其他技术的信息,但我在应用它们时遇到了麻烦。此外,我的客户端将同时使用,以允许一次快速配置多个设备。不确定这是否会改变我编写测试的方式或是否会添加额外的测试。感谢您的帮助。

这是我的代码。基本上,Device 有 4 个主要功能:ConnectSendOutput/ErrClose 分别用于连接到设备、向其发送一组配置命令、捕获 session 的输出以及关闭客户端。

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 服务器时,你提出了一个很好的观点。对我有帮助的最大认识是,您只能对可以控制单元输入和输出的事物进行单元测试。 configureClientreadPipe(给它一个 strings.Reader)将是不错的选择。从这里开始。

任何通过直接与磁盘、网络、stdout 等对话而离开您的程序的东西,例如 Connect 方法,您可以将其视为程序外部接口(interface)的一部分。你不对那些进行单元测试。您对它们进行集成测试。

Device 更改为接口(interface)而不是结构,并制作一个实现它的 MockDevice。真正的设备现在可能是 SSHDevice。您可以通过插入 MockDevice 对程序的其余部分(使用设备接口(interface))进行单元测试,以将自己与网络隔离。

SSHDevice 将在您的集成测试中进行测试。启动一个真正的 ssh 服务器(也许是你使用 crypto/ssh 包在 Go 中编写的测试服务器,但任何 sshd 都可以)。使用 SSHDevice 启动您的程序,让它们相互对话,并检查输出。您将经常使用 os/exec 包。编写集成测试比编写单元测试更有趣!

关于unit-testing - 在 Go 中对 SSH 客户端进行单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49418982/

有关unit-testing - 在 Go 中对 SSH 客户端进行单元测试的更多相关文章

  1. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  2. ruby - Capistrano 3 在任务中更改 ssh_options - 2

    我尝试使用不同的ssh_options在同一阶段运行capistranov.3任务。我的production.rb说:set:stage,:productionset:user,'deploy'set:ssh_options,{user:'deploy'}通过此配置,capistrano与用户deploy连接,这对于其余的任务是正确的。但是我需要将它连接到服务器中配置良好的an_other_user以完成一项特定任务。然后我的食谱说:...taskswithoriginaluser...task:my_task_with_an_other_userdoset:user,'an_othe

  3. ruby-on-rails - Prawn - 表格单元格内的链接 - 2

    我正在尝试用Prawn生成PDF。在我的PDF模板中,我有带单元格的表格。在其中一个单元格中,我有一个电子邮件地址:cell_email=pdf.make_cell(:content=>booking.user_email,:border_width=>0)我想让电子邮件链接到“mailto”链接。我知道我可以这样链接:pdf.formatted_text([{:text=>booking.user_email,:link=>"mailto:#{booking.user_email}"}])但是将这两行组合起来(将格式化文本作为内容)不起作用:cell_email=pdf.make_c

  4. ruby - 单元测试文件 I/O 方法 - 2

    我对单元测试还是比较陌生。我用Ruby编写了一个类,它接受一个文件,在该文件中搜索给定的Regex模式,替换它,然后将更改保存回文件。我希望能够为此方法编写单元测试,但我不知道我将如何去做。有人能告诉我我们如何对处理文件i/o的方法进行单元测试吗? 最佳答案 看看这个HowdoIunit-testsavingfiletothedisk?基本上这个想法是一样的,文件系统是你的类的依赖。所以引入一个可以在你的单元测试中模拟的角色/接口(interface)(这样你在单元测试时就没有依赖性);角色中的方法应该是您从文件系统中需要的所有东西

  5. ruby - 尝试运行 minitest 单元测试时出错 - 2

    尝试使用rubytest/test_foo.rb运行minitest单元测试时出现以下错误:Warning:youshouldrequire'minitest/autorun'instead.Warning:oradd'gem"minitest"'before'require"minitest/autorun"'From:/home/emile/.rvm/rubies/ruby-2.0.0-p0/lib/ruby/2.0.0/minitest/autorun.rb:15:```test_foo.rb看起来像这样:require'minitest/autorun'classTestFoo

  6. ruby - 使用 ruby​​ gem net-ssh-multi 同时在多个服务器上执行 sudo 命令 - 2

    在previousquestion中我想出了如何在多个服务器上启动经过密码验证的sshsession来运行单个命令。现在我需要能够执行“sudo”命令。问题是,net-ssh-multi没有分配sudo需要运行的伪终端(pty),导致以下错误:[127.0.0.1:stderr]sudo:sorry,youmusthaveattytorunsudo根据documentation,可以通过调用channel对象的方法来分配伪终端,但是,以下代码不起作用:它会生成上面的“notty”错误:require'net/ssh'require'net/ssh/multi'Net::SSH::Mul

  7. ruby 单元测试 : run some code after each failed test - 2

    在Test::Unit中的ruby​​单元测试断言失败后,在执行teardown之前,是否有一些简洁优雅的方法来立即执行我的代码?我正在做一些自动化的GUI测试,并希望在出现问题后立即截图。 最佳答案 如果您使用的是1.9,请不要使用Test::Unit::TestCase作为您的基类。对其进行子类化并覆盖#run_test以进行救援,截取屏幕截图并重新提出:classMyAbstractTestCase或者,我认为这实际上是最简洁的方法,您可以使用before_teardownHook:classMyTestCase这不适用于1.

  8. ruby - 在 TCPServer (Ruby) 中,我如何从客户端获取 IP/MAC? - 2

    我想在Ruby的TCPServer中获取客户端的IP地址。以及(如果可能的话)MAC地址。例如,Ruby中的时间服务器,请参阅评论。tcpserver=TCPServer.new("",80)iftcpserverputs"Listening"loopdosocket=tcpserver.acceptifsocketThread.newdoputs"Connectedfrom"+#HERE!HowcanigettheIPAddressfromtheclient?socket.write(Time.now.to_s)socket.closeendendendend非常感谢!

  9. ruby-on-rails - Ruby on Rails 单表继承(STI)和单元测试问题(使用 PostgreSQL) - 2

    我正在使用带有单个“帐户”表的STI模型来保存用户和技术人员的信息(即用户...8)错误:test_the_truth(用户测试):ActiveRecord::StatementInvalid:PGError:ERROR:关系“技术人员”不存在:从“技术人员”中删除...从本质上讲,标准框架不承认Technicians和Users表(或PostgreSQL称它们为“关系”)不存在,事实上,应该别名为Accounts。有什么想法吗?我对RoR比较陌生,不知道如何解决这个问题而又不完全删除STI。 最佳答案 原来问题是由于存在:./te

  10. ruby - 在 ruby​​ 中对字符串进行排序以使空字符串位于末尾的好方法是什么? - 2

    在Ruby中,默认排序将空字符串放在第一位。['','g','z','a','r','u','','n'].sort给予:["","","a","g","n","r","u","z"]但是,在end处需要空字符串是很常见的。做类似的事情:['','g','z','a','r','u','','n'].sort{|a,b|a[0]&&b[0]?ab:a[0]?-1:b[0]?1:0}工作并给予:["a","g","n","r","u","z","",""]但是,这不是很可读,也不是很灵活。在Ruby中是否有一种合理且干净的方法让sort将空字符串放在最后?只映射到一个没有空字符串的数组,

随机推荐