Насмешливые объекты A и B, когда метод A возвращает B в Go - PullRequest
0 голосов
/ 13 декабря 2018

Я пытаюсь реализовать модульные тесты в Go для существующей службы, которая использует структуру пула соединений и структуру соединения из существующей библиотеки (вызовите эти LibraryPool и LibraryConnection) для подключения к внешней службе.Чтобы использовать их, сервисные функции в основном коде используют уникальный глобальный экземпляр пула, который имеет метод GetConnection(), например:

// Current Main Code
var pool LibraryPool // global, instantiated in main()

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ... 
}

func main() {
  pool := makePool() // builds and returns a LibraryPool
  // sets up endpoints that use the service functions as handlers
  // ...
}

Я хотел бы выполнить эти юнит-тестысервисные функции без подключения к внешнему сервису, и поэтому я хотел бы посмеяться над LibraryPool и LibraryConnection.Чтобы учесть это, я думал об изменении основного кода на что-то вроде этого:

// Tentative New Main Code
type poolInterface interface {
  GetConnection() connInterface
}

type connInterface interface {
  Do(command string)
}

var pool poolInterface

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ...
}

func main() {
  pool := makePool() // still builds a LibraryPool
}

В тестах я бы использовал фиктивные реализации MockPool и MockConnection этих интерфейсов и глобальныйПеременная pool будет создана с использованием MockPool.Я бы создал этот глобальный pool в функции setup() внутри функции TestMain () .

Проблема в том, что в новом главном коде LibraryPool неправильно реализует poolInterface, поскольку GetConnection() возвращает connInterface вместо LibraryConnection (даже если LibraryConnection является допустимой реализацией connInterface).

Чтобыло бы хорошим способом приблизиться к этому виду тестирования?Кстати, основной код тоже гибкий.

1 Ответ

0 голосов
/ 14 декабря 2018

Ну, я постараюсь ответить, полностью объяснив, как я вижу этот дизайн.Извините заранее, если это слишком много, и не в точку ..

  • Entity / Domain
    • Ядро приложения, будет включать в себя структуру объекта, не будет импортировать ЛЮБОЙпакет внешнего слоя, но может быть импортирован любым пакетом (почти)
  • Приложение / вариант использования
    • «Служба».Будет отвечать главным образом за логику приложения, не будет знать о транспорте (http), будет «общаться» с БД через интерфейс.Здесь вы можете проверить домен, например, если ресурс не найден или текст слишком короткий.Все, что связано с бизнес-логикой.
  • transport
    • Будет обрабатывать запрос http, декодировать запрос, получать службу, выполняющую его задачи, и кодировать ответ.Здесь вы можете вернуть 401, если в запросе отсутствует обязательный параметр, или пользователь не авторизован, или что-то ...
  • инфраструктура
    • подключение к БД
    • Может быть, какой-нибудь http-движок, маршрутизатор и прочее.
    • Абсолютно независим от приложений, не импортируйте никакой внутренний пакет, даже Pseron

Например, допустим, мы хотим сделать что-то столь же простое, как вставить person в базу данных.

package person будет включать в себя только структуру struct

package person

type Person struct{
  name string
}

func New(name string) Person {
  return Person{
    name: name,
  {
}

О базе данных, скажем, выиспользуйте sql, я рекомендую сделать пакет с именем sql для обработки репо.(если вы используете postgress, используйте пакет postgress ...).

personRepo получит dbConnection, который будет инициализирован в main и реализует DBAndler.только соединение будет «общаться» с БД напрямую, главная цель хранилища - быть шлюзом к БД и говорить в терминах приложения.(соединение не зависит от приложения)

package sql

type DBAndler interface{
  exec(string, ...interface{}) (int64, error)
}

type personRepo struct{
  dbHandler DBHandler
}

func NewPersonRepo(dbHandler DBHandler) &personRepo {
  return &personRepo{
    dbHandler: dbHandler,
  }
}

func (p *personRepo) InsertPerson(p person.Person) (int64, error) {
  return p.dbHandler.Exec("command to insert person", p)
}

Служба получит этот репозиторий как зависимость (как интерфейс) в initailzer и будет взаимодействовать с ним для выполнения бизнес-логики

package service

type PersonRepo interface{
  InsertPerson(person.Person) error
}

type service struct {
  repo PersonRepo
}

func New(repo PersonRepo) *service {
  return &service{
    repo: repo
  }
}

func (s *service) AddPerson(name string) (int64, error) {
  person := person.New(name)
  return s.repo.InsertPerson(person)
}

Ваш обработчик транспорта будет инициализирован со службой как зависимость, и он обработает запрос http.

package http

type Service interface{
  AddPerson(name string) (int64, error)
}

type handler struct{
  service Service
}

func NewHandler(s Service) *handler {
  return &handler{
    service: s,
  }
}

func (h *handler) HandleHTTP(w http.ResponseWriter, r *http.Request) {
  // read request
  // decode name

  id, err := h.service.AddPerson(name)

  // write response
  // ... 
}

И в main.go вы все свяжете:

  1. Инициализировать соединение БД
  2. Инициализировать personRepo с этим соединением
  3. Инициализировать службу с репо
  4. Инициализировать транспорт с пакетом услуги

main

func main() {
  pool := makePool()
  conn := pool.GetConnection()

  // repo
  personRepo := sql.NewPersonRepo(conn)

  // service
  personService := service.New(personRepo)

  // handler
  personHandler := http.NewPersonHandler(personService)

  // Do the rest of the stuff, init the http engine/router by passing this handler.

}

Обратите внимание, что каждая структура пакета была инициализирована с interface, но вернула struct, а также интерфейсы были объявлены в пакете, который их использовал, а не в пакете, который их реализовал.

Это облегчает модульное тестирование этих пакетов.например, если вы хотите протестировать сервис, вам не нужно беспокоиться о запросе http, просто используйте некоторую «фиктивную» структуру, которая реализует интерфейс, от которого зависит сервис (PersonRepo), и вы готовы идти ..

Что ж, я надеюсь, что это немного вам помогло, поначалу это может показаться странным, но со временем вы увидите, как это выглядит как большой кусок кода, но это помогает, когда вам нужно добавить функциональностьили переключение драйвера БД и тому подобное .. Я рекомендую вам прочитать о дизайне, управляемом доменом в go, а также шестиугольную арку.

edit:

Кроме того, таким образом вы проходите соединение с сервисом, сервис не не импортирует и не использует глобальный пул БД.Честно говоря, я не знаю, почему это так часто встречается, я полагаю, что у него есть свои преимущества, и оно лучше для какого-то приложения, но в целом я думаю, что позволить вашей службе зависеть от какого-то интерфейса, даже не зная, что происходит, гораздолучшая практика.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...