5 advanced testing techniques on Go

Salute to all! Less than a week remains 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 it. In this article, we’ll 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 to work 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 sets is the process of developing a test for testing a common interface that can be used on many implementations of this interface. Below you can see how we pass on 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 worked with code databases that use this method. Frequently used tests on plug-in systems that are written to test the 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 save hours, days, and even enough time to solve the problem of equality of classes P and NP. Also, replacing one base system with another eliminates the need to write (a large number) of additional tests, and there is also confidence that such an approach will not disrupt your application. Implicitly, you need to create an interface that defines the area of ​​the test area. With the help of dependency injection, you can customize a set of a package passed to the implementation of the entire package.

A full example can be found here. Despite the fact that this example is contrived, one can imagine that one database is remote, and the second is in memory.

Another cool example of this technique is located in the standard library in the package. golang.org/x/net/nettest. It provides the means to verify that net.Conn satisfies the interface.

Avoid interface contamination

You can't talk about testing in Go, but don't talk about interfaces.

Interfaces are important in the context of testing, because 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 stub (mock) to implement the package;
B) The package exports its own stub (mock).

“The larger the interface, the weaker the abstraction”
– Rob Pike, Sayings about Go

Interfaces should be carefully checked before exporting. It is often tempting to export interfaces to allow users to mimic the behavior they need. Instead, document which interfaces suit your structures so as not to create hard dependencies between the consumer package and your own. A great example of this is the package. errors.

When we have an interface that we do not want to export, you 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, can be flexible in changing the interface in accordance with the new requirements. We usually 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, simply by 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, which can also sometimes lead to their excessive use 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 feed from your package so that others can use it. Also, 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 feeds, you further complicate the life of a package user, which is not worth doing. As soon as the channel is exported from the package, you create difficulties in testing for whoever uses this channel. For successful testing, the user must know:

  • When data is finished being sent over the channel.
  • Were there any errors in getting data.
  • How does the packet clear the channel after completion, if clear at all?
  • How to wrap the API of a package, so as not to call it directly?

Pay attention to the reading queue example. Here is an example of a library that reads from a queue and provides the user with a feed for reading.

type Reader struct {...}
func (r * Reader) ReadChan () <-chan Msg {...}

Now a user of your library 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 his own messages to the channel:

func TestConsumer (t testing.T, q queueIface) {
    cons: = & consumer {
        r: q,
    }
    for msg: = range cons.r.ReadChan () {
        // Test thing.
    }
}

Wait, what about 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 to actually write to this stub, which replicates the behavior of the library we use. If the library simply wrote down 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 concurrency 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 Gorutin.
Sometimes it is still desirable or necessary to export the channel. To level some of the problems listed above, you can provide channels through accessors, instead of direct access and leave them open only for reading (← chan) or write only (chan ←) at the announcement.

Use net / http / httptest

Httptest allows you to perform http.Handler code without starting the server or binding to the port. This speeds up testing and allows you to perform tests in parallel with lower costs.

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 by httptest You can divide the test only into the function you want to test. No routers, middleware, or any other side effects that occur when setting up servers, services, handler factories, handler factories factories, or any other things that come to your mind that you used to consider a good idea.

To see this principle in action, read the article by Mark Berger.

Use a separate package. _test

Most of the tests in the ecosystem are created in files. pkg_test.gobut still remain in the same package: package pkg. A separate test package is a package that you create in a new file, foo_test.goin the directory of the module you want to test, foo /with a declaration package foo_test. From here you can import github.com/example/foo and other dependencies. This feature allows you to do many things. This is the recommended solution for cyclic dependencies in tests, it prevents the emergence of “brittle tests” (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 from occurring by restricting access to private variables. In particular, if your tests “break” and you use separate test packets, it is almost guaranteed that the client using the function that broke in the tests will also break when called.

Finally, it helps to avoid import cycles in tests. Most packages are likely to depend on other packages that you wrote besides being tested, so you will eventually encounter a situation where the import cycle occurs naturally. The 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 net / url implements the URL parser which net / http imports for use. but net / url must be tested using a real use case, importing net / http. Thus it turns out net / url_test.

Now that you are using a separate test package, you may need access to the unexported entities in the package where they were previously available. Some developers encounter this for the first time when they are 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 solely during testing, since the files _test.go excluded from regular builds.

What 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:

Writing Table Driven Tests in Go by Dave Cheney
The Go Programming Language Chapter on Testing.
Or watch these videos:
Hashimoto's Advanced Testing With Gophercon 2017
Andrew Gerrand's Testing Techniques Talk from 2014

We hope this translation was useful for you. We are waiting for comments, and we invite everyone to learn more about the course on the open day, which will be held on May 23.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *