JavaRush /Courses /Go SELF /Coverage as a hint: what is critical to cover

Coverage as a hint: what is critical to cover

Go SELF
Level 38, Lesson 1
Available

1. Coverage: what it measures and what it does not

When you first see “coverage: 63%”, your brain usually does one of two wrong things: either it celebrates (“yay, more than half!”) or it starts panicking (“it’s all over, we urgently need 100%”). Let’s start calmly: coverage in Go is about which parts of the code executed when running tests. In the classic mode it answers “was this statement executed at least once?”, not “did we verify the result”, and definitely not “are there no bugs”.

Imagine coverage as a motion sensor in an office. If someone walked through the hallway, the sensor triggered. But the sensor does not know whether the person was sober, whether they went where they should, or whether they stole your monitor on the way.

Let’s look at a very small function from our training app (a hypothetical task package for tasks):


package task

func TitleForList(title string, done bool) string {
	if done {
		return "[x] " + title
	}
	return "[ ] " + title
}

If you write a “test” that just calls the function and checks nothing, coverage will go up—but the value is close to zero:

package task

import "testing"

func TestTitleForList_NoChecks(t *testing.T) {
	_ = TitleForList("buy milk", false)
	_ = TitleForList("buy milk", true)
}

This test really does “walk through the code”, but it does not catch regressions. If someone breaks the format ("[x]" becomes "(x)"), the test will stay green, coverage will also look cheerful, and you will be sad.

2. Coverage as a terrain map

Coverage becomes useful not when you see the percentage, but when you look at which lines did not execute and ask yourself the right questions: “Is this a normal unreachable branch? Is this forgotten error handling? Is this dead code? Or did we just not think about an edge case?”

In Go, the standard path is usually this: you run tests while saving a coverage profile, and then open the report with go tool cover. For example, go test -coverprofile=coverage.out saves the coverage profile to a file, and then you can print a per-function breakdown or open an HTML report with line highlighting.

Here is a typical set of commands worth remembering (this is not “terminal magic”, it’s just two steps: collect data → view data):

go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
go tool cover -html=coverage.out

In the HTML report, Go highlights code: covered parts in green, uncovered parts in red. That is exactly what turns coverage from a “percentage for the sake of percentage” into a diagnostic tool: you can literally see where tests have never been.

You can imagine the coverage workflow as the following flowchart:

flowchart TD
    A[Run tests with coverprofile] --> B[Open HTML coverage]
    B --> C{Red zone: is this an important branch?}
    C -->|Yes| D[Write a meaningful test for this branch]
    C -->|No| E{Is this dead or unnecessary code?}
    E -->|Yes| F[Delete or simplify the code]
    E -->|No| G[Leave it as-is, but understand why]

Notice the key point: sometimes the correct reaction to red is not “add a test”, but “remove unnecessary code” (or at least understand why it cannot be reached). Coverage is great precisely because it provokes this conversation.

3. What is critical to cover

When you have little time (and you almost always do), it is important not to try to cover absolutely everything, but to cover what breaks most often and what is most expensive to fix when it breaks. Coverage, in this sense, is a hint about where to look, but the engineering brain makes the decision.

Below is a priority table that is convenient to keep in mind specifically as “what to cover first” when coverage shows red zones.

What to cover first Why it is critical How it usually looks in code
if
/
switch
branches that change behavior
These are different scenarios, not “implementation details”
if done { ... } else { ... }
Error handling In production, things break exactly there: IO, parsing, validation
if err != nil { return ... }
Boundary values “empty”, “zero”, “minus one”, “too long”—classic bug territory
title == ""
,
id <= 0
,
len(s) > limit
Exported behavior (public package API) This is a contract: breaking it is especially painful
func New(...)
,
func (t Task) Title() string
Deterministic output Otherwise tests become flaky, and users see “dancing” output output from
map
, sorting

A separate important thought: you should not cover “lines”, you should cover contracts. One line of code can be critical (for example, an access rights check), while ten lines can be decorative (for example, formatting a log message that is never used). Coverage shows lines, but it does not explain meaning. Meaning is your job.

4. The task domain and invariants

Let’s continue our “end-to-end” story with tasks. Suppose we have a task package that is responsible for the task model and its basic rules. We already discussed that the model must protect its invariants: an empty title is a bad idea, because a task list will turn into a set of empty lines.

Let’s make a minimal model and a constructor:

package task

import "errors"

var ErrInvalidTitle = errors.New("invalid title")

type Task struct {
	title string
	done  bool
}

func New(title string) (Task, error) {
	if title == "" {
		return Task{}, ErrInvalidTitle
	}
	return Task{title: title}, nil
}

Now the coverage question sounds very practical: “Do we even have a test that goes into the title == "" branch?” If not, that branch is red. And it is exactly the kind of red that is dangerous: not because of the “percentage”, but because you can silently break validation and nobody will notice until tasks without names show up in storage.

A meaningful test here is small and very useful:

package task

import (
	"errors"
	"testing"
)

func TestNew_EmptyTitle(t *testing.T) {
	_, err := New("")
	if !errors.Is(err, ErrInvalidTitle) {
		t.Fatalf("expected ErrInvalidTitle, got %v", err)
	}
}

Notice: we check not only err != nil, but also the error class via errors.Is. This matters because in a real application (CLI/HTTP) we will make decisions: show the user “invalid title” or “internal error”.

And a second test—the happy path—to cover the other branch:

package task

import "testing"

func TestNew_OK(t *testing.T) {
	got, err := New("buy milk")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got.title != "buy milk" {
		t.Fatalf("title mismatch: %q", got.title)
	}
}

Yes, we reached into the internal field title. In an “ideal world” we would read the title through a Title() method. But if you already have the method—use it. If you don’t yet—this kind of test is acceptable in training code; just don’t make it a habit for public packages.

5. Coverage and error branches

Beginner tests often look like a celebration of optimism: “well, the user will enter it correctly”. And then reality is: the user will enter "", " ", "abc", "99999999999999999999999", and will also paste spaces, newlines, and the philosophical question “why doesn’t it work”.

Let’s add a small helper to our task package that parses an id from a string. This is a typical piece for CLI (arguments) and for HTTP (path param). We are not diving into networking; we care about the logic itself:

package task

import (
	"errors"
	"strconv"
)

var ErrInvalidID = errors.New("invalid id")

func ParseID(s string) (int, error) {
	id, err := strconv.Atoi(s)
	if err != nil || id <= 0 {
		return 0, ErrInvalidID
	}
	return id, nil
}

The coverage hint here is very straightforward: you have at least two branches—success and error. And the error is actually “two-headed”: either Atoi failed, or the number is <= 0. If you test only "42", coverage will be only partially green: the error branch will remain red, and that is an honest hint: “you are not testing what most often hits you in production”.

A table-driven test (short, readable, no copy-paste) is a perfect fit:

package task

import (
	"errors"
	"testing"
)

func TestParseID(t *testing.T) {
	cases := map[string]struct {
		in     string
		wantOK bool
	}{
		"ok":         {in: "42", wantOK: true},
		"zero":       {in: "0", wantOK: false},
		"negative":   {in: "-3", wantOK: false},
		"nonnumeric": {in: "abc", wantOK: false},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			_, err := ParseID(tc.in)
			if tc.wantOK && err != nil {
				t.Fatalf("expected OK, got err=%v", err)
			}
			if !tc.wantOK && !errors.Is(err, ErrInvalidID) {
				t.Fatalf("expected ErrInvalidID, got %v", err)
			}
		})
	}
}

From a coverage point of view, you did an important thing: the “red” error branches become green not because “green is required”, but because you documented the behavior contract for bad input.

6. Coverage vs. fake tests

Sometimes people try to “raise coverage” and start writing tests like “called it and it didn’t crash”. It works like a painkiller: it feels like it got better, but the disease (lack of assertions) is still there.

Let’s take TitleForList and write a proper test that verifies the format:

package task

import "testing"

func TestTitleForList(t *testing.T) {
	if got := TitleForList("buy milk", false); got != "[ ] buy milk" {
		t.Fatalf("got %q", got)
	}
	if got := TitleForList("buy milk", true); got != "[x] buy milk" {
		t.Fatalf("got %q", got)
	}
}

This test makes coverage a tiny bit higher (yes, the branches executed), but the main point is that it fixes a UX contract: exactly how we show a task in the list.

A good internal question when you see a green line in coverage: “Is this line green because we actually verified it, or because we merely walked through it?” If it is the latter—that green is plastic.

7. Determinism and disciplined work with coverage

Determinism: sorting and a test for stable output

There is a classic source of “weird” issues: map. You already know that iteration order over a map in Go is not guaranteed. That means output built by iterating over a map can “dance” between runs. And here coverage does not always help directly (the code can be covered), but the practice of “critically covering stability” helps a lot.

Suppose our reporting layer builds text like:

done=3
undone=5

And we store counters in a map[string]int. Let’s make a function that guarantees a stable key order:

package report

import (
	"sort"
	"strconv"
	"strings"
)

func StablePairs(m map[string]int) string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var b strings.Builder
	for _, k := range keys {
		b.WriteString(k + "=" + strconv.Itoa(m[k]) + "\n")
	}
	return b.String()
}

And now the test: it is not about “covering lines”, it is about fixing the determinism of the result.

package report

import "testing"

func TestStablePairs(t *testing.T) {
	m := map[string]int{"undone": 5, "done": 3}
	got := StablePairs(m)
	want := "done=3\nundone=5\n"

	if got != want {
		t.Fatalf("got:\n%q\nwant:\n%q", got, want)
	}
}

And this is truly “critical coverage”, because it protects you from flaky tests and from surprising behavior in CLI/scripts.

How to interpret coverage without fanaticism

When you start looking at HTML reports, you will fairly quickly see odd things: some lines are red but “it seems impossible to get there”; some parts are not colored the way you expected; sometimes counters (in other modes) look unintuitive.

First, coverage is “not an exact science” (in a good sense): the tool works by rewriting code and counts coverage by instructions/blocks, not by “your mental model of lines”. So it can show results that feel strange at first.

Second, coverage has different modes. The most basic one is set (“executed or not”), there is count (“how many times executed”), and there is atomic (similar to count, but with guarantees for parallel execution). At this stage, set is almost always enough: it answers the main question “do tests go here at all?”

Third, red is sometimes actually fine. For example, the code may contain defensive “just in case” branches that are in fact unreachable under current contracts, or branches for specific OS/architectures (via build tags). In such cases you do not need to force a test into that branch at any cost. It is much more important to honestly understand: “why is it red?” and “do I want it to exist at all?”

Coverage as a hint in a quality gate

As part of a quality gate, it is useful to treat coverage not as a KPI, but as a habit of asking a couple of correct questions during review. If you added a new if branch and did not add tests, coverage in the report will often show a red chunk right there. This is a very convenient “beacon”: not because the percentage dropped, but because you introduced new behavior without a check.

Another healthy habit: if you fix a bug—especially in error handling or an edge case—a good test for that bug almost always increases coverage exactly in the area that used to be “dark”. And that is a very pleasant feedback loop: coverage helps confirm that you really wrote a test that goes down the path that previously did not execute.

And if you see a huge red chunk in the middle of an “important” function, it sometimes means the function is too large and does too much. Coverage here acts as an indirect signal for refactoring: splitting into small functions is easier than trying to “punch through” all branches in one monster.

8. Typical mistakes when working with coverage

Mistake #1: treating coverage as proof of correctness.
Sometimes it feels like: “we have 90%—so there are almost no bugs”. But coverage measures the fact that code executed, not the quality of checks. You can execute a branch and assert nothing about the result. The right habit is to look at the test and ask: what exactly does it guarantee, what contract does it fix, and how will it fail on regression?

Mistake #2: chasing 100% and writing tests for the sake of green lines.
Chasing the percentage often leads to tests that mirror the code instead of verifying behavior, and to testing “internals” instead of the contract. As a result, tests become brittle: any implementation improvement breaks tests even though behavior did not change. It is better to cover critical branches (errors, boundaries, public functions) than to pad the percentage with “decorative” checks.

Mistake #3: not covering error branches because “they are rare”.
In reality, errors and bad input are the most frequent visitors. If tests verify only the happy path, you get a false sense of safety: everything is green while the world is kind. As soon as the world is not kind, the application starts behaving strangely. A coverage report often shows such places directly: red if err != nil { ... } is almost always a signal: “a test is needed here”.

Mistake #4: confusing “executed” with “verified”.
A test like Do(); _ = result makes lines green but does not protect you. If you did not compare the result to an expected value, did not check errors.Is, did not check length/content—the test does not catch regressions. A good discipline: in every test there must be a moment where you explicitly say “I expect this”.

Mistake #5: ignoring nondeterminism and getting flaky tests despite high coverage.
You can have great coverage and still have unstable tests if output depends on map iteration order or on the current time. Coverage will not save you: it will show green execution, but it will not guarantee repeatability. That is why it is critical to cover determinism: sort before output, fix time where needed, and verify a stable result.

1
Task
Go SELF, level 38, lesson 1
Locked
Standby greeting
Standby greeting
1
Task
Go SELF, level 38, lesson 1
Locked
Final price
Final price
1
Task
Go SELF, level 38, lesson 1
Locked
Port from a string
Port from a string
1
Task
Go SELF, level 38, lesson 1
Locked
Stable report
Stable report
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION