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

Удачного кодирования!