📜 ⬆️ ⬇️

Golang testing outside gotour



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/afero

Pros :


Cons :

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-readall
matthias-endler.de/2018/go-io-testing

Afterword


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 .

Source: https://habr.com/ru/post/425025/


All Articles