我正在用 Golang 编写一个注册路由测试。我有点被模拟困住了。来自 Rails 和 Rspec,您执行了以下操作
allow_any_instance_of(EmailDelivery).to receive(:send)
并且电子邮件未发送
在 go 中,我使用 EmailDelivery 结构和方法
Send
我想在测试中模拟这个方法,不做任何事情
这里需要使用接口吗?还有其他“神奇”的方式吗,或者它在 Go 中就不是惯用的了
目前,我有 struct
Router
,我在其中注册路由 - 像这样(使用 mux 路由器)
router.HandleFunc('/register', WithCors(router.RegisterHandler)).Methods(method, "OPTIONS")
看来我应该使用方法
EmailDelivery
和结构 RealEmailDelivery 创建一个接口 Send
。然后在测试中,我应该创建新的结构 FakeEmailDelivery 并实现 Send 方法而不执行任何操作。这样可以吗?
从那里开始,我看到两个选择:
router.RegisterHandler(w, req, fakeEmailDelivery)
您对这两种选择有何看法?您将如何以最佳方式组织这段代码?
提前致谢
基本上像往常一样:针对接口进行开发,编写这些接口的模拟实现并在单元测试中使用它。
为了保持您的示例,让我们看一下邮件程序。
type Mailer interface {
Send(to []string, subject, body string) bool
}
现在,您的处理程序使用此邮件程序:
type FooHandler struct {
Mailer mailer
}
func(h *FooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
mailer.Send([]string{"[email protected]"},"Test", "Just a test.")
}
在单元测试中,您现在可以实现 Mock 或 Stub 或使用 https://github.com/stretchr/testify 的 mock 包:
type MockMailer struct {
mock.Mock
}
func (m *MockMailer) Send(to, cc, bcc []string, subject, body string) {
// Setup your mock here
}
func TestHandler(t *testing.T){
h := FooHandler(Mailer: new(MockMailer))
// Do what you must
}
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const (
recipient = "[email protected]"
)
// Mailer is a heavily simplified interface for sending emails.
// In a real world scenario, you would probably have a LOT more parameters.
type Mailer interface {
Send(to []string, subject, body string) bool
}
// FooHandler is a simple HTTP handler that sends an email and
// stands in for a more complex handler.
type FooHandler struct {
mailer Mailer
}
// Message is a simple struct that holds the email subject and body.
// Mainly used for illustration purposes.
type Message struct {
Subject string
Body string
}
// ServeHTTP implements the http.Handler interface.
// It decodes the request body into a Message struct and sends it via the Mailer.
// In a real world scenario, you would probably have a lot more error handling.
func (h *FooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var msg Message
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, "Bad Request: "+err.Error(), http.StatusBadRequest)
return
}
if sent := h.mailer.Send([]string{recipient}, msg.Subject, msg.Body); !sent {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
// MockMailer is a mock implementation of the Mailer interface.
// Here is where the "magic" happens. We use the testify/mock package
// to create a mock implementation of the Mailer interface.
// The mock implementation is then used in our tests to assert
// that the correct methods are called with the correct parameters.
type MockMailer struct {
mock.Mock
}
// Send is the mock implementation of the Mailer interface.
// It asserts that the correct parameters are passed to the method
// and returns a predefined value.
func (m *MockMailer) Send(to []string, subject, body string) bool {
args := m.Called(to, subject, body)
return args.Bool(0)
}
func TestHandler(t *testing.T) {
mailer := new(MockMailer)
ok := mailer.On("Send", []string{recipient}, "Test", "Just a test.").Return(true)
fail := mailer.On("Send", []string{recipient}, "Fail", "Will fail").Return(false)
testCases := []struct {
desc string
mock *mock.Call
message Message
expectedStatus int
}{
{
desc: "Successfull mailing",
mock: ok,
message: Message{Subject: "Test", Body: "Just a test."},
expectedStatus: http.StatusOK,
},
{
desc: "Failed mailing",
mock: fail,
message: Message{Subject: "Fail", Body: "Will fail"},
expectedStatus: http.StatusInternalServerError,
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
json, err := json.Marshal(tC.message)
assert.NoError(t, err)
handler := &FooHandler{mailer: mailer}
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(json)))
assert.Equal(t, tC.expectedStatus, recorder.Code)
})
}
}