> For the complete documentation index, see [llms.txt](https://sons-organization-15.gitbook.io/learn-go-with-tests/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sons-organization-15.gitbook.io/learn-go-with-tests/hoi-dap-q-and-a/error-types.md).

# Error types

[**Bạn có thể tìm thấy toàn bộ code tại đây**](https://github.com/quii/learn-go-with-tests/tree/main/q-and-a/error-types)

**Việc tạo các kiểu dữ liệu riêng cho lỗi có thể là một cách trang nhã để dọn dẹp code, giúp code của bạn dễ sử dụng và dễ test hơn.**

Pedro trên Gopher Slack có hỏi:

> Nếu tôi tạo một lỗi như `fmt.Errorf("%s must be foo, got %s", bar, baz)`, có cách nào để kiểm tra mà không cần so sánh giá trị chuỗi không?

Hãy tạo một hàm giả định để giúp khám phá ý tưởng này.

```go
// DumbGetter sẽ lấy nội dung thân bài (body) dạng chuỗi của url nếu nhận được mã 200
func DumbGetter(url string) (string, error) {
	res, err := http.Get(url)

	if err != nil {
		return "", fmt.Errorf("problem fetching from %s, %v", url, err)
	}

	if res.StatusCode != http.StatusOK {
		return "", fmt.Errorf("did not get 200 from %s, got %d", url, res.StatusCode)
	}

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body) // bỏ qua err cho ngắn gọn

	return string(body), nil
}
```

Việc viết một hàm có thể thất bại vì nhiều lý do khác nhau là điều không hiếm gặp, và chúng ta muốn đảm bảo mình xử lý đúng từng kịch bản.

Như Pedro đã nói, chúng ta *có thể* viết một bài test cho status error như sau.

```go
t.Run("khi bạn không nhận được mã 200, bạn sẽ nhận được một lỗi trạng thái", func(t *testing.T) {

	svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		res.WriteHeader(http.StatusTeapot)
	}))
	defer svr.Close()

	_, err := DumbGetter(svr.URL)

	if err == nil {
		t.Fatal("mong đợi một lỗi")
	}

	want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot)
	got := err.Error()

	if got != want {
		t.Errorf(`got "%v", want "%v"`, got, want)
	}
})
```

Bài test này tạo ra một server luôn trả về `StatusTeapot`, sau đó chúng ta sử dụng URL của nó làm đối số cho `DumbGetter` để xem nó có xử lý các phản hồi không phải `200` một cách chính xác hay không.

## Những vấn đề của cách test này

Cuốn sách này luôn nhấn mạnh việc *lắng nghe test* và bài test này cho cảm giác không ổn:

* Chúng ta đang xây dựng cùng một chuỗi ký tự như production code để test nó.
* Nó gây khó chịu khi đọc và viết.
* Liệu chuỗi thông điệp lỗi chính xác có phải là điều chúng ta *thực sự quan tâm* không?

Điều này nói lên gì? Trải nghiệm viết test phản ánh trải nghiệm của người dùng code.

Người dùng code sẽ xử lý lỗi như thế nào? Tốt nhất họ chỉ có thể so sánh chuỗi lỗi -- rất dễ sai và khó bảo trì.

## Những gì chúng ta nên làm

Với TDD, chúng ta có lợi thế là có thể tư duy theo kiểu:

> *Tôi* muốn sử dụng code này như thế nào?

Những gì chúng ta có thể làm cho `DumbGetter` là cung cấp một cách để người dùng sử dụng hệ thống kiểu (type system) để hiểu loại lỗi nào đã xảy ra.

Điều gì sẽ xảy ra nếu `DumbGetter` có thể trả về cho chúng ta thứ gì đó như:

```go
type BadStatusError struct {
	URL    string
	Status int
}
```

Thay vì một chuỗi ký tự mang tính "ma thuật", chúng ta có *dữ liệu* thực sự để làm việc.

Hãy thay đổi bài test hiện tại để phản ánh nhu cầu này:

```go
t.Run("khi bạn không nhận được mã 200, bạn sẽ nhận được một lỗi trạng thái", func(t *testing.T) {

	svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		res.WriteHeader(http.StatusTeapot)
	}))
	defer svr.Close()

	_, err := DumbGetter(svr.URL)

	if err == nil {
		t.Fatal("mong đợi một lỗi")
	}

	got, isStatusErr := err.(BadStatusError)

	if !isStatusErr {
		t.Fatalf("không phải là BadStatusError, nhận được %T", err)
	}

	want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}

	if got != want {
		t.Errorf("got %v, want %v", got, want)
	}
})
```

Chúng ta sẽ phải làm cho `BadStatusError` triển khai (implement) error interface.

```go
func (b BadStatusError) Error() string {
	return fmt.Sprintf("did not get 200 from %s, got %d", b.URL, b.Status)
}
```

### Bài test làm gì?

Thay vì kiểm tra chuỗi ký tự chính xác của lỗi, chúng ta đang thực hiện một [type assertion](https://tour.golang.org/methods/15) trên lỗi để xem nó có phải là một `BadStatusError` hay không. Điều này thể hiện mong muốn của chúng ta về *loại* lỗi một cách rõ ràng hơn. Giả sử assertion thành công, chúng ta có thể kiểm tra các thuộc tính của lỗi xem chúng có chính xác không.

Khi chúng ta chạy bài test, nó sẽ cho chúng ta biết rằng chúng ta đã không trả về đúng loại lỗi:

```
--- FAIL: TestDumbGetter (0.00s)
    --- FAIL: TestDumbGetter/when_you_dont_get_a_200_you_get_a_status_error (0.00s)
    	error-types_test.go:56: was not a BadStatusError, got *errors.errorString
```

Hãy sửa `DumbGetter` bằng cách cập nhật code xử lý lỗi để sử dụng kiểu dữ liệu của chúng ta:

```go
if res.StatusCode != http.StatusOK {
	return "", BadStatusError{URL: url, Status: res.StatusCode}
}
```

Sự thay đổi này đã mang lại một số *hiệu ứng tích cực thực sự*:

* Hàm `DumbGetter` của chúng ta đã trở nên đơn giản hơn, nó không còn bận tâm đến những chi tiết phức tạp của một chuỗi lỗi nữa, nó chỉ tạo ra một `BadStatusError`.
* Các bài test của chúng ta giờ đây phản ánh (và làm tài liệu) những gì người dùng code *có thể* làm nếu họ quyết định muốn xử lý lỗi tinh vi hơn là chỉ logging. Chỉ cần thực hiện một type assertion và sau đó bạn có thể dễ dàng truy cập vào các thuộc tính của lỗi.
* Nó vẫn "chỉ" là một `error`, vì vậy nếu họ muốn, họ có thể chuyển nó lên trên call stack hoặc log nó như bất kỳ `error` nào khác.

## Tổng kết

Nếu bạn thấy mình đang test nhiều điều kiện lỗi khác nhau, đừng rơi vào cái bẫy so sánh các thông điệp lỗi.

Việc này dẫn đến các bài test dễ bị hỏng và khó đọc/viết, đồng thời nó cũng phản ánh những khó khăn mà người dùng code sẽ gặp phải nếu họ cũng cần bắt đầu thực hiện những việc khác nhau tùy thuộc vào loại lỗi đã xảy ra.

Hãy luôn đảm bảo các bài test phản ánh cách *bạn* muốn sử dụng code, vì vậy hãy cân nhắc việc tạo các error type để đóng gói các loại lỗi. Điều này giúp việc xử lý các loại lỗi khác nhau trở nên dễ dàng hơn cho người dùng code, đồng thời giúp code xử lý lỗi đơn giản và dễ đọc hơn.

## Phụ lục

Kể từ Go 1.13, có những cách mới để làm việc với các lỗi trong thư viện chuẩn, điều này đã được đề cập trong [Go Blog](https://blog.golang.org/go1.13-errors)

```go
t.Run("khi bạn không nhận được mã 200, bạn sẽ nhận được một lỗi trạng thái", func(t *testing.T) {

	svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		res.WriteHeader(http.StatusTeapot)
	}))
	defer svr.Close()

	_, err := DumbGetter(svr.URL)

	if err == nil {
		t.Fatal("mong đợi một lỗi")
	}

	var got BadStatusError
	isBadStatusError := errors.As(err, &got)
	want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot}

	if !isBadStatusError {
		t.Fatalf("không phải là BadStatusError, nhận được %T", err)
	}

	if got != want {
		t.Errorf("got %v, want %v", got, want)
	}
})
```

Trong trường hợp này, chúng ta đang sử dụng [`errors.As`](https://pkg.go.dev/errors#example-As) để cố gắng trích xuất lỗi của mình vào kiểu dữ liệu tùy chỉnh. Nó trả về một giá trị `bool` để biểu thị sự thành công và trích xuất lỗi vào biến `got` cho chúng ta.
