0

I want to get test coverage of a REST service written in Go. I am spawning REST service through a goroutine, then making HTTP requests using rest client, and reviewing the HTTP responses. Tests are passing successfully but go test -cover returns 0% test coverage. Is there a way to get the actual test coverage of all the packages used inside the go lang REST service.

my test file: main_test.go

import (
    "testing"
)

// Test started when the test binary is started. Only calls main.
func TestSystem(t *testing.T) {
    go main()    // Spinning up the go lang REST server in a separate go routine.
    http.Post("https://localhost/do_something")
}

my output:

go test -cover  main_test.go
ok      command-line-arguments  0.668s  coverage: 0.0% of statements 
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
mann
  • 27
  • 4
  • 1
    You started your service and made request to it but you did not gave even a millisecond to process this request. What will become if you add a `time.Sleep(time.Second)` after `http.Post`? Although I recomend you to think about organizing your tests into suites: a) startup with `go main()`, b) tests itself, c) tear-down if needed – Alex Yu Sep 18 '19 at 11:14
  • 3
    I suspect the problem is that you're calculating stats for the wrong package. Use the `-coverpkg` flag to indicate which package(s) to calculate coverage stats for. See [here](https://stackoverflow.com/q/28349085/13860). – Jonathan Hall Sep 18 '19 at 11:24
  • 1
    Also, as mentioned in comments below, the way you're invoking your goroutine is completely wrong. You probably shouldn't run `main()` in a goroutine. Instead, start a goroutine that starts your app, but can be cancelled when the tests are done, perhaps with a `context.Context`. – Jonathan Hall Sep 18 '19 at 11:33
  • The proper way to test main is by using `func TestMain(m *testing.M)` as described in the package documentation of package testing. Rule of thumb: **Always** read the **whole** package documentation first. – Volker Sep 18 '19 at 12:47
  • 3
    That's not a unit test... it's just not. Honestly, running everything and just making a request is the same as a bash script like `go run . &; curl 0.0.0.0:8080/do_something` – Elias Van Ootegem Sep 18 '19 at 13:07
  • 1
    Why you omit check of response and error from `http.Post`? At least you need to add `resp, err:= http.Post("https://localhost/do_something")` and check `resp and err ` afterwards. Maybe you're trying POST on inexistent url? – Alex Yu Sep 18 '19 at 13:21
  • I would scrap this test altogether. It's not a unit test; it's calling `main` from within a test func which means all your flags etc. are going to be wrong; it calls `main` in a goroutine with no way to stop it; it makes no assertions therefore it will *always* pass unless it panics; it delivers exactly zero value as a test. Just delete it. – Adrian Sep 18 '19 at 13:40

3 Answers3

1

Preface

This could not be a complete answer covering all cases.

It's an attempt to explain how it could be possible to get zero coverage from what OP explained.

Test itself

Tests are passing successfully but go test -cover returns 0% test coverage.

  1. Start of service with go main() inside Test-function
  2. Attempt of http.Post to service without checks for errors and that response is not nil
  3. No panics during test so test is passing

Problems in test

  1. No guarantee that service is actually started before attempt of http.Post
  2. Results of http.Post are not checked

For first problem:

  • bad/dirty solution: add time.Sleep after go main()
  • good/clean solution: instrument service with some kind of signalling/monitoring that serve loop is started

For second problem:

Take a look at such Test-function:

func TestPost(t *testing.T) {
    var b bytes.Buffer
    http.Post("http:/inexistent.com/inexistent-url", "", &b)
    t.Log("No problems!")
}

This test will always pass. Because it tests non-existense of panics only.

To make this test more correct:

func TestPost(t *testing.T) {
    var b bytes.Buffer
    if _, err:= http.Post("http:/inexistent.com/inexistent-url", "", &b); err!=nil{
    t.Errorf("http.Post : %v", err)
}

Proposals

  1. Check http.Post results
  2. Check that tested service is actually started
Alex Yu
  • 3,412
  • 1
  • 25
  • 38
1

Thanks all for your reply. I am able to get the coverage by -coverpkg. Not sure what exactly solved the issue below are couple of changes which worked for me as suggested by @Flimzy 1. Moved REST server Initialization code from main to a function. and then in test case starting the REST server within go routine and gracefully terminating it by sending a message to channel

    channel := make(chan error) // create a channel will listen for error
    go func(channel chan error) {
        inithandler.InitRestServer(channel, )
        _ = <-channel // terminate the channel
    }(channel)
http.Post("https://localhost/do_something") // call the endpoint and resp verification
.
.
channel <- errors.New("stop")

trigger test with -coverpkg

go test main_test.go -cover -coverpkg ./...
mann
  • 27
  • 4
-1

Regardless of the root cause, I think there’s an issue with your general approach here.

I would suggest that some of what you’re testing here isn’t your code and you shouldn’t bother. You don’t need to actually test the http protocol and http package, right?

So the way I’ve handled this in the past is to break apart my the meaningful code that I want to test out of the http handler functions. The http handlers should only be responsible for validating input, calling the actual business logic, and then formatting output.

Once you’ve broken apart your code this way, you can directly call your meaningful functions from your tests. Your http handler functions should be so simple that there is no place for bugs to lurk.

Don’t seek 100% test coverage. Test what matters.

VolatileCoder
  • 244
  • 1
  • 8
  • golang test coverage works by instrumenting original code with assignments per each line. It does not monitor threads. Read [The cover story](https://blog.golang.org/cover). I thiink the real problem is that `TestSubSystem` exits right after `http.Post` without giving a chance to execute even a line in tested service. – Alex Yu Sep 18 '19 at 11:19
  • "It’s likely what’s happening here is the code coverage system subsystem is only monitoring the originating thread" -- No, this is not how it works, at all. – Jonathan Hall Sep 18 '19 at 11:22
  • Thanks for the link to the cover story; I hadn’t seen this one. – VolatileCoder Sep 18 '19 at 11:25
  • I don't think the early exit is the explanation, either. You'd probably get _some_ coverage if that were the problem, not 0. But even so, the way the goroutine is done is clearly wrong. – Jonathan Hall Sep 18 '19 at 11:32