Start testing your Go app in the right way

Source: Internet
Author: User
Tags creative commons attribution
This is a creation in Article, where the information may have evolved or changed.

Start testing your Go app in the right way

/** * 谨献给Yoyo * * 原文出处:https://www.toptal.com/go/your-introductory-course-to-testing-with-go * @author dogstar.huang <chanzonghuang@gmail.com> 2016-08-11 */

It is important to have a clear mind when learning anything new.

If you're quite unfamiliar with go and come from a language like JavaScript or Ruby, you're probably accustomed to using out-of-the-box frameworks to help you mock, assert, and do other tests of your ridicule, assertions, and other tests of witchcraft.

now, eliminate the idea based on external dependencies or frameworks! a few years ago, when I was learning this remarkable programming language, testing was the first hurdle I encountered, when only a few resources were available.

Now I know that testing success in Go means being light-dependent (as with go everything), relying at least on external class libraries, and writing better, reusable code. This Blake Mizerany experience introduces the courage to try a third-party test library, is a good start to adjust your mind. You'll see some arguments about using the external class library and the "Go Way" framework.

Do you want to learn go? Take a look at our Golang introductory tutorial.

The idea of building your own test framework and simulation seems counterintuitive, but it's also easy to think that this is a good starting point for learning the language. Also, unlike what I learned at the time, you have this article as a guide throughout the common test scripts and the introduction of what I think is the best practice for effective testing and keeping the code clean.

Take the "Go" approach and eliminate the reliance on external frameworks.

Table Test in Go

The basic test unit-the reputation of "unit testing"-can be any part of a program, which in its simplest form requires only one input and returns an output. Let's look at a simple function that will be written for testing. Obviously, it's far from perfect and complete, but for demonstration purposes it's good enough:
avg.go

func Avg(nos ...int) int {      sum := 0    for _, n := range nos {        sum += n    }    if sum == 0 {        return 0    }    return sum / len(nos)}

The above function, func Avg(nos ...int) which returns 0 or gives it an integer mean of a series of numbers. Now let's write a test for it.

In go, give the test file the same name as the file that contains the code to be tested, and take the appended suffix as the _test best practice. For example, the above code is a avg.go file named, so our test file will be named avg_test.go .

Note that these examples are just extracts from the actual files, because package definitions and imports are omitted for simplification.

Here is Avg the test for the function:

avg_test.go

func TestAvg(t *testing.T) {      for _, tt := range []struct {        Nos    []int        Result int    }{        {Nos: []int{2, 4}, Result: 3},        {Nos: []int{1, 2, 5}, Result: 2},        {Nos: []int{1}, Result: 1},        {Nos: []int{}, Result: 0},        {Nos: []int{2, -2}, Result: 0},    } {        if avg := Average(tt.Nos...); avg != tt.Result {            t.Fatalf("expected average of %v to be %d, got %d\n", tt.Nos, tt.Result, avg)        }    }}

There are a few things to note about function definitions:

    • First, the "test" prefix of the function name is tested. This is required so that the tool detects it as an effective test.
    • The second half of the test function name is usually the name of the function or method to be tested, which is here Avg .
    • We also need to pass testing.T in a test structure called, which allows control of the test flow. For more details on this API, please visit this document.

Now, let's talk about the format that this example writes. A test suite (a series of tests) is running through a Agv() function, and each test contains a specific input and expected output. In our example, each test passes in a series of integers ( Nos ) and expects a specific return value ( Result ).

The table test derives its name from its structure and is easily represented as a two-column table: the input variable and the expected output variable.

Golang Interface Simulation

The greatest and most powerful feature that the go language provides is called the interface. In addition to gaining the power and flexibility of the interface in the design of the program architecture, the interface provides us with an amazing opportunity to decouple components and thoroughly test them at junctions.

An interface is a collection of specified methods and is also a variable type.

Let's look at a fictitious scenario, assuming that it needs to be from IO. Reader reads the first n bytes and returns them as a string. It looks like this:
readn.go

// readN reads at most n bytes from r and returns them as a string.func readN(r io.Reader, n int) (string, error) {      buf := make([]byte, n)    m, err := r.Read(buf)    if err != nil {        return "", err    }    return string(buf[:m]), nil}


Obviously, the main thing to test is readN this feature, which returns the correct output when given a variety of inputs. This can be done with a tabular test. But there are also two special scenes to cover, that is to check:

    • readNCalled by a buffer of size n
    • readNReturns an error if an exception is thrown

In order to know the size of the r.Read buffer to be passed, and to control the error it returns, we need to simulate the pass readN -through r . If you look at the reader type in the go document, we see that io.Reader it looks like this:

type Reader interface {         Read(p []byte) (n int, err error)}

It seems quite easy. To meet what io.Reader we need to do is there is a way to self Read -simulate. So, it ReaderMock could be this:

type ReaderMock struct {      ReadMock func([]byte) (int, error)}func (m ReaderMock) Read(p []byte) (int, error) {      return m.ReadMock(p)}

Let's analyze the above code a little bit. Any ReaderMock instance clearly satisfies the io.Reader interface, because it implements the necessary Read methods. Our simulations also contain fields ReadMock that allow us to set the exact behavior of the simulation method, which makes it easy for any dynamic instance to behave.

To ensure that the interface is able to meet the needs at runtime, a great technique of not consuming memory is to insert the following code into our code:

var _ io.Reader = (*MockReader)(nil)  

This checks the assertion but does not allocate anything, which allows us to ensure that the interface is implemented correctly at compile time, before the program actually uses it to run to any function. Optional tips, but very practical.

Go ahead and let's write the first test: r.Read called by the size n of the buffer. In order to do this, we use the ReaderMock following:

func TestReadN_bufSize(t *testing.T) {      total := 0    mr := &MockReader{func(b []byte) (int, error) {        total = len(b)        return 0, nil    }}    readN(mr, 5)    if total != 5 {        t.Fatalf("expected 5, got %d", total)    }}

As you can see above, we define a "false" function with a local variable io.Reader , which is used to assert the validity of our tests later. Pretty easy.

Take a look at the second scenario that needs to be tested, which requires us Read to simulate to return an error:

func TestReadN_error(t *testing.T) {      expect := errors.New("some non-nil error")    mr := &MockReader{func(b []byte) (int, error) {        return 0, expect    }}    _, err := readN(mr, 5)    if err != expect {        t.Fatal("expected error")    }}

In the above test, no matter what call mr.Read (our simulated reader) would return both defined errors, it would be reliable to assume that readN the normal operation would do the same.

Golang Method Simulation

Usually we don't need a simulation method, because instead we tend to use structs and interfaces. These are easier to control, but occasionally encounter this necessity, and I often see confusion around this topic. Someone even asked how to simulate log.Println something like this. Although there is little need to test log.Println the input of the case, we will use this opportunity to prove it.

Consider the following simple if statement, based on n the value of the output record:

func printSize(n int) {      if n < 10 {        log.Println("SMALL")    } else {        log.Println("LARGE")    }}

In the example above, let's assume a ridiculous scenario: a particular test log.Println is called by the correct value. To simulate this feature, you first need to wrap it up:

var show = func(v ...interface{}) {      log.Println(v...)}

The sound method in this way-as a variable-allows us to overwrite it in the test and assign it any behavior we want. Indirectly, the log.Println line of code is replaced show , then our program becomes:

func printSize(n int) {      if n < 10 {        show("SMALL")    } else {        show("LARGE")    }}

Now we can test it out:

The 
  func testprintsize (t *testing. T) {var got string oldshow: = Show Show = Func (v. ... interface{}) {if Len (v)! = 1 {t.fatalf        ("expected show to being called with 1 param, got%d", Len (v))} var ok bool got, OK = v[0]. (string) If!ok {t.fatal ("expected show to Bes called with a string")}} for _, tt: = Rang e []struct{N int out string} {{2, "SMALL"}, {3, "SMALL"}, {9, "SMALL"}, {10 , "LARGE"}, {one, "LARGE"}, {A, "LARGE"},} {got = "" PrintSize (TT. N) if got! = TT. Out {t.fatalf ("on%d, expected '%s ', got '%s ' \ n", TT. N, TT. Out, Got)}}//Careful though, we must don't forget to restore it to it original value//before Finishin    G The test, or it might interfere with other tests in our//suite, giving us unexpected and hard to trace behavior. Show = Oldshow}  

We should not "simulate log.Println ", but in those very accidental cases, when we really need to simulate a package-level approach for good reason, the only way to do that (as far as I know) is to declare it as a package-level variable so that we can control its value.

However, if we do need to simulate log.Println something like this, if we use a custom logger, we can write a more elegant solution.

Golang Template Rendering Test

Another fairly common scenario is to test the output of a render template as expected. Let's consider a pair http://localhost:3999/welcome?name=Frank of GET requests, which will return the following body:

If it is obviously not enough now, the query parameter name name matches the label of the class span , which is not a coincidence. In this case, the obvious test should verify that this happens correctly each time the multilayer output is crossed. Here I find the Goquery class library very useful.

Goquery uses a jquery-like API to query the HTML structure, which is essential for testing the validity of the program's label output.

Now we can write our tests in this way:
welcome__test.go

func TestWelcome_name(t *testing.T) {      resp, err := http.Get("http://localhost:3999/welcome?name=Frank")    if err != nil {        t.Fatal(err)    }    if resp.StatusCode != http.StatusOK {        t.Fatalf("expected 200, got %d", resp.StatusCode)    }    doc, err := goquery.NewDocumentFromResponse(resp)    if err != nil {        t.Fatal(err)    }    if v := doc.Find("h1.header-name span.name").Text(); v != "Frank" {        t.Fatalf("expected markup to contain 'Frank', got '%s'", v)    }}

First, we check that the response status code is not 200/ok before processing.

I think it's not too farfetched to assume that the rest of the code snippet above is self-explanatory: we use http the package to extract the URL and create a new Goquery-compliant document based on the response, which we then use to query the returned DOM. We checked the h1.header-name encapsulated text in the inside span.name ' Frank '.

Testing the JSON interface

Golang is often used to write some kind of API, so last but not least, let's look at some advanced ways to test the JSON API.

Imagine that if the previous terminal returned JSON instead of HTML, then the body from which http://localhost:3999/welcome.json?name=Frank we would expect the response would look like this:

{"Salutation": "Hello Frank!"}

Asserting the JSON response, as you can imagine, is not much different than asserting the template response, and one thing that is special is that we don't need any external libraries or dependencies. Golang's standard library is sufficient. Here is our test to confirm that the correct JSON is returned for the given parameter:
welcome__test.go

func TestWelcome_name_JSON(t *testing.T) {      resp, err := http.Get("http://localhost:3999/welcome.json?name=Frank")    if err != nil {        t.Fatal(err)    }    if resp.StatusCode != 200 {        t.Fatalf("expected 200, got %d", resp.StatusCode)    }    var dst struct{ Salutation string }    if err := json.NewDecoder(resp.Body).Decode(&dst); err != nil {        t.Fatal(err)    }    if dst.Salutation != "Hello Frank!" {        t.Fatalf("expected 'Hello Frank!', got '%s'", dst.Salutation)    }}

If any structure other than decoding is returned, json.NewDecoder an error is returned, and the test will fail. Given the success of decoding the response structure, we check that the contents of the field are up to expectations-in our case, "Hello frank!".

Setup and Teardown

The Golang test is easy, but there is a problem in this before the JSON test and the template rendering test. They all assume that the server is running, and this creates an unreliable dependency. Also, it is not a good idea to need a "live" server.

Testing "real-time" data on a "live" production server has never been a good idea, from a local spin up or development copy, so nothing can be done without serious damage.

Fortunately, Golang provides a package to httptest create a test server. The test raises its own independent server, independent of our primary server, so the test does not interfere with the production environment.

In this case, it is desirable to create a generic setup and teardown method to be used by all test calls that need to run the server. Based on this new, more secure model, our tests ultimately look like this:

func setup() *httptest.Server {      return httptest.NewServer(app.Handler())}func teardown(s *httptest.Server) {      s.Close()}func TestWelcome_name(t *testing.T) {      srv := setup()    url := fmt.Sprintf("%s/welcome.json?name=Frank", srv.URL)    resp, err := http.Get(url)    // verify errors & run assertions as usual    teardown(srv)}

Note app.Handler() the reference. This is a best practice function that returns an application that http Handler can either instantiate a production server or instantiate a test server.

Conclusion

GolangThe test is a good opportunity to assume the external vision of your program and assume the footsteps of the visitors, or in most cases, the users of your API. It offers great opportunities to ensure you provide good code and quality experience.

Whenever you're unsure of the more complex features in your code, testing can be useful as a reassurance, and it also guarantees that the rest of the system will continue to work well when it comes to modifying the components of larger systems.

Hopefully this article will be useful to you if you know any other test techniques also welcome to post a comment.


------------------------

    • This work is licensed under the Creative Commons Attribution-NonCommercial use-Share 3.0 non-localized version license agreement in the same way.
    • This article is translated by: Dogstar, published in AI Translation (itran.cc); Welcome reprint, but please specify the source, thank you!
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.