
5 Advanced Go Testing Techniques
- Transfer
Salute to everyone! Less than a week is left before the start of the “Golang Developer” course and we continue to share useful material on the topic. Go!

Go has a good and reliable built-in library for testing. If you write on Go, then you already know that. In this article, we will talk about several strategies that can improve your testing skills with Go. From the experience of writing our impressive code base on Go, we learned that these strategies really work and thus help save time and effort in working with the code.
Use test suites
If you learn for yourself only one useful thing from this article, then it must be the use of test suites. For those who are not familiar with this concept, testing by kits is the process of developing a test to test a common interface that can be used on many implementations of this interface. Below you can see how we pass several different implementations
Lucky readers have worked with code bases that use this method. Often used in plug-in systems tests that are written to test an interface can be used by all implementations of this interface to understand how they meet the requirements of behavior.
Using this approach will potentially help to save hours, days, and even enough time to solve the problem of equality of classes P and NP. Also, when replacing one base system with another, the need to write (a large number) of additional tests disappears, and there is also confidence that this approach will not disrupt the operation of your application. Implicitly, you need to create an interface that defines the area of the tested area. Using dependency injection, you can customize a set from a package that is passed into the implementation of the entire package.
You can find a complete example here . Despite the fact that this example is far-fetched, one can imagine that one database is remote and the other is in memory.
Another cool example of this technique is located in the standard library in a package
Avoid Interface Pollution
You can’t talk about testing in Go, but don’t talk about interfaces.
Interfaces are important in the context of testing, since they are the most powerful tool in our testing arsenal, so you need to use them correctly.
Packages often export interfaces for developers, and this leads to the fact that:
A) Developers create their own mock for implementing the package;
B) The package exports its own mock.
Interfaces must be carefully checked before export. It is often tempting to export interfaces to give users the ability to simulate the behavior they need. Instead, document which interfaces suit your structures so as not to create a tight relationship between the consumer package and your own. A great example of this is the errors package .
When we have an interface that we don’t want to export, we can use internal / package subtreeto save it inside the package. Thus, we can not be afraid that the end user may depend on him, and, therefore, may be flexible in changing the interface in accordance with new requirements. Usually we create interfaces with external dependencies in order to be able to run tests locally.
This approach allows the user to implement their own small interfaces by simply wrapping some part of the library for testing. For more information on this concept, read the rakyl article on interface pollution .
Do not export concurrency primitives
Go offers easy-to-use concurrency primitives that can also sometimes lead to their overuse due to the same simplicity. First of all, we are concerned about the channels and the sync package. Sometimes it is tempting to export a channel from your package so that others can use it. In addition, a common mistake is embedding
If you export channels, you additionally complicate the life of the package user, which is not worth doing. As soon as the channel is exported from the package, you create difficulties when testing for the one who uses this channel. For successful testing, the user must know:
Take a look at the queue reading example. Here is an example library that reads from the queue and provides the user with a feed for reading.
Now your library user wants to implement a test for his consumer:
The user can then decide that dependency injection is a good idea and write their own messages in the channel:
Wait, what about the errors?
Now we need to somehow generate events in order to actually write to this stub, which replicates the behavior of the library we are using. If the library just wrote the synchronous API, then we could add all the parallelism to the client code, so testing becomes easier.
If you are in doubt, just remember that it is always easy to add parallelism to the consumer package (consuming package), and it is difficult or impossible to remove it after exporting from the library. And most importantly, do not forget to write in the package documentation whether the structure / package is safe for simultaneous access to several goroutines.
Sometimes it is still desirable or necessary to export the channel. In order to mitigate some of the problems mentioned above, you can provide channels through accessors instead of direct access and leave them open only for reading (
Use
Here is an example of the same test implemented in two ways. There is nothing grandiose here, but this approach reduces the amount of code and saves resources.
Perhaps the most important feature is that with the help
To see this principle in action, check out Marc Berger's article .
Use a separate package
Most of the tests in the ecosystem are in files
This strategy prevents fragile tests by restricting access to private variables. In particular, if your tests break and you use separate test packages, it is almost guaranteed that a client using a function that breaks in the tests will also break when called.
Finally, it helps to avoid import cycles in tests. Most packages are more likely to depend on other packages that you wrote besides the testing ones, so you will end up with a situation where the import cycle occurs naturally. An external package is located above both packages in the package hierarchy. Take an example from The Go Programming Language (Chapter 11 Section 2.4), where it
Now, when you use a separate test package, you may need access to unexported entities in the package where they were previously available. Some developers are faced with this for the first time when testing something based on time (for example, time.Now becomes a stub using a function). In this case, we can use an additional file to provide entities exclusively during testing, since files
What do you need to remember?
It is important to remember that none of the methods described above is a panacea. The best approach in any business is to critically analyze the situation and independently choose the best solution to the problem.
Want to learn more about testing with Go?
Read these articles:
Dave Cheney's Writing Table Driven Tests in Go
The Go Programming Language chapter on Testing.
Or watch these videos:
Hashimoto's Advanced Testing With Go talk from Gophercon 2017
Andrew Gerrand's Testing Techniques talk from 2014
We hope this translation has been useful to you. We are waiting for comments, and everyone who wants to learn more about the course, we invite you to open day , which will be held on May 23.

Go has a good and reliable built-in library for testing. If you write on Go, then you already know that. In this article, we will talk about several strategies that can improve your testing skills with Go. From the experience of writing our impressive code base on Go, we learned that these strategies really work and thus help save time and effort in working with the code.
Use test suites
If you learn for yourself only one useful thing from this article, then it must be the use of test suites. For those who are not familiar with this concept, testing by kits is the process of developing a test to test a common interface that can be used on many implementations of this interface. Below you can see how we pass several different implementations
Thinger
and run them with the same tests.type Thinger interface {
DoThing(input string) (Result, error)
}
// Suite tests all the functionality that Thingers should implement
func Suite(t *testing.T, impl Thinger) {
res, _ := impl.DoThing("thing")
if res != expected {
t.Fail("unexpected result")
}
}
// TestOne tests the first implementation of Thinger
func TestOne(t *testing.T) {
one := one.NewOne()
Suite(t, one)
}
// TestOne tests another implementation of Thinger
func TestTwo(t *testing.T) {
two := two.NewTwo()
Suite(t, two)
}
Lucky readers have worked with code bases that use this method. Often used in plug-in systems tests that are written to test an interface can be used by all implementations of this interface to understand how they meet the requirements of behavior.
Using this approach will potentially help to save hours, days, and even enough time to solve the problem of equality of classes P and NP. Also, when replacing one base system with another, the need to write (a large number) of additional tests disappears, and there is also confidence that this approach will not disrupt the operation of your application. Implicitly, you need to create an interface that defines the area of the tested area. Using dependency injection, you can customize a set from a package that is passed into the implementation of the entire package.
You can find a complete example here . Despite the fact that this example is far-fetched, one can imagine that one database is remote and the other is in memory.
Another cool example of this technique is located in the standard library in a package
golang.org/x/net/nettest
. It provides a means to verify that net.Conn is satisfying the interface.Avoid Interface Pollution
You can’t talk about testing in Go, but don’t talk about interfaces.
Interfaces are important in the context of testing, since they are the most powerful tool in our testing arsenal, so you need to use them correctly.
Packages often export interfaces for developers, and this leads to the fact that:
A) Developers create their own mock for implementing the package;
B) The package exports its own mock.
“The larger the interface, the weaker the abstraction”
- Rob Pike, Go Talks
Interfaces must be carefully checked before export. It is often tempting to export interfaces to give users the ability to simulate the behavior they need. Instead, document which interfaces suit your structures so as not to create a tight relationship between the consumer package and your own. A great example of this is the errors package .
When we have an interface that we don’t want to export, we can use internal / package subtreeto save it inside the package. Thus, we can not be afraid that the end user may depend on him, and, therefore, may be flexible in changing the interface in accordance with new requirements. Usually we create interfaces with external dependencies in order to be able to run tests locally.
This approach allows the user to implement their own small interfaces by simply wrapping some part of the library for testing. For more information on this concept, read the rakyl article on interface pollution .
Do not export concurrency primitives
Go offers easy-to-use concurrency primitives that can also sometimes lead to their overuse due to the same simplicity. First of all, we are concerned about the channels and the sync package. Sometimes it is tempting to export a channel from your package so that others can use it. In addition, a common mistake is embedding
sync.Mutex
without setting it to private. This, as usual, is not always bad, but it creates certain problems when testing your program. If you export channels, you additionally complicate the life of the package user, which is not worth doing. As soon as the channel is exported from the package, you create difficulties when testing for the one who uses this channel. For successful testing, the user must know:
- When data ends up being sent over the channel.
- Were there any errors when receiving data.
- How does a packet flush the channel after completion, if flush at all?
- How to wrap a package API so that you don’t call it directly?
Take a look at the queue reading example. Here is an example library that reads from the queue and provides the user with a feed for reading.
type Reader struct {...}
func (r *Reader) ReadChan() <-chan Msg {...}
Now your library user wants to implement a test for his consumer:
func TestConsumer(t testing.T) {
cons := &Consumer{
r: libqueue.NewReader(),
}
for msg := range cons.r.ReadChan() {
// Test thing.
}
}
The user can then decide that dependency injection is a good idea and write their own messages in the channel:
func TestConsumer(t testing.T, q queueIface) {
cons := &Consumer{
r: q,
}
for msg := range cons.r.ReadChan() {
// Test thing.
}
}
Wait, what about the errors?
func TestConsumer(t testing.T, q queueIface) {
cons := &Consumer{
r: q,
}
for {
select {
case msg := <-cons.r.ReadChan():
// Test thing.
case err := <-cons.r.ErrChan():
// What caused this again?
}
}
}
Now we need to somehow generate events in order to actually write to this stub, which replicates the behavior of the library we are using. If the library just wrote the synchronous API, then we could add all the parallelism to the client code, so testing becomes easier.
func TestConsumer(t testing.T, q queueIface) {
cons := &Consumer{
r: q,
}
msg, err := cons.r.ReadMsg()
// handle err, test thing
}
If you are in doubt, just remember that it is always easy to add parallelism to the consumer package (consuming package), and it is difficult or impossible to remove it after exporting from the library. And most importantly, do not forget to write in the package documentation whether the structure / package is safe for simultaneous access to several goroutines.
Sometimes it is still desirable or necessary to export the channel. In order to mitigate some of the problems mentioned above, you can provide channels through accessors instead of direct access and leave them open only for reading (
←chan
) or only for writing ( chan←
) when declaring. Use
net/http/httptest
Httptest
allows you to performhttp.Handler
code without starting the server or binding to the port. This speeds up testing and allows you to run tests in parallel at a lower cost. Here is an example of the same test implemented in two ways. There is nothing grandiose here, but this approach reduces the amount of code and saves resources.
func TestServe(t *testing.T) {
// The method to use if you want to practice typing
s := &http.Server{
Handler: http.HandlerFunc(ServeHTTP),
}
// Pick port automatically for parallel tests and to avoid conflicts
l, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
defer l.Close()
go s.Serve(l)
res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool")
if err != nil {
log.Fatal(err)
}
greeting, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(greeting))
}
func TestServeMemory(t *testing.T) {
// Less verbose and more flexible way
req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil)
w := httptest.NewRecorder()
ServeHTTP(w, req)
greeting, err := ioutil.ReadAll(w.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(greeting))
}
Perhaps the most important feature is that with the help
httptest
you can only split the test into the function you want to test. No routers, middleware, or any other side effects that come up when setting up servers, services, processor factories, processor factories, or any other things that you would think would be a good idea. To see this principle in action, check out Marc Berger's article .
Use a separate package
_test
Most of the tests in the ecosystem are in files
pkg_test.go
, but still remain in the same package: package pkg
. A separate test package is a package that you create in a new file,foo_test.go
, in the directory of the module you want to test,, foo/
with the declaration package foo_test
. From here you can import github.com/example/foo
other dependencies. This feature allows you to do many things. This is the recommended solution for cyclic dependencies in tests, it prevents the appearance of “brittle tests” and allows the developer to feel what it is like to use your own package. If your package is difficult to use, then testing with this method will also be difficult. This strategy prevents fragile tests by restricting access to private variables. In particular, if your tests break and you use separate test packages, it is almost guaranteed that a client using a function that breaks in the tests will also break when called.
Finally, it helps to avoid import cycles in tests. Most packages are more likely to depend on other packages that you wrote besides the testing ones, so you will end up with a situation where the import cycle occurs naturally. An external package is located above both packages in the package hierarchy. Take an example from The Go Programming Language (Chapter 11 Section 2.4), where it
net/url
implements a URL parser that net/http
imports for use. However, net / url
you need to test using a real use case by importing net / http
. So it turns out net/url_test
.Now, when you use a separate test package, you may need access to unexported entities in the package where they were previously available. Some developers are faced with this for the first time when testing something based on time (for example, time.Now becomes a stub using a function). In this case, we can use an additional file to provide entities exclusively during testing, since files
_test.go
are excluded from regular builds. What do you need to remember?
It is important to remember that none of the methods described above is a panacea. The best approach in any business is to critically analyze the situation and independently choose the best solution to the problem.
Want to learn more about testing with Go?
Read these articles:
Dave Cheney's Writing Table Driven Tests in Go
The Go Programming Language chapter on Testing.
Or watch these videos:
Hashimoto's Advanced Testing With Go talk from Gophercon 2017
Andrew Gerrand's Testing Techniques talk from 2014
We hope this translation has been useful to you. We are waiting for comments, and everyone who wants to learn more about the course, we invite you to open day , which will be held on May 23.