如何在不创建实际网络连接的情况下测试依赖于net.Conn的代码?

时间:2016-06-30 13:45:52

标签: unit-testing go

如果我的代码适用于net.Conn,如何在不实际创建与localhost的网络连接的情况下为其编写测试?

我在网上看不到任何解决方案;人们似乎要么忽略它(没有测试),编写不能并行运行的测试(即使用实际的网络连接,使用up端口),或者使用io.Pipe。

但是,net.Conn定义SetReadDeadlineSetWriteDeadline;和io.Pipe没有。 net.Pipe 没有,尽管表面上声称要实现该接口,但它只是用以下方式实现:

func (p *pipe) SetDeadline(t time.Time) error {
    return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

func (p *pipe) SetReadDeadline(t time.Time) error {
    return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

func (p *pipe) SetWriteDeadline(t time.Time) error {
    return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

(见:https://golang.org/src/net/pipe.go

所以...还有其他方法吗?

我会接受任何答案,说明如何在测试中使用流,而工作截止日期不是实际的网络套接字。

(当然,这个cloudflare blogpost涵盖了使用截止日期的动机,以及为什么在每个连接的goroutine中永久阻止不是一个可接受的解决方案;但不管这个论点,特别是在这种情况下,我正在寻找一个解决方案 for tests 我们故意要处理坏连接挂起的边缘情况等。)

(注意,这可能看起来像是Simulate a tcp connection in Go的副本,但请注意,该问题中提出的所有解决方案都没有实现截止日期功能,具体是我在这里要求如何测试的问题)

2 个答案:

答案 0 :(得分:5)

您的问题非常开放,因此无法向您提供“正确答案”。但我想我明白你坚持的地步。这个答案也是公开的,但它应该会让你重回正轨。

前几天我写了short article,其中显示了你必须使用的原则。

在我做一些小例子之前,我们需要解决一个重点:

我们不测试网络包。我们认为,包没有错误并且确实如此,文档说的是什么。这意味着我们并不关心Go团队如何实施 SetReadDeadlineSetWriteDeadline。我们只测试程序中的用法。

第1步:重构代码

你没有发布任何代码片段,所以我给你一个简单的例子。我猜你有一个方法或功能,你正在使用网络包。

func myConn(...) error {
  // You code is here
  c, err := net.Dial("tcp", "12.34.56.78:80")
  c.setDeadline(t)
  // More code here
}

你可以测试你需要重构你的函数,所以它只是使用net.Conn接口。为此,必须将net.Dial()调用移到函数之外。请记住,我们不想测试net.Dial函数。

新功能看起来像这样:

func myConn(c, net.Conn, ...) error {
  // You code is here
  c.setDeadline(t)
  // More code here
}

第2步:实施net.Conn界面

对于测试,您需要实现net.Conn接口:

type connTester struct {
    deadline time.Time
}

func (c *connTester) Read(b []byte) (n int, err error) {
    return 0, nil
}

...

func (c *connTester) SetDeadline(t time.Time) error {
    c.deadline = t
    return nil
}

...

完整实施,包括小型支票: https://play.golang.org/p/taAmI61vVz

第3步:测试

测试时,我们不关心Dial()方法,我们只是创建一个指向testtype的指针,它实现了net.Conn接口并将其放入你的函数中。之后,如果正确设置了截止日期参数,我们会查看我们的测试用例。

func TestMyConn(t *testing.T){
  myconnTester = &connTester{}
  err := myConn(myconnTester,...)
  ...
  if myconntester.deadline != expectedDeadline{
  //Test fails
  }
}

因此,在测试时,您应该始终考虑要测试的功能。我认为抽象你真正想写的功能是最困难的部分。在简单的单元测试中,您永远不应该测试标准库的功能。希望这些例子可以帮助您重回正轨。

答案 1 :(得分:2)

对于单元测试中的受控版本,需要换出的代码应该落后于抽象。在这种情况下,抽象将是net.Conn接口。生产代码将使用go std lib net.Conn,但测试代码将使用配置了完整逻辑的测试存根来运行您的功能。

引入抽象是一种强大的模式,应该允许交换所有IO或基于时序的代码,以允许在单元测试期间控制代码的执行。

@apxp在评论中说明了相同的方法。

同样的方法应该适用于截止日期。它可能会很难模拟到达的截止日期,因为您可能必须使用多个响应配置存根。即。第一个响应成功,但第二个响应模拟已达到的截止日期,并为第二个请求引发错误。