unit testing OS specific code

789 views
Skip to first unread message

Brieuc Jeunhomme

unread,
Jan 4, 2022, 12:30:09 PM1/4/22
to golang-nuts
Hi,

I'm writing code that uses the golang.org/x/sys/windows/svc package.  This package compiles only for windows builds, for other GOOS values, I get a build error.  That's fair, but I'm wondering how to unit test the code that calls svc functions when working on a non-windows environment.  So code that looks like:

func myFunction() {
    svc.Foo(make(chan svc.Bar))
}

Is there a well known way to do that, or am I missing something

Dan Kortschak

unread,
Jan 4, 2022, 2:51:25 PM1/4/22
to golan...@googlegroups.com
You can use build constraints, for example `// +build windows` and `//
+build !windows` to selectively build the tests only on the desired
platform[1].


[1]https://pkg.go.dev/cmd/go#hdr-Build_constraints



Brieuc Jeunhomme

unread,
Jan 5, 2022, 5:08:52 AM1/5/22
to golang-nuts
Yes, that's what I wanted to do, but it looks very tedious at best.  In "normal" unit tests, it's simple: import the package you're using, inject its functions, reuse its types and constants.

But here ,since under non-windows platforms, I can't import the package, it means I have to define wrappers for everything I use: types, constants, functions.  And when a function gets a channel in argument, the wrapper is much more than a trivial one liner, it often needs a goroutine.

For example, let's say you want to unit test MyFunction, which calls foo.Bar(chan foo.Baz, chan foo.Foobar) and foo compiles only under a single OS.  If you want to write a portable unit test for MyFunction, you can't import foo.  So you have to wrap everything in foo.  And since you can't have the wrappers take a foo.Baz or foo.Foobar argument, the wrapper for foo.Bar will be non-trivial, it will require to read/write from both channels, convert whatever comes in/out of them into your wrapper type... That's a lot of manual work, and it's error prone.  I suppose there's a better way to do that than to wrap everything manually?

Jan Mercl

unread,
Jan 5, 2022, 5:20:47 AM1/5/22
to Brieuc Jeunhomme, golang-nuts
On Wed, Jan 5, 2022 at 11:09 AM Brieuc Jeunhomme <bjeun...@gmail.com> wrote:

> But here ,since under non-windows platforms, I can't import the package, it means I have to define wrappers for everything I use: types, constants, functions.

I don't get it. Put the common code in a file/in files with no build
constraints and the target specific bits in files with build
constraints for the target. What wrappers are needed and why?

Package level declarations are visible in all files within a package -
meaning they have package scope. The sole exception is the package
qualifier explicitly or implicitly declared by an import statement.
Such identifier has file scope.

Brieuc Jeunhomme

unread,
Jan 5, 2022, 5:32:03 AM1/5/22
to golang-nuts
So:
$ cat mypackage.go
package mypackage

import "something/something/foo"

func MyFunction() {
  some code
  some code
  some code
        foo.Bar(make(chan foo.Baz), make(chan foo.Foobar))
}

Is the code I would really like to write.

foo has build constraints.  As you can see, my code doesn't, it's the exact thing you recommended: the common code is in mypackage.go, the platform specific code is in foo (which I don't control, it isn't my code, so I can't change it).  How to unit test MyFunction to verify that it uses foo.Bar correctly?

Normally, I would just pass foo.Bar in argument to the function or do something like var fooBar = foo.Bar so I can replace it in unit tests.  But in the case of a foo package that has platform build constraints, I don't see how it's possible without creating a wrapper.

Axel Wagner

unread,
Jan 5, 2022, 5:33:25 AM1/5/22
to Brieuc Jeunhomme, golang-nuts
I'm also confused.

On Wed, Jan 5, 2022 at 11:09 AM Brieuc Jeunhomme <bjeun...@gmail.com> wrote:
For example, let's say you want to unit test MyFunction, which calls foo.Bar(chan foo.Baz, chan foo.Foobar) and foo compiles only under a single OS.  If you want to write a portable unit test for MyFunction, you can't import foo. 

To me, there are two possibilities:
1. MyFunction is portable, i.e. it contains build-tag guarded implementations for the other relevant platforms as well, which work without importing `foo`. In that case, you can just call it as normal, it doesn't actually matter to its API or test.
2. MyFunction is not portable, it only works on certain platforms. In that case, why would you need a portable unit test? It can only be called on platforms `foo` works on, so there is no need to test it on any other platform.

Obviously, you have to actually *run* the tests on the platforms which are supported, but that seems a given.
 
So you have to wrap everything in foo.  And since you can't have the wrappers take a foo.Baz or foo.Foobar argument, the wrapper for foo.Bar will be non-trivial, it will require to read/write from both channels, convert whatever comes in/out of them into your wrapper type... That's a lot of manual work, and it's error prone.  I suppose there's a better way to do that than to wrap everything manually?

On Tuesday, January 4, 2022 at 7:51:25 PM UTC kortschak wrote:
On Tue, 2022-01-04 at 08:29 -0800, Brieuc Jeunhomme wrote:
> Hi,
>
> I'm writing code that uses the golang.org/x/sys/windows/svc package.
> This package compiles only for windows builds, for other GOOS values,
> I get a build error. That's fair, but I'm wondering how to unit test
> the code that calls svc functions when working on a non-windows
> environment. So code that looks like:
>
> func myFunction() {
> svc.Foo(make(chan svc.Bar))
> }
>
> Is there a well known way to do that, or am I missing something

You can use build constraints, for example `// +build windows` and `//
+build !windows` to selectively build the tests only on the desired
platform[1].


[1]https://pkg.go.dev/cmd/go#hdr-Build_constraints



--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/30f195e2-bc40-468d-aeb7-eb19101a6762n%40googlegroups.com.

Axel Wagner

unread,
Jan 5, 2022, 5:39:54 AM1/5/22
to Brieuc Jeunhomme, golang-nuts
On Wed, Jan 5, 2022 at 11:32 AM Brieuc Jeunhomme <bjeun...@gmail.com> wrote:
So:
$ cat mypackage.go
package mypackage

import "something/something/foo"

func MyFunction() {
  some code
  some code
  some code
        foo.Bar(make(chan foo.Baz), make(chan foo.Foobar))
}

Is the code I would really like to write.

foo has build constraints.  As you can see, my code doesn't, it's the exact thing you recommended: the common code is in mypackage.go, the platform specific code is in foo (which I don't control, it isn't my code, so I can't change it).  How to unit test MyFunction to verify that it uses foo.Bar correctly?

By writing a normal test for it and running that on one of the platforms `foo` supports. You can't run it on any other platform, because it doesn't build on any other platform.
 
Normally, I would just pass foo.Bar in argument to the function or do something like var fooBar = foo.Bar so I can replace it in unit tests.

That's certainly possible. You would declare
var fooBar func(whatever) error
And then have a file `bar_foo.go` with

//go:build amd64|…

import "something/something/foo"

func init() {
    fooBar = foo.Bar
}

and a file `bar_nofoo.go` with

//go:bulid !amd64&…

func init() {
    fooBar = func(whatever) error { return errors.New("not implemented") }
}

But, personally, I see very limited benefit in this. It means your package will build, but error at runtime on platforms which are not supported by `foo`. And your tests will not test anything relevant - you can't verify that you are calling `foo.Bar` *correctly*, you can just verify that you call your mock with certain arguments which you guess are right. That's a change-detection test, it's not a good test.

Just import `foo`, call it as normal and run your tests on a platform which is actually supported by `foo`. And/or write an implementation that works on other platforms as well, without importing `foo`.
 
  But in the case of a foo package that has platform build constraints, I don't see how it's possible without creating a wrapper.

On Wednesday, January 5, 2022 at 10:20:47 AM UTC Jan Mercl wrote:
On Wed, Jan 5, 2022 at 11:09 AM Brieuc Jeunhomme <bjeun...@gmail.com> wrote:

> But here ,since under non-windows platforms, I can't import the package, it means I have to define wrappers for everything I use: types, constants, functions.

I don't get it. Put the common code in a file/in files with no build
constraints and the target specific bits in files with build
constraints for the target. What wrappers are needed and why?

Package level declarations are visible in all files within a package -
meaning they have package scope. The sole exception is the package
qualifier explicitly or implicitly declared by an import statement.
Such identifier has file scope.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Jan Mercl

unread,
Jan 5, 2022, 5:58:49 AM1/5/22
to Brieuc Jeunhomme, golang-nuts
On Wed, Jan 5, 2022 at 11:32 AM Brieuc Jeunhomme <bjeun...@gmail.com> wrote:
>
> So:
> $ cat mypackage.go
> package mypackage
>
> import "something/something/foo"
>
> func MyFunction() {
> some code
> some code
> some code
> foo.Bar(make(chan foo.Baz), make(chan foo.Foobar))
> }
>
> Is the code I would really like to write.
>
> foo has build constraints. As you can see, my code doesn't, it's the exact thing you recommended: the common code is in mypackage.go, the platform specific code is in foo (which I don't control, it isn't my code, so I can't change it). How to unit test MyFunction to verify that it uses foo.Bar correctly?

If the foo package has build constraints such that it does not build
on all targets, so must have the file which imports it. Or it will,
transitively, not build on all targets as well.

Can you just special-case, ie. add build constraints to the *_test.go
files that import the foo package? Seems like a easy solution to me.

Asad Ur Rahman

unread,
Jan 5, 2022, 6:08:04 PM1/5/22
to golang-nuts
I think the build constraints works with directory names like 'windows' and 'unix', tags like `// +build windows`  or even the source code file name with postfix like 'abc_windows.go' . All works the same like the build tags. 
So if you build your package on linux for Windows it will compile but wont be able to run on linux. The Unit tests (like go test . ) does both, it compiles and runs it. May compile the unit tests with command (go test -v -c -o mytests . ). But you still would need windows to run the windows API specific low-level functions.
Reply all
Reply to author
Forward
0 new messages