
No one likes writing tests. Of course, I'm kidding, everyone loves to write them! As prompted by tmlidy and HR, the right answer for interviews - I really love and write tests. But suddenly you like to write tests in another language. How to start writing covered code on go?
Part 1. We test handler
In go out of the box there is support for the http server in "net / http", so you can raise it without any effort. The opportunities that have opened up make us feel extremely powerful, and therefore our code will return the 42nd user.
func userHandler(w http.ResponseWriter, r *http.Request) { var user User userId, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { w.Write([]byte( "Error")) return } if userId == 42 { user = User{userId, "Jack", 2} } jsonData, _ := json.Marshal(user) w.Write(jsonData) } type User struct { Id int Name string Rating uint }
This code receives the user id parameter as input, further emulates the presence of a user in the database, and returns. Now we need to test it ...
')
There is a wonderful thing “net / http / httptest”, it allows you to simulate the challenge of our handler and then compare the answer.
r := httptest.NewRequest("GET", "http://127.0.0.1:80/user?id=42", nil) w := httptest.NewRecorder() userHandler(w, r) user := User{} json.Unmarshal(w.Body.Bytes(), &user) if user.Id != 42 { t.Errorf("Invalid user id %d expected %d", user.Id, 42) }
Part 2. Honey, we have an external API here.
And why do we need to take a breath if we only have warmed up? Inside our services, sooner or later external api will appear. This is a strange often hiding beast that can behave as you please. For tests, we would like a more compliant colleague. And our newly discovered httptest will help us here too. As an example, the code to call an external api with data transfer is further.
func ApiCaller(user *User, url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return updateUser(user, resp.Body) }
To defeat this, we can do an external API mock, the simplest version looks like this:
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, `{ "result": "ok", "data": { "user_id": 1, "rating": 42 } }`) })) defer ts.Close() user := User{id: 1} err := ApiCaller(&user, ts.URL)
ts.URL will contain a string of the format `http: //127.0.0.1: 49799`, which will be api for our implementation
Part 3. Let's work with the base
There is a simple way: to raise a docker with a base, roll up migrations, fixtures and start our excellent service. But we will try to write tests, having a minimum of dependencies with external services.
Implementing work with a database in go allows you to replace the driver itself, and, bypassing 100 pages of code and thinking, I suggest you take the
github.com/DATA-DOG/go-sqlmock library
You can deal with sql.Db by the dock. Take a slightly more interesting example in which will be orm for -
gorm .
func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() }
I hope this example at least made you think how to test it. In "mock.ExpectExec" you can substitute a regular expression that covers the desired case. The only thing you need to remember is that the order of setting the expectations should coincide with the order and the number of calls.
func TestDbListener(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() result := []string{"id", "name", "counter"} mock.ExpectQuery("SELECT \\* FROM `Users`").WillReturnRows(sqlmock.NewRows(result).AddRow(1, "Jack", 2)) mock.ExpectExec("UPDATE `Users`").WithArgs(3, 1).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() gormDB, _ := gorm.Open("mysql", db) DbListener(gormDB.LogMode(true)) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
I found many examples on testing the base
here .
Part 4. Working with the file system
We tried our strength in different areas and accepted the fact that everything was a good wet. It's not so simple. I suggest two approaches, mock or use the file system.
Option 1 - all mokay on
github.com/spf13/aferoPros :
- Nothing needs to be redone if you already use this library. (but then it’s boring to read it)
- Work with a virtual file system, which will greatly speed up your tests.
Cons :
- Requires revision of existing code.
- Chmod does not work in the virtual file system. But it can be a feature because the documentation states - “Avoid security issues and permissions”.
Of these few points, I immediately made 2 tests. In the file system version, I created an unreadable file and checked how the system would work.
func FileRead(path string) error { path = strings.TrimRight(path, "/") + "/" // files, err := ioutil.ReadDir(path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := ioutil.ReadFile(deleteFileName) if err != nil { return err } err = os.Remove(deleteFileName) // } return nil }
Using afero.Fs requires minimal improvements, but fundamentally does not change anything in the code
func FileReadAlt(path string, fs afero.Fs) error { path = strings.TrimRight(path, "/") + "/" // files, err := afero.ReadDir(fs, path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := afero.ReadFile(fs, deleteFileName) if err != nil { return err } err = fs.Remove(deleteFileName) // } return nil }
But our fun will be incomplete if we do not find out how much faster afero than native.
Minute benchmarks:
BenchmarkIoutil 5000 242504 ns/op 7548 B/op 27 allocs/op BenchmarkAferoOs 300000 4259 ns/op 2144 B/op 30 allocs/op BenchmarkAferoMem 300000 4169 ns/op 2144 B/op 30 allocs/op
So, the library is ahead of the standard one by an order of magnitude, but now you can use the virtual file system or the real one.
Recommend:
haisum.imtqy.com/2017/09/11/golang-ioutil-readallmatthias-endler.de/2018/go-io-testingAfterword
I honestly really like the 100% coverage, but the nonlibrary code does not need it. And even it does not guarantee protection against errors. Focus on the requirements of the business, and not on the ability of the function to return 10 different errors.
For those who like to poke the code and start tests, the
repository .