unit-testing - 如何在另一个方法中存根
问题描述
我正在编写一个网络应用程序,它将向第三方服务发送请求以进行一些计算,然后将其发送回前端。
这是我正在尝试编写的测试的相关部分。
客户端.go
func (c *ClientResponse) GetBankAccounts() (*BankAccounts, *RequestError) {
req, _ := http.NewRequest("GET", app.BuildUrl("bank_accounts"), nil)
params := req.URL.Query()
params.Add("view", "standard_bank_accounts")
req.URL.RawQuery = params.Encode()
c.ClientDo(req)
if c.Err.Errors != nil {
return nil, c.Err
}
bankAccounts := new(BankAccounts)
defer c.Response.Body.Close()
if err := json.NewDecoder(c.Response.Body).Decode(bankAccounts); err != nil {
return nil, &RequestError{Errors: &Errors{Error{Message: "failed to decode Bank Account response body"}}}
}
return bankAccounts, nil
}
助手.go
type ClientResponse struct {
Response *http.Response
Err *RequestError
}
type ClientI interface {
ClintDo(req *http.Request) (*http.Response, *RequestError)
}
func (c *ClientResponse) ClientDo(req *http.Request) {
//Do some authentication with third-party service
errResp := *new(RequestError)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
// Here I'm repourposing the third-party service's error response mapping
errResp.Errors.Error.Message = "internal server error. failed create client.Do"
}
c.Response = resp
c.Err = &errResp
}
我只想测试该GetBankAccounts()
方法,所以我想存根 ClientDo
,但我不知道如何做到这一点。这是我到目前为止的测试用例。
client_test.go
type StubClientI interface {
ClintDo(req *http.Request) (*http.Response, *RequestError)
}
type StubClientResponse struct {}
func (c *StubClientResponse) ClientDo(req *http.Request) (*http.Response, *RequestError) {
return nil, nil
}
func TestGetBankAccounts(t *testing.T) {
cr := new(ClientResponse)
accounts, err := cr.GetBankAccounts()
if err != nil {
t.Fatal(err.Errors)
}
t.Log(accounts)
}
ClintDo
仍然指向helper.go上的实际方法,我怎样才能让它在测试中使用 on ?
更新:我也尝试了以下方法,但这也不起作用,它仍然将请求发送到实际的第三方服务。
client_test.go
func TestGetBankAccounts(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, toJson(append(BankAccounts{}.BankAccounts, BankAccount{
Url: "https://foo.bar/v2/bank_accounts/1234",
Name: "Test Bank",
})))
}))
server := httptest.NewServer(mux)
cr := new(ClientResponse)
cr.Client = server.Client()
accounts, err := cr.GetBankAccounts()
if err != nil {
t.Fatal(err.Errors)
}
t.Log(accounts)
}
助手.go
type ClientResponse struct {
Client *http.Client
Response *http.Response
Err *RequestError
}
type ClientI interface {
ClintDo(req *http.Request) (*http.Response, *RequestError)
}
func (c *ClientResponse) ClientDo(req *http.Request) {
//Do some authentication with third-party service
errResp := *new(RequestError)
client := c.Client
resp, err := client.Do(req)
if err != nil {
// Here I'm repourposing the third-party service's error response mapping
errResp.Errors.Error.Message = "internal server error. failed create client.Do"
}
c.Response = resp
c.Err = &errResp
}
更新 2
我能够从@dm03514 的回答中取得一些进展,但不幸的是,现在我在测试中得到了 nil 指针异常,但在实际代码中却没有。
客户端.go
func (c *ClientResponse) GetBankAccounts() (*BankAccounts, *RequestError) {
req, _ := http.NewRequest("GET", app.BuildUrl("bank_accounts"), nil)
params := req.URL.Query()
params.Add("view", "standard_bank_accounts")
req.URL.RawQuery = params.Encode()
//cr := new(ClientResponse)
c.HTTPDoer.ClientDo(req)
// Panic occurs here
if c.Err.Errors != nil {
return nil, c.Err
}
bankAccounts := new(BankAccounts)
defer c.Response.Body.Close()
if err := json.NewDecoder(c.Response.Body).Decode(bankAccounts); err != nil {
return nil, &RequestError{Errors: &Errors{Error{Message: "failed to decode Bank Account response body"}}}
}
return bankAccounts, nil
}
助手.go
type ClientResponse struct {
Response *http.Response
Err *RequestError
HTTPDoer HTTPDoer
}
type HTTPDoer interface {
//Do(req *http.Request) (*http.Response, *RequestError)
ClientDo(req *http.Request)
}
type ClientI interface {
}
func (c *ClientResponse) ClientDo(req *http.Request) {
// This method hasn't changed
....
}
client_test.go
type StubDoer struct {
*ClientResponse
}
func (s *StubDoer) ClientDo(req *http.Request) {
s.Response = &http.Response{
StatusCode: 200,
Body: nil,
}
s.Err = nil
}
func TestGetBankAccounts(t *testing.T) {
sd := new(StubDoer)
cr := new(ClientResponse)
cr.HTTPDoer = HTTPDoer(sd)
accounts, err := cr.GetBankAccounts()
if err != nil {
t.Fatal(err.Errors)
}
t.Log(accounts)
}
=== RUN TestGetBankAccounts
--- FAIL: TestGetBankAccounts (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x12aae69]
解决方案
有两种常见的方法来实现这一点:
- 使用接口的依赖注入(你的例子)
- Custom
http.Transport
,它有一个可以在单元测试中覆盖的钩子
看起来您已经接近接口方法,并且缺乏配置具体实现的显式方法。考虑一个与您的 ClientDo 类似的接口:
type HTTPDoer interface {
Do func(req *http.Request) (*http.Response, *RequestError)
}
依赖注入让调用者配置依赖并将它们传递到实际调用这些依赖的资源中。在这种情况下,您的ClientResponse
结构将引用 a HTTPDoer
:
type ClientResponse struct {
Response *http.Response
Err *RequestError
HTTPDoer HTTPDoer
}
这允许调用者配置将调用的具体实现ClientResponse
。在生产中这将是实际的http.Client
,但在测试中它可能是实现该Do
功能的任何东西。
type StubDoer struct {}
func (s *StubDoer) Do(....)
单元测试可以配置StubDoer
,然后调用GetBankAccounts
,然后进行断言:
sd := &StubDoer{...}
cr := ClientResponse{
HTTPDoer: sd,
}
accounts, err := cr.GetBankAccounts()
// assertions
它被称为依赖注入的原因是调用者初始化资源(StubDoer
),然后将该资源提供给目标(ClientResponse
)。ClientResponse
对 的具体实现一无所知HTTPDoer
,只知道它遵守接口!
我写了一篇博客文章,详细介绍了单元测试上下文中的依赖注入。
推荐阅读
- regex - Ansible通过逐行循环文件来使用正则表达式替换IP地址
- sql - 显示一行有一个时间戳和一个对应的 x 整数数组,显示为 x 行,有 x 个时间戳和一个对应的整数
- graph-databases - 在处理可选字段时在 gremlin 中使用注入进行长遍历
- c++ - 在终端(macOS)中编译 hello world.cpp
- c# - 如何隐藏 Html.ActionLink 页面加载
- azure-iot-hub - 如何在 AWS IOT 和 Azure IOT HUB 中查看 MQTT 数据
- arrays - 如何在 Angular 7 打字稿中合并 2 个数组
- angular - 如何存储 API-Url 以便我可以在 dist 文件夹中轻松更改
- vb.net - VB.Net DataGridView Bindingsource.Filter ,如果没有找到则显示空白行
- ansible - 如何使用 playbook 将 .iso 文件复制到 VMware 数据存储