Go has a great approach to testing — simplicity. Writing and running tests in Go is super easy, easier than in many other languages.

Unit tests live next to the code they test. This is obvious, and I don’t want to change anything about it. Unit tests should be automated and run as part of the CI pipeline.

I want to talk about higher-level tests, for example, integration testing.

In this area, the situation gets complicated because some things just cannot be automated, especially when working with embedded devices. Automated testing seems like an all-or-nothing situation. I can SSH into a device and test some things. I can connect over a serial port and test something. But what if I need to switch the power off? Then I am looking at investing in hardware that is made only to facilitate testing. Of course, in a perfect world, I would buy as many relays as I want, connect them to some fancy remote-controlled I/O equipment, and programmatically switch the relays. But in reality, this kind of investment is not feasible in the short term.

Is there a middle ground, where I am still able to run most of the automated tests, but for the things that cannot be automated, I can fall back to manual action?

Sure, we have test cases in test tracking software. I can go to some website or Excel, write down what needs to be done in a test, and repeat it as many times as I want. I can say that in the middle of the test I should run an automated test for some parts of the system. But that approach is tedious. It is really annoying having to click my way through tests.

How about semi-automating manual tasks? How about mocking the behaviors with instructions to an operator? For example, at the beginning of the test, a prompt is displayed and says to switch on the device and press Enter. The test will “think” that it powers up the device, but in reality, the operator will be doing it. Sounds like an interesting idea.

Of course, this kind of testing cannot be integrated with the CI pipeline, but neither can the Excel test plan.

Sample Implementation

The Code

Printing text and scanning it from the console is the simplest way to interact with the user. In this example, I wrote a helper function and used it in tests:

package demo_test

import (
    "fmt"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func PromptString(prompt string) (string, error) {
    fmt.Printf("%s\n> ", prompt)
    var input string
    _, err := fmt.Scanln(&input)
    if err != nil {
        return "", err
    }
    return input, nil
}

func TestYesNo(t *testing.T) {
    a, err := PromptString("Is this a great thing?")
    require.NoError(t, err)
    assert.Equal(t, "yes", a)
}

func TestAdd(t *testing.T) {
    a, err := PromptString("What is 2 + 2?")
    require.NoError(t, err)
    assert.Equal(t, "4", a)
}

Running It

The Problem

Unfortunately, a simple go test ./... is not going to work. The go test command assumes that everything is automated and there is no need for user interaction. It kind of breaks the whole idea, unless we are willing to use workarounds.

Workarounds

I can think of many workarounds for that problem, and I will describe one in more detail.

The idea is to not run go test in a way that runs tests, but to use it to build the binary that has all the tests.
It can be done using the -c flag.

This will build the binary:

go test -c -o demo_test ./demo_test.go

And now we can run it:

./demo_test

It is very similar to a regular test, but this time we can use the TTY:

$ ./demo_test 
Is this a great thing?
> yes
What is 2 + 2?
> 4
PASS

You can read more about the go test command in the documentation.