Как заглушить метод внутри другого

Я пишу веб-приложение, которое будет отправлять запросы в стороннюю службу для выполнения некоторых расчетов и отправлять их обратно на фронт.

Вот соответствующие части для теста, который я пытаюсь написать.

client.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
}

helper.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)
}

helper.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, но, к сожалению, теперь я получаю исключения с нулевым указателем в тесте, но не в реальном коде.

client.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
}

helper.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]

person Praveen Premaratne    schedule 06.06.2020    source источник
comment
Обычный подход к заглушке HTTP-запросов заключается в предоставлении *http.Client в качестве аргумента с возможным возвратом к http.DefaultClient, когда предоставленный клиент равен нулю. В тестах используется клиент тестового сервера или клиент, настроенный на возврат стандартных ответов через пользовательский транспорт. Этот общий подход не заглушает запросы методов в вопросе, но, возможно, он соответствует вашим потребностям.   -  person thwd    schedule 07.06.2020
comment
@thwd спасибо за ответ, это похоже на то, что вы предлагаете? stackoverflow.com/questions/53360256/   -  person Praveen Premaratne    schedule 07.06.2020
comment
Или, если бы вы могли привести пример, который был бы очень признателен, в основном потому, что большинство ответов для тестирования Go никогда не работают для меня. Пример, как тот, что на образце выше.   -  person Praveen Premaratne    schedule 07.06.2020


Ответы (1)


Есть два распространенных способа добиться этого:

  • Внедрение зависимостей с использованием интерфейсов (ваш пример)
  • Пользовательский http.Transport, у которого есть хук, который вы можете переопределить в своих модульных тестах.

Похоже, вы близки к подходу к интерфейсу и вам не хватает явного способа настройки конкретной реализации. Рассмотрим интерфейс, похожий на ваш ClientDo:

type HTTPDoer interface {
  Do func(req *http.Request) (*http.Response, *RequestError)
}

Внедрение зависимостей заставляет вызывающую сторону настраивать зависимости и передавать их в ресурсы, которые фактически вызывают эти зависимости. В этом случае ваша структура ClientResponse будет иметь ссылку на 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, только то, что она придерживается интерфейса!


Я написал сообщение в блоге, в котором подробно описывается внедрение зависимостей в контексте модульных тестов.

person dm03514    schedule 06.06.2020
comment
Спасибо, что помогли мне с этим. Я смог добиться некоторого прогресса в этом, но теперь я получаю исключение с нулевым указателем на яичках, но не в самом методе. Я обновил пост новой версией кода. - person Praveen Premaratne; 07.06.2020
comment
Мне удалось это исправить, это я был глуп и не возвращал ответ, а вместо этого назначал переданному получателю. Теперь я изменил сигнатуру метода на ту, что у вас есть, и она работает. - person Praveen Premaratne; 07.06.2020