Friday, August 6, 2021

Top 5 Advanced Go Testing Techniques

 

Top 5 Advanced Go Testing Techniques

Test- driven development is an extraordinary method to keep the quality of your code high, while protecting yourself from regression and proving to yourself as well as other people that your code does what it should. Go has robust in-built testing library. If you are thinking to build or software with Go, it will be better to know the Go testing techniques so as to develop a perfect software. So here we’ll discuss strategies to level up Go testing that will save your time and effort to maintain the code. 

Top 5 Advanced Go Testing Techniques-

1. Use test suites-

Suite testing is a process of developing a test against common interface that can be used against various implementations of that interface. Here you will see how you can pass in multiple different Thinger implementations and have them run against 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)
}

Tests that are written against the interface are usable by all implementations of interface to determine if the behavior requirements are met. This strategy will save time to solve the P versus NP problem. While swapping two underlying systems, you don’t need to write extra tests and it won’t break your app. Implicitly it requires that you create an interface defining the surface area of that you’re testing. By the use of dependency injection, you set up the suite from your package passing in the implementation for package.

Another great example of this in the standard library is golang.org/x/net/nettest package. It provides the means to verify a net.Conn satisfies its interface.

2. Don’t export concurrency primitives-

Go provides easy to use concurrency primitives that can sometimes causes their overuse. Mainly concerned is about channels and sync package. Some of the time, it is enticing to export a channel from your package for consumers to use. Also, it is common mistake to embed sync.Mutex without making it private. Likewise with anything, this isn’t in every case bad however it does difficulties when testing your program. When you export channels, you expose the consumer of the package to extra complexity they shouldn’t care about. When the channel is exported from a package, you open up challenges in testing for one consuming that channel. So as to test properly, the consumer needs to know about- when data is finished being sent on the channel, whether there are any errors receiving the data or not, After completion, how does the package clean up channel etc.

Consider an example of reading a queue. Here’s an example library that reads from the queue and exposes a channel for the consumer to read from. User of library wants to implement a test for their consumer: User might decide that DI(dependency injection) is a good idea for this and write messages with channel.

However, what about errors?

How to generate events to actually write into this mock that replicate the behavior of actual library you’re using? If library wrote synchronous API, then you could add this concurrency in our client code and it becomes easy to test.

Always remember that, it is easy to add concurrency in consuming package and difficult to remove once exported from a library. Don’t forget to mention in package documentation whether or not a struct/package is safe for concurrent access by various goroutines. Some of the times, it is still necessary to export channel through accessors rather than directly and force them to be ready-only or write only channels in declaration.

3. Avoid interface pollution-

Interfaces are important for testing as they are the most powerful tool in test arsenal, hence it is important to use them appropriately. Packages export an interface for consumers to use that turns leads to- consumers implementing their own mock of package implementation or the package exporting own mock. 
It will be better to consider interfaces before exporting. Mostly Programmers are tempted to export interfaces to mock out their behavior. Rather than, document that interfaces your structs satisfy just like you don’t create a hard dependency between consumer package and your own. Error package is a good example of this. When you have interface in program that you don’t want to export can use an internal/ package subtree to keep it scoped to the package. With this, we remove the concern that other consumers might depend on it and so can be flexible in the evolution of interfaces as new needs present themselves. Generally we create interfaces around external dependencies and use dependency injection to run tests locally. With this, consumer can implement small interfaces of their own, just wrapping the consumed surface of library for their own testing.

4. Make use of a separate _test package-

Generally tests in ecosystem are created in files pkg_test.go but still live in the same package: package pkg. Separate test package is a package you create in new file,   foo_test.go, in the package directory of package you want to test, foo/, with declaration package foo_test. 

You can import github.com/example/foo and some other dependencies. It enables lots of things. This is a suggested workaround for cyclic dependencies in tests, it allows developers to feel what it’s like to consume their own package.  If a package is hard to use, it will also be hard to test using this method. This strategy prevents tests by restricting access to private variables. Particularly if tests break and you’re using a separate test packages it;s almost possible that a client using the feature that broke in tests will also break when called.

This helps in keeping away from import cycles in tests. Many packages depend on other packages you wrote aside from those being tested, hence you’ll be in a situation where an import cycle occur as a natural consequence. External package sits above both packages in the package hierarchy. Taking example from the Go Programming Language(Chp. 11 Sec 2.4), net/url implements a URL parser that net/http imports for use.

But, net/url would like to test by using real use case by importing net/http. Hence net/url_test was came to focus.

Now, if you use separate test package, you may need access to unexported entities in your package where they were accessible. Most of the people hit this first while testing something time based. In such a situation you can use extra file to expose them exclusively during testing since _test.go files are excluded from regular builds.

5. Use net/http/httptest-

httptest allows you to exercise your http.Handler code without spinning up a server. It speeds up tests and allows them to run in parallel with less effort.

Here’s an example of the same test implemented using both of the methods. It saves you a considerable amount of code and 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))
}

May be the biggest thing using httptest gets you is the ability to compartmentalize your test to just the function you want to test. No routers, middleware or any other side-effect coming from setting up servers, services, handler factories etc are thrown by ideas your former self thought were good.

Know the reasons to use enterprise mobile apps at- Why you should use Golang for enterprise mobile apps?

Wrap Up-

It will better to analyse the situation before applying any testing technique. The above mentioned top 5 Go testing techniques will surely help you to develop the best software. In case of any difficulty you can consult with Solace experts and get a free quote for software development. We will be happy to help you.


No comments:

Post a Comment