Распространенный сценарий, иллюстрирующий полезность интерфейсов, - это тестирование кода, который для чего-то использует внешнюю клиентскую библиотеку; в данном случае для получения данных из воображаемого веб-API.
Представьте, что у нас есть пакет external
, который представляет собой библиотеку, написанную для взаимодействия с внешней веб-службой. Скорее всего, он будет экспортировать Client
объект с методами для взаимодействия с API. Представьте себе метод GetData
, который в этом случае может только возвращать данные, но может делать веб-запрос и теоретически возвращать ошибку, если это была реальная реализация.
Здесь у нас есть пакет foo
, который представляет собой код, который мы пишем, который хочет использовать Client
для получения данных из внешнего API, а затем что-то с ним делать. У нас есть два возможных случая ошибки в этой реализации: один, в котором GetData
возвращает ошибку, возможно, из-за сбоя веб-запроса, и другой, когда возвращенные данные не соответствуют нашим ожиданиям, и поэтому мы не можем их обработать. Оба пути приведут к тому, что наша функция Controller
вернет ошибку.
Теперь давайте посмотрим, как мы будем тестировать функцию Controller
. У нас может быть два основных теста: один для проверки успешности функции, а другой - для двух случаев отказа. Проблема в том, что мы не можем повлиять на поведение внешнего API и, следовательно, не можем заставить GetData
работать или терпеть неудачу.
Вышеупомянутый тест TestController_Success
пройдёт, но TestController_Failure
не пройдёт из-за нашей неспособности проверить случаи отказа. Это подтверждено и проиллюстрировано в отчете о покрытии.
Выявлены не только случаи сбоев, но и наши модульные тесты теперь недетерминированы и зависят от внешнего API, который может потерпеть неудачу или добиться успеха по своему желанию. Нам нужен способ заглушить поведение GetData
в нашем коде, чтобы мы могли управлять его выводом во время наших модульных тестов, и именно интерфейсы становятся очень полезными.
Использование интерфейсов для включения заглушки
Если мы можем определить интерфейс, которому удовлетворяет объект Client
внешней библиотеки, то вместо этого мы можем использовать этот интерфейс в нашем коде. Это позволит нам использовать фальшивый клиентский объект во время тестирования и настоящий - во время нормальной работы.
На других языках это потребовало бы от нас изменения кода внешней библиотеки, чтобы явно указать, что Client
реализовал наш новый интерфейс (о нет!), Но поскольку интерфейсы Go удовлетворяются неявно, компилятор уже это знает! Ура!
Итак, здесь мы определяем IExternalClient
интерфейс (пуристы, пожалуйста, не распинайте мое имя), который определяет методы, которые мы используем в нашем foo Controller
, и модифицируем функцию контроллера, чтобы она принимала тип интерфейса IExternalClient
в качестве параметра. Остальная часть функции контроллера затем работает так же, как и раньше, вызывая GetData
метод интерфейса, а не конкретную внешнюю Client
реализацию.
Теперь давайте посмотрим, насколько проще протестировать наш Controller
метод. Мы можем реализовать интерфейс IExternalClient
, просто реализовав метод GetData
на нашем MockClient
объекте, но в нашей реализации MockClient
он возвращает то, что мы хотим вернуть. Затем мы загружаем наши реализации от IExternalClient
до Controller
в наши тесты. Мы можем использовать MockClient
, чтобы возвращать разные значения для результата GetData
и нашего FailingClient
, чтобы GetData
возвращал ошибку.
Теперь мы можем легко обрабатывать все ветви нашей Controller
функции, что подтверждается обновленным отчетом о покрытии.
Как видите, написание и использование интерфейсов может повысить гибкость и тестируемость вашего кода, а также может быть простым решением для устранения внешних зависимостей.
Стандартные библиотеки Go широко используют интерфейсы, и вы можете найти отличные примеры в таких пакетах, как io и net / http.
Удачного кодирования!