Hi,
This is the main entrypoint of my app and I would like to test it. However, given that my Go knowledge is a few days old, I am struggling to write a test for it. Technically it boots up http server and shuts down gracefully if signal is received. How do I test this please?
Thanks
func main() {
srv := http.NewServer()
log.Print("app starting")
shutChan := make(chan struct{})
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)
// This calls ListenAndServe behind the scene
if err := srv.Start(ctx); err != nil {
panic(err)
}
<-shutChan
log.Print("app shutdown")
}
func listen(cancel context.CancelFunc, signChan chan os.Signal) {
sig := <-signChan
log.Printf("%v signal received", sig)
cancel()
}
Do you mean testing with a unit test called with go test ? It will be possible only if one can generate a SIGINT or SIGTERM signal by code. I don’t know if this is possible (signChan<- Beside, one can’t test a main function with go test. To do so you would have to encapsulate your code into functions that you can then test.
For these reasons, I would test this code manually by running it and type ctrl-C.
By the way, I could not find the method Start() in the http documentation or in the source. Normally, you should call srv.Shutdown(ctx) in your listen function and the given context is typically a timeout context because Shutdown will block until all connections have returned to idle. It’s not impossible that this may block forever.
Note also that os.Interrupt is equal to syscall.SIGINT (defined in os/exec_posix.go).
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
}
To test something in main, you have to put the code in main into another function that you can then call from the test function.
Change this
func main() {
do things ...
}
into
func main() {
foo()
}
func foo() {
do things ...
}
func TestFoo(t *testing.T) {
foo()
if state != expected {
t.Error("unexpected state")
}
}
I will do that. Thank you.