Testing graceful http server shutdown that uses channels and signals

Thanks for the input.

Yes, I meant to say I wanted to test with go test command. Currently I am testing manually as you suggested and the whole thing works fine. Since I am a learner I didn’t know how to simulate manual test in unit test.

The Start() function actually triggers ListenAndServe that comes as part of http.NewServer() at the very beginning of the code so it is a wrapper. You can see it in here which is what you answered previously.

I still need to somehow test something with cmd/api/main.go though. If you are wondering what my app looks like it is below. I am a learner so open for any suggestions for improvements.

cmd/api/main.go

package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/joho/godotenv"
	"myapp/internal/app"
	"myapp/internal/http"
)

func main() {
	srv := http.NewServer()

	log.Print("app starting")

	signChan := make(chan os.Signal, 1)

	signal.Notify(signChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	ctx, cancel := context.WithCancel(context.Background())

	go listen(cancel, signChan)

	if err := app.New(srv).Start(ctx); err != nil {
		panic(err)
	}

	log.Print("app shutdown")
}

func listen(cancel context.CancelFunc, signChan chan os.Signal) {
	sig := <-signChan

	log.Printf("%v signal received", sig)

	cancel()
}

internal/app/api.go

package app

import (
	"context"
	"fmt"
	"log"
	"time"

	"myapp/internal/http"
)

type App struct {
	srv http.Server
}

func New(srv http.Server) App {
	return App{srv: srv}
}

func (a App) Start(ctx context.Context) error {
	shutChan := make(chan struct{})

	go shutdown(ctx, shutChan, a)

	if err := a.srv.Start(); err != nil {
		return fmt.Error("http: failed to start")
	}

	<-shutChan

	return nil
}

func shutdown(ctx context.Context, shutChan chan<- struct{}, a App) {
	<-ctx.Done()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	err := a.srv.Shutdown(ctx)
	if err != nil {
		log.Print("http: interrupted some active connections")
	} else {
		log.Print("http: served all active connections")
	}

	close(shutChan)
}

internal/http/server.go

package http

import (
	"net/http"
)

type Server struct {
	*http.Server
}

func NewServer(adr string) Server {
	return Server{
		&http.Server{Addr: adr},
	}
}

func (s Server) Start() error {
	if err := s.ListenAndServe(); err != http.ErrServerClosed {
		return err
	}

	return nil
}
2 Likes