From packages to commands
Happiness is a one-line main
function
This is the second part of a two-part tutorial on designing Go packages, guided by tests:
- Write packages, not programs
- From packages to commands
This test is fine?
In Part 1, we talked about what it means to develop software as importable packages, not just one-off programs, and we used the example of printing “Hello, world”. Pretty simple, right? But it presents us with some interesting problems when we try to package this behaviour as a reusable software component.
Right from the start, we’re thinking about our package as a contribution to the universal library of Go software, and that starts with testing. So here’s our first attempt at a test for the hello.Print
function:
package hello_test
import (
"hello"
"testing"
)
func TestPrintPrintsHelloMessageToTerminal(t *testing.T) {
.Parallel()
t.Print()
hello}
This looks reasonable, and it does call our function. But there’s a problem: how could this test fail?
Tests are bug detectors
Go tests pass by default, so unless we take some specific action to make it fail (we’ll see how to do that in a moment), this test will always pass. That might sound great, at first: we like passing tests!
But how useful is a test that can never fail? What is the point of a test, actually? Why do we write them? Partly to help us design the API of the system under test, as we’ve seen, but the most important role of any test is as a bug detector.
To see how that works, let’s think about potential bugs that could be in our hello.Print
function. There are many, of course: it could print the wrong message, print the right message in the wrong language, print nothing but delete all your files, and so on.
These would all be important bugs, and we’d like to be able to detect and fix them, but let’s strip it right back to basics. The most fundamental bug that could be in Print
is that it doesn’t do anything at all.
For example, suppose the implementation looked like this:
package hello
func Print() {}
The question we need to ask ourselves here is “would our test detect this bug?” In other words, will the test fail when Print
does nothing? Well, we already know the answer to that. The test can’t fail at all, so it can’t detect any bugs, including this one.
So when should a test fail?
Most tests have one or more ways of failing under specific circumstances. In other words, we have some code in the test like this:
if CONDITION {
.Errorf("test failed because %s", reasons)
t}
So what would CONDITION
be in our case? It’s not easy to work out at first. Let’s remind ourselves what the desired behaviour of Print
is. Its job is to print a hello message to the terminal. Fine, but what does that mean specifically?
In the original version of the program, we called fmt.Println
to do this job. So how does that work? Let’s take a look at the source code.
In most editors, such as Visual Studio Code, you can place the cursor on some function call like this, right-click, and select Go to definition to see how it’s implemented (or you can just hold down the Cmd key and click on the function name).
This is helpful for navigating our own projects, but it works with imported packages too, even packages like fmt
that are part of the standard library. This code spelunking is fun and interesting, and I recommend you spend some time exploring the standard library, or even packages from the universal library that interest you.
Where does fmt.Println
print to?
Here’s what we find when we spelunk into the definition of fmt.Println
:
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
It seems that there’s a more general function, fmt.Fprintln
. Its first argument is, we find, an io.Writer
, representing the destination to “print” to, and the other arguments are the data to be printed.
Where does the text go when we call fmt.Println
, then? The answer is os.Stdout
, which is a file handle representing the standard output. If our program is being run from a terminal, then, its standard output will be that terminal, which explains why we see the “Hello, world” message there.
In the real program, that is indeed where we want the message to go. But the problem with testing our Print
function is that there’s no way to “inspect” what’s been printed to os.Stdout
. Anything we print to it can be seen by the user, but not the program. What could we do?
Well, perhaps the answer already occurred to you. If we can’t test what got printed to os.Stdout
, then don’t print there: print somewhere else! Since Fprintln
accepts any object that implements io.Writer
, could we construct our own?
Meet bytes.Buffer
, an off-the-shelf io.Writer
Like all good interfaces, io.Writer
is easy to implement, because it’s small. All we need is something with a Write
method.
For example, we could create some struct type whose Write
method stores the supplied text inside the struct, and we could add a String
method so that we can retrieve that text and inspect it in the test.
That’s not necessary, though, because such a type already exists in the standard library: bytes.Buffer
. It’s an all-purpose io.Writer
that remembers what we write to it, so it’s ideal for testing. Because a buffer is such a helpful friend, we often affectionately name it just buf
for short.
Let’s rewrite our test using a buffer as the print destination, then:
func TestPrintTo_PrintsHelloMessageToGivenWriter(t *testing.T) {
.Parallel()
t:= new(bytes.Buffer)
buf .PrintTo(buf)
hello:= "Hello, world\n"
want := buf.String()
got if want != got {
.Errorf("want %q, got %q", want, got)
t}
}
New behaviour, new name
The first thing to note here is that the test name has changed. That makes sense, because we’ve changed our definition of the way the program is supposed to behave. gotestdox
reports:
✔ PrintTo prints hello message to given writer
We begin with the standard t.Parallel()
prelude, and then, since we’ll need a bytes.Buffer
as our printing destination, we create one and name it buf
.
Now comes the call to the function under test. Since the behaviour of the function has changed, its name should change too. It now takes an io.Writer
argument to tell it where to print its message to, so let’s rename it PrintTo
. And that reads quite naturally in the code:
.PrintTo(buf) // prints hello message to buf hello
Indeed, this is probably the easiest way to find a good name for things: write the code that uses it, and see what makes sense in context. Then use that name. Again, this usually works better than deciding in advance what the name should be, and then trying to jam it awkwardly into code where it doesn’t fit.
Unlike the first version of this test, we can now write down a condition under which the test should fail. First, we know what message we want to see printed to buf
; it’s “Hello, world”. Let’s name that variable want
.
What we actually got, though, is the result of calling buf.String()
, which returns all the text written to buf
since it was created. We’ll name this value got
.
And now we know the failure condition: if got
is not equal to want
, then we should fail. Since that can now happen, we have a useful test at last.
Implementing hello
, guided by tests
So, how close are we to having this test pass? Well, that would obviously require PrintTo
to exist, and it doesn’t yet, even though we’ve done a lot of useful thinking about it. And we’re still not quite ready to write it, because there’s one thing we need first. We need to see our test fail.
If you think that sounds weird, I don’t blame you. A failing test is usually bad news—and it would be, if we were under the impression that we’d successfully implemented the PrintTo
function.
But we know we haven’t, so if we can get the test to run at all, it should fail, shouldn’t it? The alternative would be that it passes, even though the PrintTo
function does nothing whatsoever. That would be weird.
We agree, I hope, that if PrintTo
does nothing, then that’s a bug. Would we detect that bug with this test? Yes, I think we would. Running this test against an empty PrintTo
function should fail. Let’s see why.
If PrintTo
doesn’t write anything to the buffer, then when we call buf.String()
we’ll get the empty string. That won’t be equal to want
, which is the string “Hello, world”.
That mismatch should fail the test, and it should also give us some confidence in our bug detector. No doubt there could be other bugs in PrintTo
that we won’t detect at the moment. That’s nearly always true. But we feel at least we should detect this one, so let’s find out.
Creating a module
We can’t actually run the test just yet, though. At the moment, running go test
gives us a compile error:
go: cannot find main module
This makes sense, since we haven’t yet created a go.mod
file to identify our module. A module in Go is a collection of (usually related) packages. While a module can contain any number of packages, it’s a good idea to keep things simple and put each of our packages in its own module.
We’ll do that with the hello
package, so we’ll name its containing module hello
too. To do that, we need a little bit of paperwork.
Each Go module needs to have a special file named go.mod
that identifies it by name. It’s just a text file, so we could create it by hand, but an easier way is to use the go
tool itself:
go mod init github.com/bitfield/hello
go: creating new go.mod: module github.com/bitfield/hello
One folder, one package
But we still have a problem when running go test
, because our old main.go
file from listing hello/1
is still kicking around in this folder:
found packages hello (hello_test.go) and main (main.go)
The rule in Go is that each distinct package must occupy a separate folder. That is to say, you can’t have a file that declares package hello
in the same folder as another file that declares package main
, for example. And right now that’s what we have.
We still need a main
package somewhere, because we’re going to build an executable binary. But this folder ain’t big enough for both main
and the hello
package to co-exist: one of them must move to a subfolder.
It doesn’t really matter which one we move, but it’s worth asking: which of these two packages is the more important? hello
contains all the substantive code in our program, and it’s the package that other users will be importing. By contrast, main
does almost nothing at all. It’s a very thin shim around the hello
package which is required only to make it executable, and no one else will ever want to refer to the main
package except us.
It seems logical to me that the more important package should be at the top level, that is, the root folder of the repository. Subordinate packages should go in subordinate folders. And since hello
is clearly the boss, it should occupy the penthouse. Let’s move main.go
to a subfolder, then:
mkdir -p cmd/hello
mv main.go cmd/hello
It doesn’t matter what we call the subfolder, but there’s a loose convention among Gophers that code implementing “commands” (that is, executable binaries) is placed under a folder named cmd
.
A null implementation
Let’s try go test
again:
go build hello: no non-test Go files
That’s absolutely correct. In order to run tests, there must be some package to build, and there isn’t. So let’s create a hello.go
file that declares package hello
. What else shall we put in it?
Well, we need the PrintTo
function to be at least defined, or our test won’t compile. On the other hand, we don’t want it to actually do anything yet, because we want to verify that our test can tell when it doesn’t. So let’s write a null implementation: just the same empty function we saw before.
package hello
func PrintTo(w io.Writer) {}
For it to compile, the function must take a parameter, and we know its type should be io.Writer
. Nothing else is actually required, since Go is quite happy for you to write empty functions that do nothing. But we think the test should fail, so let’s see what happens:
--- FAIL: TestPrintTo_PrintsHelloMessageToGivenWriter (0.00s)
hello_test.go:16: want "Hello, world", got ""
Nice! That’s exactly what we hoped for, even though it looks like something bad happened. Don’t think of it as a failing test: think of it instead as a succeeding bug detector. We know there is a bug, so if the test passed at this stage, we would have to conclude that our bug-detecting machinery is faulty.
The real implementation
The test is doing one half of its job: detecting when the function doesn’t work. The other half of its job is to detect when the function does work. So does it? Let’s write the real implementation of PrintTo
and find out:
func PrintTo(w io.Writer) {
.Fprintln(w, "Hello, world")
fmt}
Here’s the result:
go test
PASS
Ideal. We now have an importable, testable package that makes our “print hello to some io.Writer
” behaviour available to other users, provided that we remember to publish it with an appropriate software licence.
Refactoring to use our new package
While we wait for the world to rush to our door to take advantage of our generous contribution to the Go universal library, let’s use the new package ourselves to rebuild our hello
command.
In fact, we only need to make minor changes to our main
package from listing hello/2
. Specifically, we now need to pass in the destination for PrintTo
to print to:
package main
import (
"os"
"github.com/bitfield/hello"
)
func main() {
.PrintTo(os.Stdout)
hello}
Let’s run it and see if that works:
go run cmd/hello/main.go
Hello, world
This is great. We’re back precisely where we started. From one point of view, we’ve added no value at all, since the program’s behaviour is exactly the same. But from another point of view, we’ve added all the value in the world, since we created an importable package.
Someone else who wants this behaviour can now add it to their own program as a pluggable component. And because we’ve made it flexible about where it prints to, that program needn’t even be a command-line tool. It could be, for example, a web server:
func main() {
.Println("Listening on http://localhost:9001")
fmt.ListenAndServe(":9001", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.PrintTo(w)
hello}))
}
Going further
Here’s a mini-project for you to practice building an importable package, guided by tests, in the way that we’ve talked about.
Instead of a program that simply prints “Hello, world”, try writing a program that asks the user for their name, then prints “Hello, [NAME]”. Put all the substantive behaviour of the program in an importable package that’s used by
main
.You’ll find one possible solution in listing
greet/1
.