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:
readN
Called by a buffer of size n
readN
Returns 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
Golang
The 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!