Strive for Platform-Independent Tests

Write platform-independent tests whenever possible.

This suggestion is corollary to and supports the previous two guidelines. Maintenance of multi-platform code is not a trivial business, and has its cost. By making sure that the code is designed to be as much as possible independent of the platform it's run on, and only really low-level parts do differ, we have hopefully reduced the cost. But the code has to be tested, and since tests are also code, the same rules apply.

Platform-independent approach to tests works best in a case where the result of an operation or behaviour must be the same across all platforms despite different low-level implementation details. In such a case, the low-level code does not necessarily need to be covered, as long as tests are run against all target platforms.

To illustrate this, let's slightly update our example. We'll add a feature that has to work uniformly across all platforms, but will have three different implementations. Let's say we need to add a fourth feature which prints "this feature has different implementations but yields the same result" on all three supported operating systems (here, as in GOOS).

This is what the code looks like:

  • this is our platform-independent high-level code, something.go:
package something

import (
	"fmt"
)

const (
	Feature1 = "common feature for all platforms"
)

func RunFeature4() {
	fmt.Println(Feature4())
}

func Feature4() string {
	return doFeature4()
}

Notice the RunFeature4 function which internally calls the doFeature4 function. Where is it declared? It's not declared in any file that is visible for all platforms simultaneously. Instead, each of the platforms has its own implementation. Here we go:

  • for darwin, something_darwin.go:
package something

import (
	"fmt"
)

var (
	feature4 = []byte(`this feature has different implementations but yields the same result`)
)

func DoFeature3() {
	fmt.Println("this is a unique feature for the mac")
}

func doFeature4() string {
	return string(feature4)
}

Here, the special implementation of doFeature4 uses the slice of bytes as the source.

  • for linux, something_linux.go:
package something

func DoFeature3() {}

func doFeature4() string {
	return "this feature has different implementations but yields the same result"
}

In the world of Linux, things are often more straightforward. We just return a string.

  • here is windows, something_windows.go:
package something

const (
	Feature2 = "this is a windows specific version"
)

var (
	feature4 = []rune(`this feature has different implementations but yields the same result`)
)

func DoFeature3() {}

func doFeature4() string {
	return string(feature4)
}

In Windows, our implementation is based on a slice of runes.

  • finally, this is how the feature is used, main.go:
package main

import (
	"fmt"

	"../../something"
)

func main() {
	fmt.Println(something.Feature1)
	fmt.Println(something.Feature2)

	something.DoFeature3()
	something.RunFeature4()
}
  • building and running:
# Darwin.
$ GOOS=darwin go build -o bin/myapp-darwin ./cmd/myapp
$ ./bin/myapp-darwin
common feature for all platforms
this is a version supported by a number of unix-like systems
this is a unique feature for the mac
this feature has different implementations but yields the same result

# Linux.
$ GOOS=linux go build -o bin/myapp-linux ./cmd/myapp
$ docker run -it --rm -v $(pwd)/bin:/root/bin/ alpine:3.13 ash
% /root/bin/myapp-linux
common feature for all platforms
this is a version supported by a number of unix-like systems
this feature has different implementations but yields the same result

The output suggests that the feature works as expected. But how can we prove it remains working over time, especially given that changes to software are made often, not to mention that platforms do not stand still too, and are being constantly updated? Of course, with tests. In this case, the outcome is and must be exactly the same for all platforms. So this outcome is the result we want to make sure is always correct, not the implementation detail. If something happens to the implementation, then the broken test will highlight it.

In cases like this, write platform-independent tests.

To illustrate this situation, here are two example tests:

  • the first checks the feature itself
  • the second tests how it works end-to-end.

Here is the something_test.go file, which is in effect on all platforms:

package something_test

import (
	"bufio"
	"os"
	"strings"
	"testing"

	"../something"
)

func TestFeature4(t *testing.T) {
	exp := "this feature has different implementations but yields the same result"
	act := something.Feature4()

	if act != exp {
		t.Errorf("expected: %s, got: %s\n", exp, act)
	}
}

func TestRunFeature4(t *testing.T) {
	stdout := os.Stdout
	r, w, err := os.Pipe()
	if err != nil {
		t.Errorf("failed to create reader and writer: %s\n", err)
		t.FailNow()
	}

	os.Stdout = w
	something.RunFeature4()

	b := bufio.NewReader(r)
	out, err := b.ReadString('\n')
	if err != nil {
		t.Errorf("failed to read from reader: %s\n", err)
		t.FailNow()
	}

	if err := w.Close(); err != nil {
		t.Errorf("failed to close writer: %s\n", err)
		t.FailNow()
	}

	os.Stdout = stdout

	exp := "this feature has different implementations but yields the same result"
	if act := strings.TrimSuffix(out, "\n"); act != exp {
		t.Errorf("expected: %q, got: %q\n", exp, act)
	}
}

Notice how neither of the two tests has any knowledge about the platform-specific code. In the first test, TestFeature4, we check that the feature is implemented correctly. The second test, TestRunFeature4, does a full check, including capturing the result from the standard output.

Here are test results:

  • on macOS:
$ go test ./something/... -v
=== RUN   TestFeature4
--- PASS: TestFeature4 (0.00s)
=== RUN   TestRunFeature4
--- PASS: TestRunFeature4 (0.00s)
PASS
ok  	_/Users/user/projects/xplatform/something	0.005s
  • and on Linux, using the golang:1.15 image:
$ docker run -it --rm -v $(pwd):/root/xplatform golang:1.15 bash
% cd /root/xplatform/
% go test ./something/... -v
=== RUN   TestFeature4
--- PASS: TestFeature4 (0.00s)
=== RUN   TestRunFeature4
--- PASS: TestRunFeature4 (0.00s)
PASS
ok  	_/root/xplatform/something 0.003s

The output is exactly the same, and tests pass in both cases. And that's what we should strive for.