golang源码分析:httptest

Golang
211
0
0
2024-01-19

httptest是golang官方源码自带的测试包,它可以非常方便获取http请求结构体,http返回值结构体,以及在本地启动一个loopback的server,方便我们做单测。对于go的web应用程序中往往需要与其他系统进行交互, 比如通过http访问其他系统, 此时就需要一种方法用于打桩来模拟Web服务端和客户端,httptest包即Go语言针对Web应用提供的解决方案。

1,获取返回值

rr := httptest.NewRecorder()

我们创建一个 ResponseRecorder (which satisfies http.ResponseWriter)来记录响应,主要利用httptest.NewRecorder()创建一个http.ResponseWriter,模拟了真实服务端的响应,这种响应时通过调用http.DefaultServeMux.ServeHTTP方法触发的。对应源码位于src/net/http/httptest/recorder.go

type ResponseRecorder struct {
  // Code is the HTTP response code set by WriteHeader.
  //
  // Note that if a Handler never calls WriteHeader or Write,
  // this might end up being 0, rather than the implicit
  // http.StatusOK. To get the implicit value, use the Result
  // method.
  Code int


  // HeaderMap contains the headers explicitly set by the Handler.
  // It is an internal detail.
  //
  // Deprecated: HeaderMap exists for historical compatibility
  // and should not be used. To access the headers returned by a handler,
  // use the Response.Header map as returned by the Result method.
  HeaderMap http.Header


  // Body is the buffer to which the Handler's Write calls are sent.
  // If nil, the Writes are silently discarded.
  Body *bytes.Buffer


  // Flushed is whether the Handler called Flush.
  Flushed bool


  result      *http.Response // cache of Result's return value
  snapHeader  http.Header    // snapshot of HeaderMap at first Write
  wroteHeader bool
}
func NewRecorder() *ResponseRecorder {
  return &ResponseRecorder{
    HeaderMap: make(http.Header),
    Body:      new(bytes.Buffer),
    Code:      200,
  }
}

它对应的方法如下,可以根据实际需求进行调用。

func (rw *ResponseRecorder) Header() http.Header {
func (rw *ResponseRecorder) writeHeader(b []byte, str string) {
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
func (rw *ResponseRecorder) WriteString(str string) (int, error) {
func (rw *ResponseRecorder) WriteHeader(code int) {
func (rw *ResponseRecorder) Flush() {
func (rw *ResponseRecorder) Result() *http.Response {

2,获取http请求

req := httptest.NewRequest(
    http.MethodPost,
    "/health-check",
    bytes.NewReader(reqBody),
  )

在测试的时候可以把http.NewRequest替换为httptest.NewRequest。httptest.NewRequest的第三个参数可以用来传递body数据,必须实现io.Reader接口。httptest.NewRequest不会返回error,无需进行err!=nil检查。解析响应时没直接使用ResponseRecorder,而是调用了Result函数。源码位于src/net/http/httptest/httptest.go

func NewRequest(method, target string, body io.Reader) *http.Request {
  req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(method + " " + target + " HTTP/1.0\r\n\r\n")))
  if body != nil {
    switch v := body.(type) {
    case *bytes.Buffer:
      req.ContentLength = int64(v.Len())
    case *bytes.Reader:
      req.ContentLength = int64(v.Len())
    case *strings.Reader:
      req.ContentLength = int64(v.Len())
    default:
      req.ContentLength = -1
    }
    if rc, ok := body.(io.ReadCloser); ok {
      req.Body = rc
    } else {
      req.Body = io.NopCloser(body)
    }
  }

3,本地起模拟服务器

httptest.NewServer(http.HandlerFunc(healthHandler))

模拟服务器的创建使用的是httptest.NewServer函数,它接收一个http.Handler处理API请求的接口。代码示例中使用了Hander的适配器模式,http.HandlerFunc是一个函数类型,实现了http.Handler接口,这里是强制类型转换,不是函数的调用,源码位于:src/net/http/httptest/server.go

type Server struct {
  URL      string // base URL of form http://ipaddr:port with no trailing slash
  Listener net.Listener


  // EnableHTTP2 controls whether HTTP/2 is enabled
  // on the server. It must be set between calling
  // NewUnstartedServer and calling Server.StartTLS.
  EnableHTTP2 bool


  // TLS is the optional TLS configuration, populated with a new config
  // after TLS is started. If set on an unstarted server before StartTLS
  // is called, existing fields are copied into the new config.
  TLS *tls.Config


  // Config may be changed after calling NewUnstartedServer and
  // before Start or StartTLS.
  Config *http.Server


  // certificate is a parsed version of the TLS config certificate, if present.
  certificate *x509.Certificate


  // wg counts the number of outstanding HTTP requests on this server.
  // Close blocks until all requests are finished.
  wg sync.WaitGroup


  mu     sync.Mutex // guards closed and conns
  closed bool
  conns  map[net.Conn]http.ConnState // except terminal states


  // client is configured for use with the server.
  // Its transport is automatically closed when Close is called.
  client *http.Client
}

默认会监听127.0.0.1:0

func newLocalListener() net.Listener {
if serveFlag != "" {
l, err := net.Listen("tcp", serveFlag)
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
func NewServer(handler http.Handler) *Server {
  ts := NewUnstartedServer(handler)
  ts.Start()
  return ts
}
func NewUnstartedServer(handler http.Handler) *Server {
  return &Server{
    Listener: newLocalListener(),
    Config:   &http.Server{Handler: handler},
  }
}
func (s *Server) Start() {
          s.URL = "http://" + s.Listener.Addr().String()
  s.wrap()
  s.goServe()
func (s *Server) StartTLS() {
 s.Listener = tls.NewListener(s.Listener, s.TLS)
  s.URL = "https://" + s.Listener.Addr().String()
  s.wrap()
  s.goServe()
func NewTLSServer(handler http.Handler) *Server {
  ts := NewUnstartedServer(handler)
  ts.StartTLS()
  return ts
}
func (s *Server) Close() {
        for c, st := range s.conns {
        if st == http.StateIdle || st == http.StateNew {
        s.closeConn(c)
      }
func (s *Server) CloseClientConnections() {
 for c := range s.conns {
    go s.closeConnChan(c, ch)
  }
func (s *Server) goServe() {
  s.wg.Add(1)
  go func() {
    defer s.wg.Done()
    s.Config.Serve(s.Listener)
  }()
}

在协程里对不同http状态进行不同处理

func (s *Server) wrap() {
  s.Config.ConnState = func(c net.Conn, cs http.ConnState) {
    switch cs {
    case http.StateNew:
        case http.StateActive:
        case http.StateIdle:
        case http.StateHijacked, http.StateClosed: