Chức năng giả trong Go


147

Tôi đang học Đi bằng cách mã hóa một dự án cá nhân nhỏ. Mặc dù nó nhỏ, tôi quyết định thực hiện kiểm tra đơn vị nghiêm ngặt để học thói quen tốt trên Go ngay từ đầu.

Các bài kiểm tra đơn vị tầm thường đều ổn và bảnh bao, nhưng hiện tại tôi đang bối rối với sự phụ thuộc; Tôi muốn có thể thay thế một số cuộc gọi chức năng bằng các cuộc gọi giả. Đây là một đoạn mã của tôi:

func get_page(url string) string {
    get_dl_slot(url)
    defer free_dl_slot(url)

    resp, err := http.Get(url)
    if err != nil { return "" }
    defer resp.Body.Close()

    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil { return "" }
    return string(contents)
}

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := get_page(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

Tôi muốn có thể kiểm tra trình tải xuống () mà không thực sự có được một trang thông qua http - tức là bằng cách chế nhạo get_page (dễ dàng hơn vì nó chỉ trả về nội dung trang dưới dạng chuỗi) hoặc http.Get ().

Tôi tìm thấy chủ đề này: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI dường như là về một vấn đề tương tự. Julian Phillips trình bày thư viện của mình, Withmock ( http://github.com/qur/withmock ) như một giải pháp, nhưng tôi không thể làm cho nó hoạt động được. Đây là những phần có liên quan trong mã thử nghiệm của tôi, phần lớn là mã sùng bái đối với tôi, phải trung thực:

import (
    "testing"
    "net/http" // mock
    "code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
    ctrl := gomock.NewController()
    defer ctrl.Finish()
    http.MOCK().SetController(ctrl)
    http.EXPECT().Get(BASE_URL)
    downloader()
    // The rest to be written
}

Đầu ra thử nghiệm như sau:

ERROR: Failed to install '_et/http': exit status 1
output:
can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

Withmock có phải là giải pháp cho vấn đề thử nghiệm của tôi không? Tôi nên làm gì để nó hoạt động?


Vì bạn đang tham gia thử nghiệm đơn vị Go, hãy tìm hiểu về GoConvey một cách tuyệt vời để thực hiện kiểm tra theo hành vi ... và trêu ghẹo: một giao diện người dùng web cập nhật tự động sắp ra mắt cũng hoạt động với các thử nghiệm "đi thử nghiệm" gốc.
Matt

Câu trả lời:


193

Kudos cho bạn để thực hành thử nghiệm tốt! :)

Cá nhân, tôi không sử dụng gomock(hoặc bất kỳ khuôn khổ chế giễu nào cho vấn đề đó; việc nhạo báng trong Go rất dễ dàng nếu không có nó). Tôi sẽ chuyển một phụ thuộc cho downloader()hàm dưới dạng tham số hoặc tôi sẽ tạo downloader()một phương thức trên một kiểu và kiểu có thể giữ get_pagephụ thuộc:

Phương pháp 1: Truyền get_page()dưới dạng tham số củadownloader()

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}

Chủ yếu:

func get_page(url string) string { /* ... */ }

func main() {
    downloader(get_page)
}

Kiểm tra:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}

download()Phương thức 2 : Tạo một phương thức của một loại Downloader:

Nếu bạn không muốn truyền phụ thuộc làm tham số, bạn cũng có thể tạo get_page()thành viên của một loại và tạo download()phương thức của loại đó, sau đó có thể sử dụng get_page:

type PageGetter func(url string) string

type Downloader struct {
    get_page PageGetter
}

func NewDownloader(pg PageGetter) *Downloader {
    return &Downloader{get_page: pg}
}

func (d *Downloader) download() {
    //...
    content := d.get_page(BASE_URL)
    //...
}

Chủ yếu:

func get_page(url string) string { /* ... */ }

func main() {
    d := NewDownloader(get_page)
    d.download()
}

Kiểm tra:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}

4
Cảm ơn rất nhiều! Tôi đã đi với cái thứ hai. (có một số chức năng khác mà tôi muốn chế giễu, vì vậy việc gán chúng cho một cấu trúc dễ dàng hơn) Btw. Tôi có một chút tình yêu trong Go. Đặc biệt tính năng đồng thời của nó là gọn gàng!
GolDranks

149
Tôi có phải là người duy nhất phát hiện ra rằng để thử nghiệm chúng ta phải thay đổi chữ ký mã / hàm chính là khủng khiếp không?
Thomas

41
@Thomas Tôi không chắc chắn nếu bạn là người duy nhất, nhưng thực sự đó là lý do cơ bản cho sự phát triển theo hướng kiểm tra - thử nghiệm của bạn hướng dẫn cách bạn viết mã sản xuất. Mã thử nghiệm là mô-đun nhiều hơn. Trong trường hợp này, hành vi 'get_page' của đối tượng Trình tải xuống hiện có thể cắm được - chúng ta có thể thay đổi linh hoạt việc thực hiện. Bạn chỉ phải thay đổi mã chính của mình nếu nó được viết xấu ngay từ đầu.
weberc2

21
@Thomas Tôi không hiểu câu thứ hai của bạn. TDD ổ đĩa mã tốt hơn. Mã của bạn thay đổi để có thể kiểm tra được (vì mã có thể kiểm tra được nhất thiết phải là mô-đun với giao diện được suy nghĩ kỹ), nhưng mục đích chính là để có mã tốt hơn - có các kiểm tra tự động chỉ là một lợi ích thứ cấp tuyệt vời. Nếu mối quan tâm của bạn là mã chức năng đang được thay đổi chỉ đơn giản là để thêm các bài kiểm tra sau thực tế, tôi vẫn khuyên bạn nên thay đổi nó đơn giản bởi vì có khả năng ai đó một ngày nào đó sẽ muốn đọc mã đó hoặc thay đổi nó.
weberc2

6
@Thomas tất nhiên, nếu bạn viết bài kiểm tra của mình khi bạn đi cùng, bạn sẽ không phải đối phó với câu hỏi hóc búa đó.
weberc2

24

Nếu bạn thay đổi định nghĩa hàm để sử dụng một biến thay thế:

var get_page = func(url string) string {
    ...
}

Bạn có thể ghi đè nó trong các bài kiểm tra của bạn:

func TestDownloader(t *testing.T) {
    get_page = func(url string) string {
        if url != "expected" {
            t.Fatal("good message")
        }
        return "something"
    }
    downloader()
}

Mặc dù cẩn thận, các thử nghiệm khác của bạn có thể thất bại nếu chúng kiểm tra chức năng của chức năng bạn ghi đè!

Các tác giả của Go sử dụng mẫu này trong thư viện chuẩn Go để chèn các móc kiểm tra vào mã để làm cho mọi thứ dễ kiểm tra hơn:

https://golang.org/src/net/hook.go

https://golang.org/src/net/dial.go#L248

https://golang.org/src/net/dial_test.go#L701


8
Downvote nếu bạn muốn, đây là một mẫu có thể chấp nhận được đối với các gói nhỏ để tránh nồi hơi liên quan đến DI. Biến chứa hàm chỉ "toàn cầu" đối với phạm vi của gói vì nó không được xuất. Đây là một lựa chọn hợp lệ, tôi đã đề cập đến nhược điểm, chọn cuộc phiêu lưu của riêng bạn.
Jake

4
Một điều cần lưu ý là hàm được định nghĩa theo cách này không thể được đệ quy.
Ben Sandler

2
Tôi đồng ý với @Jake rằng phương pháp này có chỗ đứng của nó.
m.kocikowski

11

Tôi đang sử dụng một cách tiếp cận hơi khác trong đó các phương thức cấu trúc công cộng thực hiện các giao diện nhưng logic của chúng bị giới hạn chỉ bao bọc các hàm riêng (không được báo cáo) lấy các giao diện đó làm tham số. Điều này cung cấp cho bạn mức độ chi tiết mà bạn sẽ cần để chế nhạo hầu như mọi phụ thuộc và vẫn có API sạch để sử dụng từ bên ngoài bộ thử nghiệm của bạn.

Để hiểu điều này, bắt buộc phải hiểu rằng bạn có quyền truy cập vào các phương thức không được báo cáo trong trường hợp thử nghiệm của bạn (nghĩa là từ trong _test.gotệp của bạn ) để bạn kiểm tra chúng thay vì kiểm tra các phương thức xuất khẩu không có logic bên trong gói.

Tóm lại: kiểm tra các hàm không được báo cáo thay vì kiểm tra các hàm đã xuất!

Hãy làm một ví dụ. Giả sử chúng ta có cấu trúc API Slack có hai phương thức:

  • các SendMessagephương pháp đó sẽ gửi một yêu cầu HTTP đến một webhook Slack
  • các SendDataSynchronouslyphương pháp đó được đưa ra một lát chuỗi lặp trên họ và gọi SendMessagecho mỗi lần lặp

Vì vậy, để kiểm tra SendDataSynchronouslymà không thực hiện yêu cầu HTTP mỗi lần chúng ta sẽ phải giả SendMessage, phải không?

package main

import (
    "fmt"
)

// URI interface
type URI interface {
    GetURL() string
}

// MessageSender interface
type MessageSender interface {
    SendMessage(message string) error
}

// This one is the "object" that our users will call to use this package functionalities
type API struct {
    baseURL  string
    endpoint string
}

// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
    return api.baseURL + api.endpoint
}

// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy 
func (api *API) SendMessage(message string) error {
    return sendMessage(api, message)
}

// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
    return sendDataSynchronously(api, data)
}

// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
    fmt.Println("This function won't get called because we will mock it")
    return nil
}

// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
    for _, text := range data {
        err := sender.SendMessage(text)

        if err != nil {
            return err
        }
    }

    return nil
}

// TEST CASE BELOW

// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
    err      error
    messages []string
}

// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
    // let's store all received messages for later assertions
    sender.messages = append(sender.messages, message)

    return sender.err // return error for later assertions
}

func TestSendsAllMessagesSynchronously() {
    mockedMessages := make([]string, 0)
    sender := mockedSender{nil, mockedMessages}

    messagesToSend := []string{"one", "two", "three"}
    err := sendDataSynchronously(&sender, messagesToSend)

    if err == nil {
        fmt.Println("All good here we expect the error to be nil:", err)
    }

    expectedMessages := fmt.Sprintf("%v", messagesToSend)
    actualMessages := fmt.Sprintf("%v", sender.messages)

    if expectedMessages == actualMessages {
        fmt.Println("Actual messages are as expected:", actualMessages)
    }
}

func main() {
    TestSendsAllMessagesSynchronously()
}

Điều tôi thích về cách tiếp cận này là bằng cách xem xét các phương pháp không được báo cáo, bạn có thể thấy rõ các phụ thuộc là gì. Đồng thời, API mà bạn xuất sẽ sạch hơn rất nhiều và có ít tham số hơn để vượt qua vì sự phụ thuộc thực sự ở đây chỉ là trình nhận cha mẹ đang thực hiện tất cả các giao diện đó. Tuy nhiên, mọi chức năng đều có khả năng chỉ phụ thuộc vào một phần của nó (một, có thể là hai giao diện) giúp cho việc tái cấu trúc dễ dàng hơn rất nhiều. Thật tuyệt khi thấy cách mã của bạn thực sự được ghép nối chỉ bằng cách xem các chữ ký hàm, tôi nghĩ rằng nó tạo ra một công cụ mạnh mẽ chống lại mùi mã.

Để làm cho mọi thứ dễ dàng, tôi đặt mọi thứ vào một tệp để cho phép bạn chạy mã trong sân chơi ở đây nhưng tôi khuyên bạn cũng nên xem ví dụ đầy đủ trên GitHub, đây là tệp slack.go và ở đây là slack_test.go .

đây là toàn bộ :)


Đây thực sự là một cách tiếp cận thú vị và thông tin về việc có quyền truy cập vào các phương thức riêng tư trong tệp thử nghiệm thực sự hữu ích. Nó làm tôi nhớ đến kỹ thuật pimpl trong C ++. Tuy nhiên, tôi nghĩ nên nói rằng thử nghiệm các chức năng riêng tư là nguy hiểm. Các thành viên tư nhân thường được coi là chi tiết thực hiện và có nhiều khả năng thay đổi theo thời gian hơn giao diện công cộng. Tuy nhiên, miễn là bạn chỉ kiểm tra các trình bao bọc riêng xung quanh giao diện công cộng, bạn sẽ ổn thôi.
c1moore

Vâng nói chung tôi đồng ý với bạn. Trong trường hợp này mặc dù các cơ quan phương thức riêng hoàn toàn giống với các phương thức công khai, do đó bạn sẽ kiểm tra chính xác điều tương tự. Sự khác biệt duy nhất giữa hai là các đối số chức năng. Đó là mẹo cho phép bạn tiêm bất kỳ sự phụ thuộc nào (bị chế giễu hoặc không) khi cần thiết.
Francesco Casula

Vâng tôi đồng ý. Tôi chỉ nói miễn là bạn giới hạn nó trong các phương pháp riêng tư bao bọc những phương thức công khai đó, bạn nên đi. Chỉ không bắt đầu thử nghiệm các phương thức riêng là chi tiết triển khai.
c1moore

7

Tôi sẽ làm một cái gì đó như,

Chủ yếu

var getPage = get_page
func get_page (...

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := getPage(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

Kiểm tra

func TestDownloader (t *testing.T) {
    origGetPage := getPage
    getPage = mock_get_page
    defer func() {getPage = origGatePage}()
    // The rest to be written
}

// define mock_get_page and rest of the codes
func mock_get_page (....

Và tôi sẽ tránh _ở golang. Sử dụng tốt hơn camelCase


1
sẽ có thể đi để phát triển một gói có thể làm điều này cho bạn. Tôi đang nghĩ một cái gì đó như : p := patch(mockGetPage, getPage); defer p.done(). Tôi mới đi, và đã cố gắng làm điều này bằng unsafethư viện, nhưng dường như không thể làm được trong trường hợp chung.
vitirus 8/11/2015

@Fallen đây gần như chính xác là câu trả lời của tôi được viết trong hơn một năm sau khi tôi.
Jake

1
1. Điểm tương đồng duy nhất là cách var toàn cầu. @Jake 2. Đơn giản là tốt hơn phức tạp. weberc2
Fallen

1
@fallen Tôi không coi ví dụ của bạn đơn giản hơn. Vượt qua các cuộc tranh luận không phức tạp hơn việc làm thay đổi trạng thái toàn cầu, nhưng dựa vào trạng thái toàn cầu sẽ đưa ra rất nhiều vấn đề không tồn tại. Ví dụ, bạn sẽ phải đối phó với các điều kiện cuộc đua nếu bạn muốn song song hóa các bài kiểm tra của mình.
weberc2

Nó gần giống nhau, nhưng nó không :). Trong câu trả lời này, tôi thấy cách gán hàm cho var và cách này cho phép tôi gán một triển khai khác cho các bài kiểm tra. Tôi không thể thay đổi các đối số trên hàm tôi đang kiểm tra, vì vậy đây là một giải pháp tốt cho tôi. Cách khác là sử dụng Bộ thu với cấu trúc giả, tôi chưa biết cái nào đơn giản hơn.
alexbt

0

Cảnh báo: Điều này có thể làm tăng kích thước tệp thực thi một chút và tốn một ít hiệu năng thời gian chạy. IMO, điều này sẽ tốt hơn nếu golang có tính năng như macro hoặc trang trí chức năng.

Nếu bạn muốn mô phỏng các chức năng mà không thay đổi API của nó, cách dễ nhất là thay đổi cách thực hiện một chút:

func getPage(url string) string {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

Bằng cách này, chúng ta thực sự có thể giả định một chức năng trong số các chức năng khác. Để thuận tiện hơn, chúng tôi có thể cung cấp nồi hơi chế nhạo như vậy:

// download.go
func getPage(url string) string {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

type MockHandler struct {
  GetPage func(url string) string
  Downloader func()
}

var m *MockHandler = new(MockHandler)

func Mock(handler *MockHandler) {
  m = handler
}

Trong tệp thử nghiệm:

// download_test.go
func GetPageMock(url string) string {
  // ...
}

func TestDownloader(t *testing.T) {
  Mock(&MockHandler{
    GetPage: GetPageMock,
  })

  // Test implementation goes here!

  Mock(new(MockHandler)) // Reset mocked functions
}

-2

Xem xét bài kiểm tra đơn vị là lĩnh vực của câu hỏi này, rất khuyến khích bạn sử dụng https://github.com/bouk/monkey . Gói này làm cho bạn thử nghiệm mà không thay đổi mã nguồn ban đầu của bạn. So với câu trả lời khác, nó "không xâm phạm" hơn

CHỦ YẾU

type AA struct {
 //...
}
func (a *AA) OriginalFunc() {
//...
}

KIỂM TRA

var a *AA

func NewFunc(a *AA) {
 //...
}

monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)

Mặt xấu là:

- Nhắc nhở bởi Dave.C, Phương pháp này không an toàn. Vì vậy, không sử dụng nó bên ngoài bài kiểm tra đơn vị.

- Là không thành ngữ Đi.

Mặt tốt là:

++ Không xâm phạm. Làm cho bạn làm mọi thứ mà không thay đổi mã chính. Giống như Thomas đã nói.

++ Làm cho bạn thay đổi hành vi của gói (có thể được cung cấp bởi bên thứ ba) với ít mã nhất.


1
Xin đừng làm điều này. Nó hoàn toàn không an toàn và có thể phá vỡ nhiều nội bộ khác nhau. Chưa kể nó không phải là thành ngữ Go từ xa.
Dave C

1
@DaveC Tôi tôn trọng kinh nghiệm của bạn về Golang, nhưng nghi ngờ ý kiến ​​của bạn. 1. An toàn không có nghĩa là tất cả để phát triển phần mềm, làm vấn đề giàu tính năng và tiện lợi. 2. Thành ngữ Golang không phải là Golang, là một phần của nó. Nếu một dự án là nguồn mở, thì những người khác thường chơi bẩn trên đó. Cộng đồng nên khuyến khích nó ít nhất là không đàn áp nó.
Frank Wang

2
Ngôn ngữ được gọi là Go. Bởi không an toàn, ý tôi là nó có thể phá vỡ thời gian chạy Go, những thứ như bộ sưu tập rác.
Dave C

1
Đối với tôi, không an toàn là mát mẻ cho một bài kiểm tra đơn vị. Nếu tái cấu trúc mã với nhiều 'giao diện' là cần thiết mỗi khi thực hiện kiểm tra đơn vị. Nó phù hợp với tôi hơn mà sử dụng một cách không an toàn để giải quyết nó.
Frank Wang

1
@DaveC Tôi hoàn toàn đồng ý rằng đây là một ý tưởng khủng khiếp (câu trả lời của tôi là câu trả lời được bình chọn và chấp nhận hàng đầu), nhưng để trở thành nhà mô phạm tôi không nghĩ rằng điều này sẽ phá vỡ GC vì Go GC là bảo thủ và có nghĩa là xử lý các trường hợp như thế này. Tôi sẽ rất vui khi được sửa chữa, tuy nhiên.
weberc2
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.