Code coverage is a very useful tool for knowing whether you’ve missed any obvious cases. However, reaching 100% code coverage doesn’t guarantee that there aren’t bugs in your code for some inputs. First you’ll see how go test displays code coverage and then you’ll look at the limitations of relying on code coverage alone.
Adding the -cover flag to the go test command calculates coverage information and includes a summary in the test output. If you include a second flag -coverprofile, you can save the coverage information to a file. Let’s go back to the examples/table directory and gather code coverage information:
$ go test -v -cover -coverprofile=c.out
If you run your table test with code coverage, the test output now includes a line that indicates the amount of test code coverage, 87.5%. That’s good to know, but it’d be more useful if you could see what you missed. The cover tool included with Go generates an HTML representation of your source code with that information:
$ go tool cover -html=c.out
When you run it, your web browser opens and shows you a page with code coverage information using a dark mode color scheme. Every file that’s tested appears in the combo box in the upper left. The source code is in one of three colors. Gray is used for lines of code that aren’t testable, green is used for code that’s been covered by a test, and red is used for code that hasn’t been tested. This reliance on color is unfortunate for those who have red-green color blindness (and those whose publishers don’t want to print large blocks of black ink). To address this issue, I have created a fork of the cover tool. Install and run it with:
$ go install github.com/jonbodner/coverlight@latest
$ coverlight -html c.out
The page that opens should look similar to Figure 6-1. Code with test coverage is displayed in black, code without test coverage is displayed as white text with a black background, and gray is used for untestable lines of code. From looking at this, you can see that you didn’t write a test to cover the default case, when a bad operator is passed to your function. Add that case to your slice of test cases:
{"bad_op", 2, 2, "?", 0, &UnknownOpErr{Op: "?"}},
When you rerun go test -v -cover -coverprofile=c.out and coverlight -html c.out, you see in Figure 6-2 that the final line in DoMath is covered. There is still missing code coverage for the methods for UnknownOppErr. I’ll leave it as an exercise for the reader to write an additional test to get 100% test code coverage.
Initial Code Coverage for DoMath
Figure 6-1. Initial code coverage
Final Code Coverage for DoMath
Figure 6-2. Final code coverage
Code coverage is a great thing, but it’s not enough. There’s actually a bug in your code, even though you have 100% coverage. Have you noticed it? If not, add another test case and rerun your tests:
{"another_mult", 2, 3, "*", 6, nil},
You should see the error:
table_test.go:60: Expected 6, got 5
Your case for multiplication has a typo. It adds the numbers together instead of multiplying them. (Beware the dangers of copy-and-paste coding!) Fix the code, rerun go test -v -cover -coverprofile=c.out and go tool cover -html=c.out, and you’ll see that the tests pass again.
Warning
Code coverage is necessary but not sufficient. You can have 100% code coverage and still have bugs in your code!