Home > database >  How to unit test a net.Conn function that modifies the message sent?
How to unit test a net.Conn function that modifies the message sent?

Time:10-25

First, let me tell that I've seen other questions that are similar to this one, but I don't think any question really answers it in enough detail (How does one test net.Conn in unit tests in Golang? and How does one test net.Conn in unit tests in Golang?).

What I want to test is function that in some way responds to a TCP request.

In the simplest scenario:

func HandleMessage(c net.Conn) {
  s, err := bufio.NewReader(c).ReadString('\n')
  s = strings.Trim(s, "\n")

  c.Write([]byte(s   " whatever"))
}

How would I unit test such a function, ideally using net.Pipe() to not open actual connections. I've been trying things like this:

func TestHandleMessage(t *testing.T) {
  server, client := net.Pipe()

  go func(){
    server.Write([]byte("test"))
    server.Close()
  }()

  s, err := bufio.NewReader(c).ReadString('\n')

  assert.Nil(t, err)
  assert.Equal(t, "test whatever", s) 
}

However I can't seem to understand where to put the HandleMessage(client) (or HandleMessage(server)? in the test to actually get the response I want. I can get it to the point that it either blocks and won't finish at all, or that it will return the exact same string that I wrote in the server.

Could someone help me out and point out where I'm making a mistake? Or maybe point in a correct direction when it comes to testing TCP functions.

CodePudding user response:

The net.Pipe docs say:

Pipe creates a synchronous, in-memory, full duplex network connection; both ends implement the Conn interface. Reads on one end are matched with writes on the other, copying data directly between the two; there is no internal buffering.

So the labels you are attaching to the net.Conn's (server and client) are arbitrary. If you find it simpler to understand feel free to use something line handleMessageConn, sendMessageConn := net.Pipe().

The below basically fills out the example given in the answer you mentioned.

func TestHandleMessage(t *testing.T) {
    server, client := net.Pipe()

    // Set deadline so test can detect if HandleMessage does not return
    client.SetDeadline(time.Now().Add(time.Second))

    // Configure a go routine to act as the server
    go func() {
        HandleMessage(server)
        server.Close()
    }()


    _, err := client.Write([]byte("test\n"))
    if err != nil {
        t.Fatalf("failed to write: %s", err)
    }

    // As the go routine closes the connection ReadAll is a simple way to get the response
    in, err := io.ReadAll(client)
    if err != nil {
        t.Fatalf("failed to read: %s", err)
    }

    // Using an Assert here will also work (if using a library that provides that functionality)
    if string(in) != "test whatever" {
        t.Fatalf("expected `test` got `%s`", in)
    }

    client.Close()
}

You could turn this around and put the Write/Read in the go routine but I believe the above approach is easier to understand and simplifies avoiding a limitation of the testing package:

A test ends when its Test function returns or calls any of the methods FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as the Parallel method, must be called only from the goroutine running the Test function.

Note: If you don't need a net.Conn (as is the case in this simple example) consider using HandleMessage(c io.ReadWriter) (this provides users with more flexibility and also simplifies testing).

CodePudding user response:

blocks and won't finish at all

Well, the code you posted is so incomplete as to make me unable to know if these are the real problems you face - for example, there is no c in TestHandleMessage. But giving you the benefit of the doubt, I'll say there's 2 problems with this code:

First, your test never writes to client so there is nothing to read from server. You write to server and close it but you never write anything to client. So that's the first problem, but again, that code wouldn't compile anway.

Secondly, look at this combination:

  c.Write([]byte(s   " whatever"))
  s, err := bufio.NewReader(c).ReadString('\n')

HandleMessage does not write a newline, but the client expects one. It hangs indefinitely waiting for a newline that will never be written. Again, I'm not sure if you ran into this problem or the first problem or both - becasue your code doesn't compile.

You have to change this line:

  c.Write([]byte(s   " whatever\n"))

You also have to write the initial string to the client connection, so that it can then be read by the server on the other end of the pipe.

Putting it all together, pulling out any external dependency (testify), and fixing some errors ends up with :

t.go:

package main

import (
    "net"
    "fmt"
    "bufio"
    "strings"
)

func HandleMessage(c net.Conn) {
  s, err := bufio.NewReader(c).ReadString('\n')
    if err != nil {
        panic(fmt.Errorf("HandleMessage could not read: %w", err))
    }
  s = strings.Trim(s, "\n")
  c.Write([]byte(s   " whatever\n"))
}

t_test.go:

package main

import(
    "net"
    "bufio"
    "fmt"
    "testing"
)

func TestHandleMessage(t *testing.T) {
  server, client := net.Pipe()

  go func(){
    HandleMessage(server)
  }()
    fmt.Fprintln(client, "test")
  s, err := bufio.NewReader(client).ReadString('\n')

  if err != nil {
        t.Errorf("Error should be nil, got: %s", err.Error())
    }
    if s != "test whatever\n" {
        t.Errorf("Expected result to be 'test whatever\\n', got %s", s)
    }
}
% go test t.go t_test.go -v
=== RUN   TestHandleMessage
--- PASS: TestHandleMessage (0.00s)
PASS
ok      command-line-arguments  0.100s
  • Related