Discover testscript. Discover gold.

Posted: 18 August, 2023 Category: backend Tagged: go

So golang devs had an internal testing tool called testscript, for testing... you guessed it, go itself. But like some other go-internal tools, it was so useful that it finally got excised and released into the wild, so that everyone else can use it, and it turns out to be particularly nifty for testing cli programs. The specific package to "go get" is this one.

Backstory: I had decided to transmogrify my go program into a standalone cli tool. I quickly realized it wasn't obvious how to test the actual, commandline invocations of the executable, for system tests. After a bit of googling, I finally ran into testscript.

Now, you can read the official docs, but I'd encourage you to head towards friendlier writing on the topic (and on go more generally), over at bitfield consulting's introduction to testscript.

So what does testscript do?

In a nutshell, testscript allows you to:

  1. Provide a method (via testscript.RunMain()) that invokes your main() application entrypoint, so that it can build an executable out of it (one less step for you to worry about!).

  2. Set some config params (via testscript.Run) including, crucially, where it should look for test scripts to run.

    emoji-warning Be prepared to refactor main() slightly so that its guts are actually handled by a method that both a hollowed-out main() AND a test script, can call.

So what does a test script in the land of testscript look like?

These test scripts (which you will write) all end in the suffix .txtar and have their own mini syntax that testscript understands:

  • Each line begins with a keyword corresponding to an assertion.
  • The assertions allow you to check for the existence of files, compare files, inspect stdout and stderr, determine whether the executable ran successfully or not, etc.

The most trivial of examples:

# expect hello world output and no errors
exec echo 'hello, world!'
stdout hello, world!
! stderr .

emoji-heart Honestly... it feels so lightweight and straigtforward after dealing with things in js-world (jest, mocha, chai, all the drinks). emoji-heart_eyes

That said, I'm sure the go version of such finnicketies will reveal themselves in time, once I start doing anything more involved than sniffing for outcomes. emoji-sweat_smile

Where do you put all this stuff?

It took me a really long time to decide on a project structure for my cli program, especially with this introduction of test files and new entry points. Go isn't very rigid about such things, which just adds an air of mystery to things. That said, I did find a basic guide about conventions that seem to have evolved over time. The highlights for me were:

  • the cmd folder, which more or less holds applications. This is confusing a bit until you realise that a lot of cli programs have sub-commands that are, or could be, applications in and of themselves. Think of docker compose or kubectl apply.
  • the pkg folder seems to be the equivalent of an open house... it more or less invites others to pilfer the code therein and reuse it elsewhere. emoji-smile
  • the internal folder is more or less the opposite: for stuff others shouldn't use directly, because the authors aren't expecting it to be used that way / it isn't necessarily designed for external use cases.

So, I think I'm going to go with the following organising principle for now as I only have one app in this repository.

cmd/
  |-myapp/
      |-subcommand1.go
      |-subcommand2.go
      |-main.go
testdata/
  |-scripts/
      |-this.txtar
      |-that.txtar
internal/
  |-foo.go
  |-bar.go
myapp.go
myapp_test.go

If I discover a better organising principle, I'll be happy to pivot as necessary. Lots of cool discoveries just from running into a need for testscript! emoji-thumbsup