Search code examples
unit-testinghttpgointerrupt

How to write a unit test for Interrupt Signal (Ctrl+C) of an HTTP server?


Given a small HTTP server, how can you write a unit test to make sure the interrupt is dealt with correctly?

package main

import (
    "context"
    "fmt"
    "net"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func SimpleServer() {
    ctx := context.Background()
    server := http.Server{
        Addr:        ":8080",
        BaseContext: func(net.Listener) context.Context { return ctx },
    }

    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "pong at ", time.Now().String())
    })

    go server.ListenAndServe()

    exitChannel := make(chan os.Signal, 1) // reserve with buffer size 1 so the notifiers aren't blocked
    defer close(exitChannel)
    signal.Notify(exitChannel, os.Interrupt, syscall.SIGTERM) // Docker and Kubernetes will signal with syscall.SIGTERM

    <-exitChannel

    fmt.Print("\n")
    fmt.Println("shutting down server...")

    err := server.Shutdown(ctx)
    fmt.Println(err)
}

I'm wondering how to pass in this:

exitChannel <- syscall.SIGINT

or:

syscall.Kill(syscall.Getpid(), syscall.SIGINT)

..and then somehow be notified that the server shut down.

Update:

Following Burak's answer below, here's a working solution:

func exampleCaller() {
    exitChannel, serverDownChannel := HTTPServer()

    fmt.Println("sending kill signal in...")
    for i := 3; i > 0; i-- {
        fmt.Println(i)
        time.Sleep(1 * time.Second)
    }
    exitChannel <- syscall.SIGINT
    fmt.Println("kill signal sent")

    <-serverDownChannel
    fmt.Println("server is down!")
}

func HTTPServer() (chan os.Signal, chan struct{}) {
    responseDelay := 1000 // millisecond delay before sending a response message

    ctx := context.Background()
    server := http.Server{
        Addr:        ":8080",
        BaseContext: func(net.Listener) context.Context { return ctx },
    }

    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(time.Duration(responseDelay) * time.Millisecond)
        fmt.Fprint(w, "pong at ", time.Now().String())
    })

    fmt.Println("creating channels...")
    exitChannel := make(chan os.Signal, 1)                    // reserve with buffer size 1 so the notifiers aren't blocked
    serverDownChannel := make(chan struct{})                  // NOTE: we're not passing any data into this channel
    signal.Notify(exitChannel, os.Interrupt, syscall.SIGTERM) // Docker and Kubernetes will signal with syscall.SIGTERM

    go func() {
        <-exitChannel // wait for the signal interrupt
        fmt.Print("\n")
        fmt.Println("shutting down server...")
        err := server.Shutdown(ctx)
        fmt.Println(err)
    }()

    go func() {
        server.ListenAndServe() // start the HTTP server
        close(serverDownChannel)
    }()

    return exitChannel, serverDownChannel
}

Solution

  • Instead of waiting for a signal and shutting down, you can refactor a bit to shutdown based on a signal:

    exitChannel := make(chan struct{}) 
    serverDown:=make(chan struct{})
    go func() {
       <-exitChannel
       server.Shutdown(ctx)
    }()
    go func() {
      server.ListenAndServe()
      close(serverDown)
    }()
    return exitChannel, serverDown
    

    Then, you can use the exitChannel to terminate the server externally, and serverDown to detect if the server shutdown. Hook up the signal channel externally, so when you receive the shutdown signal, you can write to the exitChannel.