Go'da Mock işlevleri


155

Bağımlılıklar konusunda kafam karıştı. Bazı işlev çağrılarını sahte olanlarla değiştirebilmek istiyorum. İşte kodumun bir parçası:

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])
    }
}

downloader()Aslında http üzerinden bir sayfa almadan test edebilmek istiyorum - yani alay ederek get_page(sadece sayfa içeriğini bir dizge olarak döndürdüğü için daha kolay) veya http.Get().

Benzer bir sorunla ilgili görünen bu konuyu buldum . Julian Phillips bir çözüm olarak kitaplığı Withmock'u sunuyor , ancak işe yarayamıyorum . Dürüst olmak gerekirse, benim için büyük ölçüde kargo kült kodu olan test kodumun ilgili kısımları:

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
}

Test çıktısı aşağıdaki gibidir:

HATA: '_et / http' yüklenemedi: çıkış durumu 1 çıktı: paket yüklenemiyor: paket _et / http: paketler http (chunked.go) ve main (main_mock.go)
/ var / klasörler / z9 / içinde bulundu ql_yn5h550s6shtb9c5sggj40000gn / T / withmock570825607 / yol / src / _et / http

Withmock, test sorunum için bir çözüm mü? Çalışması için ne yapmalıyım?


Go birim testine daldığınız için, davranışa dayalı test yapmanın harika bir yolunu bulmak için GoConvey'e bakın ... ve bilgi: otomatik olarak güncellenen ve yerel "go test" testleriyle de çalışan bir web kullanıcı arayüzü geliyor.
Matt

Yanıtlar:


199

Şahsen ben kullanmıyorum gomock(veya bu konuda herhangi bir alay çerçevesi; Go'da alay etmek onsuz çok kolay). Ya downloader()bir parametre olarak işleve bir bağımlılık geçirirdim ya da downloader()bir tür üzerinde bir yöntem yapardım ve tür get_pagebağımlılığı tutabilir :

Yöntem 1: get_page()Parametre olarak geçirindownloader()

type PageGetter func(url string) string

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

Ana:

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

func main() {
    downloader(get_page)
}

Ölçek:

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

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

Yöntem2: download()Bir tür yöntemi yapın Downloader:

Bağımlılığı bir parametre olarak geçirmek istemiyorsanız get_page(), bir türün bir üyesi de yapabilir ve download()bu türden bir yöntem oluşturabilir ve ardından şunları kullanabilirsiniz 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)
    //...
}

Ana:

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

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

Ölçek:

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

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

4
Çok teşekkürler! İkincisi ile gittim. (alay etmek istediğim başka işlevler de vardı, bu yüzden onları bir yapıya atamak daha kolaydı) Btw. Go'da biraz aşığım. Özellikle eşzamanlılık özellikleri temiz!
GolDDranks

160
Test uğruna ana kodu / işlev imzasını değiştirmemiz gerektiğini bulan tek kişi ben miyim?
Thomas

43
@Thomas Tek siz misiniz emin değilim, ancak aslında test odaklı geliştirmenin temel nedeni bu - testleriniz üretim kodunuzu yazma şeklinize rehberlik ediyor. Test edilebilir kod daha modülerdir. Bu durumda, Downloader nesnesinin 'get_page' davranışı artık takılabilirdir - uygulamasını dinamik olarak değiştirebiliriz. Yalnızca ilk başta kötü yazılmışsa ana kodunuzu değiştirmeniz gerekir.
weberc2

21
@Thomas İkinci cümlenizi anlamıyorum. TDD daha iyi kod kullanır. Kodunuz test edilebilir olması için değişir (çünkü test edilebilir kod, iyi düşünülmüş arabirimlerle mutlaka modülerdir), ancak asıl amaç daha iyi koda sahip olmaktır - otomatikleştirilmiş testlere sahip olmak harika bir ikincil avantajdır. Endişeniz, işlevsel kodun sadece gerçeğin ardından testler eklemek için değiştirilmesiyse, yine de değiştirmenizi tavsiye ederim çünkü bir gün birisinin bu kodu okumak veya değiştirmek isteyeceği iyi bir olasılık var.
weberc2

6
@Thomas, eğer testlerinizi devam ederken yazıyorsanız, bu bilmeceyle uğraşmak zorunda kalmayacaksınız.
weberc2

25

Bunun yerine bir değişken kullanmak için işlev tanımınızı değiştirirseniz:

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

Bunu testlerinizde geçersiz kılabilirsiniz:

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

Yine de dikkatli olun, geçersiz kıldığınız işlevin işlevselliğini test ederlerse diğer testleriniz başarısız olabilir!

Go yazarları, işleri test etmeyi kolaylaştırmak için koda test kancaları eklemek için Go standart kitaplığında bu deseni kullanır:


8
İsterseniz olumsuz oy kullanın, bu, DI ile ilişkili ortak metinden kaçınmak için küçük paketler için kabul edilebilir bir kalıptır. Fonksiyonu içeren değişken, dışa aktarılmadığı için paketin kapsamına yalnızca "global" dir. Bu geçerli bir seçenek, dezavantajından bahsetmiştim, kendi maceranı seç.
Jake

4
Nota bir şey bu şekilde tanımlanmış olduğu fonksiyonudur edemez özyinelemeli olun.
Ben Sandler

2
Bu yaklaşımın yeri olduğu konusunda @Jake'e katılıyorum.
m.kocikowski

1
"Yine de dikkatli olun, geçersiz kıldığınız işlevin işlevselliğini test ederlerse diğer testleriniz başarısız olabilir!" Geçersiz kılmadan önce orijinal işlevi depolamak ve testten sonra geri yüklemek yardımcı olacaktır. get_page_orig :=.get_page /* override get_page and test */ get_page =get_page_orig
Vivek Maran

11

Genel yapı yöntemlerinin arabirimleri uyguladığı ancak mantıklarının yalnızca bu arabirimleri parametre olarak alan özel (dışa aktarılmamış) işlevleri sarmalamakla sınırlı olduğu biraz farklı bir yaklaşım kullanıyorum . Bu size, neredeyse tüm bağımlılıklarla dalga geçmek için ihtiyaç duyacağınız ayrıntı düzeyini sağlar ve yine de test paketinizin dışından kullanabileceğiniz temiz bir API'ye sahip olursunuz.

Bunu anlamak için, test durumunuzdaki aktarılmamış yöntemlere (yani _test.godosyalarınızın içinden) erişiminizin olduğunu anlamanız zorunludur, böylece sarmalamadan başka içinde mantık bulunmayan dışa aktarılanları test etmek yerine bunları test edersiniz.

Özetlemek gerekirse: dışa aktarılanları test etmek yerine dışa aktarılmayan işlevleri test edin!

Bir örnek verelim. İki yöntemi olan bir Slack API yapımız olduğunu varsayalım:

  • SendMessagegevşek bir webhook bir HTTP isteği gönderir yöntem
  • SendDataSynchronouslyÜzerlerine dizeleri dolaşır bir dilim verildi ve aramaları yöntemi SendMessageher yineleme için

Öyleyse, SendDataSynchronouslyher seferinde bir HTTP isteğinde bulunmadan test etmek için alay etmemiz gerekir SendMessage, değil mi?

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()
}

Bu yaklaşımla ilgili sevdiğim şey, aktarılmamış yöntemlere bakarak bağımlılıkların ne olduğunu açıkça görebilmenizdir. Aynı zamanda, dışa aktardığınız API çok daha temizdir ve iletilmesi gereken daha az parametre vardır, çünkü buradaki gerçek bağımlılık sadece tüm bu arayüzleri uygulayan ana alıcıdır. Yine de her işlev potansiyel olarak yalnızca bir parçasına (bir, belki iki arayüz) bağlıdır ve bu da yeniden düzenleyicileri çok daha kolay hale getirir. Sadece fonksiyon imzalarına bakarak kodunuzun gerçekten nasıl bağlandığını görmek güzel, bence kod kokulu hale karşı güçlü bir araç.

İşleri kolaylaştırmak için, kodu burada oyun alanında çalıştırmanıza izin vermek için her şeyi tek bir dosyaya koyuyorum ama GitHub'daki tam örneğe de bakmanızı öneririm, işte slack.go dosyası ve burada slack_test.go .

Ve işte her şey.


Bu aslında ilginç bir yaklaşımdır ve test dosyasındaki özel yöntemlere erişimle ilgili bilgi gerçekten yararlıdır. Bana C ++ 'daki pimpl tekniğini hatırlatıyor. Ancak, özel fonksiyonları test etmenin tehlikeli olduğu söylenmelidir. Özel üyeler genellikle uygulama ayrıntıları olarak kabul edilir ve zaman içinde genel arayüze göre değişme olasılığı daha yüksektir. Özel sarmalayıcıları yalnızca genel arayüz etrafında test ettiğiniz sürece, iyi olmalısınız.
c1moore

Evet, genel olarak konuşursak sana katılıyorum. Bu durumda, özel yöntem organları herkese açık olanlarla tamamen aynı olsa da, tamamen aynı şeyi test edeceksiniz. İkisi arasındaki tek fark fonksiyon argümanlarıdır. Gerektiği gibi herhangi bir bağımlılık (alay konusu olsun veya olmasın) enjekte etmenize izin veren numara budur.
Francesco Casula

Evet katılıyorum. Ben sadece bunu halka açık olanları saran özel yöntemlerle sınırladığınız sürece, gitmekte iyi olmalısınız diyordum. Uygulama ayrıntıları olan özel yöntemleri test etmeye başlamayın.
c1moore

7

Ben şöyle bir şey yapardım

Ana

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])
    }
}

Ölçek

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 (....

Ve _golang'dan kaçınırdım. CamelCase kullansan iyi olur


1
Bunu sizin için yapabilecek bir paket geliştirmek mümkün olabilir mi? Ben böyle bir şey düşünüyorum: p := patch(mockGetPage, getPage); defer p.done(). Gitmek için yeniyim ve bunu unsafekütüphaneyi kullanarak yapmaya çalışıyordum , ancak genel durumda yapmak imkansız görünüyor.
vitiral

@ Fallen bu neredeyse tam olarak cevabımın cevabımdan bir yıl sonra yazıldı.
Jake

1
1. Tek benzerlik global değişken yoludur. @ Jake 2. Basit, karmaşıktan daha iyidir. weberc2
Fallen

1
@fallen Örneğinizin daha basit olduğunu düşünmüyorum. Argümanları aktarmak, küresel devleti mutasyona uğratmaktan daha karmaşık değildir, ancak küresel duruma güvenmek, başka türlü var olmayan birçok sorunu beraberinde getirir. Örneğin, testlerinizi paralel hale getirmek istiyorsanız yarış koşullarıyla ilgilenmeniz gerekecektir.
weberc2

Neredeyse aynı, ama değil :). Bu cevapta bir var'a nasıl fonksiyon atayacağımı ve bunun testler için farklı bir uygulama atamama nasıl izin verdiğini görüyorum. Test ettiğim fonksiyondaki argümanları değiştiremiyorum, bu yüzden bu benim için güzel bir çözüm. Alternatif, Receiver'ı mock struct ile kullanmaktır, hangisinin daha basit olduğunu henüz bilmiyorum.
alexbt

0

Uyarı: Bu, yürütülebilir dosya boyutunu biraz artırabilir ve biraz çalışma zamanı performansına mal olabilir. IMO, golang'ın makro veya işlev dekoratörü gibi bir özelliği varsa bu daha iyi olurdu.

API'sini değiştirmeden işlevlerle dalga geçmek istiyorsanız, en kolay yol, uygulamayı biraz değiştirmektir:

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

Bu şekilde aslında bir işlevi diğerlerinden alay edebiliriz. Daha rahat olması için, bu tür alaycı standart metinler sağlayabiliriz:

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

Test dosyasında:

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

Birim testinin bu sorunun alanı olduğu düşünüldüğünde maymun kullanmanızı şiddetle tavsiye ederiz . Bu Paket, orijinal kaynak kodunuzu değiştirmeden alay testi yapmanızı sağlar. Diğer yanıtla karşılaştırın, daha "müdahaleci değil".

ana

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

alay testi

var a *AA

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

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

Kötü taraf:

  • Dave.C tarafından hatırlatıldı, Bu yöntem güvenli değildir. Bu nedenle, birim testi dışında kullanmayın.
  • Deyimsel olmayan Go.

İyi tarafı:

  • Müdahaleci değildir. Ana kodu değiştirmeden bir şeyler yapmanızı sağlayın. Thomas'ın dediği gibi.
  • En az kodla paketin davranışını (belki üçüncü şahıs tarafından sağlanmıştır) değiştirmenizi sağlar.

1
Lütfen bunu yapmayın. Tamamen güvensizdir ve çeşitli Go dahili özelliklerini bozabilir. Go'nun uzaktan bile deyimsel olmadığından bahsetmiyorum bile.
Dave C

1
@DaveC Golang hakkındaki deneyiminize saygı duyuyorum, ancak fikrinizden şüpheleniyorum. 1. Güvenlik, yazılım geliştirme için her şey anlamına gelmez, zengin özelliklere ve rahatlığa önem verir. 2. Deyimsel Golang Golang değil, bunun bir parçası. Bir proje açık kaynaklı ise, diğer insanların üzerinde kirli oynaması yaygındır. Topluluk onu en azından bastırmamaya teşvik etmelidir.
Frank Wang

2
Dilin adı Go. Güvensiz derken, çöp toplama gibi Go çalışma zamanını bozabileceğini söylüyorum.
Dave C

1
Bana göre, bir birim testi için güvensizlik iyidir. Her birim testi yapıldığında daha fazla "arayüz" ile kodun yeniden düzenlenmesi gerekiyorsa. Bunu çözmek için güvenli olmayan bir yol kullanan bana daha çok uyuyor.
Frank Wang

1
@DaveC Bunun korkunç bir fikir olduğuna tamamen katılıyorum (cevabım en çok oylanan ve kabul edilen cevap), ancak bilgiçlikçi olmak için bunun GC'yi bozacağını düşünmüyorum çünkü Go GC muhafazakar ve bunun gibi davaları ele almak istiyor. Yine de düzeltilmekten mutlu olurum.
weberc2
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.