Hexagonal Architecture in Go
References:
- laying the foundation for scalability( Hexagonal Architecture In GoLang)
- The Practical Hexagonal Architecture for Golang
- YouTube Video
- GitHub: redhaanggara21/redha-rgb-golang-test
- GitHub: redhaanggara21/go-hexagonal
Структура проекта
В качестве проекта будет использовать пример из Hexagonal Architecture. Разработает user service (сервис пользователя), который предоставляет функцию регистрации для создания новых аккаунтов.
Структура проекта также построена на основе компонентов Hexagonal Architecture. Архитектура подчеркивает четкое разделение ответственности и изолирует основную логику приложения от внешних зависимостей. Вот как организована структура проекта для нашей архитектуры:
├── Dockerfile
├── cmd
│ └── runner.go
├── conf
│ └── app.yaml
├── internal
│ ├── controller
│ │ └── controller.go
│ ├── core
│ │ ├── common
│ │ │ ├── router
│ │ │ │ └── router.go
│ │ │ └── utils
│ │ │ └── logger.go
│ │ ├── dto
│ │ │ └── user.go
│ │ ├── entity
│ │ ├── model
│ │ │ ├── request
│ │ │ │ └── request.go
│ │ │ └── response
│ │ │ └── response.go
│ │ ├── port
│ │ │ ├── repository
│ │ │ │ ├── db.go
│ │ │ │ └── user.go
│ │ │ └── service
│ │ │ └── user.go
│ │ ├── server
│ │ │ └── http_server.go
│ │ └── service
│ │ └── user.go
│ └── infra
│ ├── config
│ │ └── config.go
│ └── repository
│ ├── db.go
│ └── user.go
├── schema
│ └── schema.sql
└── script
└── run.sh
Рассмотрим роли директорий.
/cmd
Этот каталог содержит точку входа вашего приложения, в данном случае файл runner.go
.
Здесь вы обычно определяете основную функцию и настраиваете свое приложение.
/internal
Этот каталог является фундаментальной частью Hexagonal Architecture, поскольку он содержит внутренний код приложения, разделенный на различные пакеты.
/internal/controller
Этот пакет содержит файлы, отвечающие за обработку HTTP-запросов и вызов соответствующей логики ядра.
Например, файл controller.go
может определять функции для обработки различных HTTP-endpoints.
Это primary adapter / driver adapter (первичный адаптер / ведущий адаптер).
/internal/core
Этот пакет содержит основную логику приложения и разделен на подпакеты:
common
: Содержит общие утилиты, используемые в application (приложении), такие, какrouter.go
(для маршрутизации HTTP) иlogger.go
(для ведения журнала).dto
: Этот пакет определяет data transfer objects (DTO, объекты передачи данных), используемые для передачи данных между различными слоями.entity
: Содержит domain entities (доменные сущности), представляющие основные структуры данных, используемые в application.model
: Этот пакет содержит структуры model (моделей), представляющие конкретные тела HTTP-запросов и ответов.port
: Здесь вы определяете интерфейсы (ports), которые представляют необходимые функции application. Например, интерфейсыrepository
определяют методы для доступа к данным, а интерфейсыservice
определяют методы для бизнес-логики.server
: Этот пакет содержит настройку HTTP-сервера.service
: Этот пакет содержит основные сервисы application, которые обрабатывают бизнес-логику.
/internal/infra
Этот пакет содержит код, связанный с инфраструктурой, они являются secondary adapters / driven adapters (вторичными адаптерами / ведомыми адаптерами):
config
: Этот файл обрабатывает настройку и разбор конфигурации.repository
: Этот пакет содержит реализации интерфейсов репозиториев, таких какdb.go
иuser.go
, взаимодействующие с базой данных.
Кроме того, у нас есть несколько других папок:
conf
: Этот каталог содержит файлы конфигурации вашего приложения, такие как файлapp.yaml
. Здесь вы храните настройки, связанные с поведением вашего приложения.schema
: Обычно содержит файлы схем базы данных, такие какschema.sql
, определяющие структуру таблиц базы данных.script
: Этот каталог содержит сценарии, такие какrun.sh
, которые могут использоваться для автоматизации общих задач или выполнения приложения.Dockerfile
: Файл, используемый для определения образа Docker для вашего приложения, позволяющий контейнеризировать его.LICENSE
: Файл содержит информацию о лицензииREADME.md
: Файл содержит документацию проекта
Организуя код таким образом, Hexagonal Architecture способствует четкому разделению основной логики приложения и внешних зависимостей, что приводит к повышению maintainability (поддерживаемости), testability (тестируемости) и flexibility (гибкости). Это позволяет легко заменять или изменять внешние компоненты без влияния на основную логику приложения.
Благодаря введению в функциональность директорий, мы можем легко визуализировать и понять структуру кода проектов.
Организация кода
Прежде чем начать, важно понять требования проекта, особенно если это функция регистрации. Функция регистрации должна позволять пользователям создавать учетную запись, предоставляя уникальное имя пользователя и надежный пароль. После успешной регистрации система должна вернуть ответ, указывающий на успех, а в случае любых ошибок должны быть возвращены соответствующие сообщения об ошибках.
Модели запросов и ответов
Начнем с создания необходимых request and response models (моделей запросов и ответов) для обработки запросов регистрации.
// ./internal/core/model/request/request.go
package request
type SignUpRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// ./internal/core/model/response/response.go
package response
import "user-service/internal/core/entity/error_code"
type Response struct {
Data interface{} `json:"data"`
Status bool `json:"status"`
ErrorCode error_code.ErrorCode `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
}
type SignUpDataResponse struct {
DisplayName string `json:"displayName"`
}
В файле internal/core/model/request/request.go
мы определяем структуру SignUpRequest
,
которая содержит информацию об имени пользователя и пароле.
Определение интерфейса UserService (Primary Port Component)
Мы определяем первичный порт, которым является интерфейс UserService
в файле internal/core/port/service/user.go
.
Этот интерфейс описывает функциональность, необходимую для обработки запросов регистрации.
// ./internal/core/port/service/user.go
package service
import (
"user-service/internal/core/model/request"
"user-service/internal/core/model/response"
)
type UserService interface {
SignUp(request *request.SignUpRequest) *response.Response
}
Реализация UserService (Application/Business Component)
В файле internal/core/service/user.go
мы реализуем интерфейс UserService
с конкретным сервисом userService
.
Этот сервис обрабатывает основную бизнес-логику проекта.
Функция SignUp
в этом сервисе валидирует запрос,
генерирует случайное отображаемое имя и сохраняет нового пользователя в базу данных с использованием UserRepository
.
// ./internal/core/service/user.go
package service
import (
"user-service/internal/core/common/utils"
"user-service/internal/core/dto"
"user-service/internal/core/entity/error_code"
"user-service/internal/core/model/request"
"user-service/internal/core/model/response"
"user-service/internal/core/port/repository"
"user-service/internal/core/port/service"
)
const (
invalidUserNameErrMsg = "invalid username"
invalidPasswordErrMsg = "invalid password"
)
type userService struct {
userRepo repository.UserRepository
}
func NewUserService(userRepo repository.UserRepository) service.UserService {
return &userService{
userRepo: userRepo,
}
}
func (u userService) SignUp(request *request.SignUpRequest) *response.Response {
// validate request
if len(request.Username) == 0 {
return u.createFailedResponse(error_code.InvalidRequest, invalidUserNameErrMsg)
}
if len(request.Password) == 0 {
return u.createFailedResponse(error_code.InvalidRequest, invalidPasswordErrMsg)
}
currentTime := utils.GetUTCCurrentMillis()
userDTO := dto.UserDTO{
UserName: request.Username,
Password: request.Password,
DisplayName: u.getRandomDisplayName(request.Username),
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
// save a new user
err := u.userRepo.Insert(userDTO)
if err != nil {
if err == repository.DuplicateUser {
return u.createFailedResponse(error_code.DuplicateUser, err.Error())
}
return u.createFailedResponse(error_code.InternalError, error_code.InternalErrMsg)
}
// create data response
signUpData := response.SignUpDataResponse{
DisplayName: userDTO.DisplayName,
}
return u.createSuccessResponse(signUpData)
}
func (u userService) getRandomDisplayName(username string) string {
return username + utils.GetUUID()
}
func (u userService) createFailedResponse(
code error_code.ErrorCode, message string,
) *response.Response {
return &response.Response{
Status: false,
ErrorCode: code,
ErrorMessage: message,
}
}
func (u userService) createSuccessResponse(data response.SignUpDataResponse) *response.Response {
return &response.Response{
Data: data,
Status: true,
ErrorCode: error_code.Success,
ErrorMessage: error_code.SuccessErrMsg,
}
}
В приведенном выше коде объект UserDTO
для одного сервиса error_code
.
Структуры определены следующим образом:
// ./internal/core/dto/user.go
package dto
type UserDTO struct {
UserName string
Password string
CreatedAt uint64
UpdatedAt uint64
}
// ./internal/core/entity/error_code/error_code.go
package error_code
type ErrorCode string
// error code
const (
Success ErrorCode = "SUCCESS"
InvalidRequest ErrorCode = "INVALID_REQUEST"
DuplicateUser ErrorCode = "DUPLICATE_USER"
InternalError ErrorCode = "INTERNAL_ERROR"
)
// error message
const (
SuccessErrMsg = "success"
InternalErrMsg = "internal error"
InvalidRequestErrMsg = "invalid request"
)
Создание интерфейса UserRepository (Secondary Port Component)
Для взаимодействия с базой данных мы создаем вторичный порт, интерфейс UserRepository
,
в файлеinternal/core/port/repository/user.go
.
Этот интерфейс определяет контракт для вставки нового объекта UserDTO
.
// ./internal/core/port/repository/user.go
package repository
import (
"errors"
"user-service/internal/core/dto"
)
var (
DuplicateUser = errors.New("duplicate user")
)
type UserRepository interface {
Insert(user dto.UserDTO) error
}
Реализация UserRepository (Secondary Adapter)
Файл internal/infra/repository/user.go
содержит реализацию интерфейса UserRepository
.
Этот репозиторий взаимодействует с базой данных для вставки нового пользователя.
Он также обрабатывает любые ошибки, которые могут возникнуть в процессе вставки,
такие как дублирование записей пользователя.
Это вторичный адаптер.
// ./internal/infra/repository/user.go
package repository
import (
"errors"
"strings"
"user-service/internal/core/dto"
"user-service/internal/core/port/repository"
)
const (
duplicateEntryMsg = "Duplicate entry"
numberRowInserted = 1
)
var (
insertUserErr = errors.New("failed to insert user")
)
const (
insertUserStatement = "INSERT INTO User ( " +
"`username`, " +
"`password`, " +
"`display_name`, " +
"`created_at`," +
"`updated_at`) " +
"VALUES (?, ?, ?, ?, ?)"
)
type userRepository struct {
db repository.Database
}
func NewUserRepository(db repository.Database) repository.UserRepository {
return &userRepository{
db: db,
}
}
func (u userRepository) Insert(user dto.UserDTO) error {
result, err := u.db.GetDB().Exec(
insertUserStatement,
user.UserName,
user.Password,
user.DisplayName,
user.CreatedAt,
user.UpdatedAt,
)
if err != nil {
if strings.Contains(err.Error(), duplicateEntryMsg) {
return repository.DuplicateUser
}
return err
}
numRow, err := result.RowsAffected()
if err != nil {
return err
}
if numRow != numberRowInserted {
return insertUserErr
}
return nil
}
Для подключения к базе данных сначала определим интерфейс Database
(в файле internal/core/port/repository/db.go
),
чтобы предоставить подключения к базе данных для сервиса.
Кроме того, мы реализуем адаптер базы данных в internal/infra/repository/db.go
,
который настраивает подключение и инициализирует драйвер базы данных.
// ./internal/core/port/repository/db.go
package repository
import (
"database/sql"
"io"
)
type Database interface {
io.Closer
GetDB() *sql.DB
}
А вот и адаптер базы данных:
// ./internal/infra/repository/db.go
package repository
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
"user-service/internal/core/port/repository"
"user-service/internal/infra/config"
)
type database struct {
*sql.DB
}
func NewDB(conf config.DatabaseConfig) (*repository.Database, error) {
db, err := newDatabase(conf)
if err != nil {
return nil, err
}
return &database{
db,
}, nil
}
func newDatabase(conf config.DatabaseConfig) (*sql.DB, error) {
db, err := sql.Open(conf.Driver, conf.Url)
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(time.Minute * time.Duration(conf.ConnMaxLifetimeInMinute))
db.SetMaxOpenConns(conf.MaxOpenConns)
db.SetMaxIdleConns(conf.MaxIdleConns)
if err := db.Ping(); err != nil {
return nil, err
}
return db, err
}
func (da database) Close() error {
return da.DB.Close()
}
func (da database) GetDB() *sql.DB {
return da.DB
}
На этом этапе мы почти закончили проект.
У нас уже есть компонент, который обрабатывает бизнес-логику (UserService
) и
компонент, который подключается к базе данных (UserRepository
и Database
).
Давайте посмотрим на архитектуру, чтобы найти, какие компоненты нам не хватает.
Создание UserController (Primary Adapter)
Нам не хватает важного компонента — контроллера (Primary Adapter) сервиса.
Теперь нам нужен первичный адаптер, UserController
, для обработки входящих HTTP-запросов
и вызова основной логики приложения.
Файл internal/controller/controller.go
содержит реализацию этого контроллера.
Он получает HTTP-запросы для функции регистрации и передает обработку запросов в UserService
.
// ./internal/controller/controller.go
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"user-service/internal/core/common/router"
"user-service/internal/core/entity/error_code"
"user-service/internal/core/model/request"
"user-service/internal/core/model/response"
"user-service/internal/core/port/service"
)
var (
invalidRequestResponse = &response.Response{
ErrorCode: error_code.InvalidRequest,
ErrorMessage: error_code.InvalidRequestErrMsg,
Status: false,
}
)
type UserController struct {
gin *gin.Engine
userService service.UserService
}
func NewUserController(
gin *gin.Engine,
userService service.UserService,
) UserController {
return UserController{
gin: gin,
userService: userService,
}
}
func (u UserController) InitRouter() {
api := u.gin.Group("/api/v1")
router.Post(api, "/signup", u.signUp)
}
func (u UserController) signUp(c *gin.Context) {
req, err := u.parseRequest(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusOK, &invalidRequestResponse)
return
}
resp := u.userService.SignUp(req)
c.JSON(http.StatusOK, resp)
}
func (u UserController) parseRequest(ctx *gin.Context) (*request.SignUpRequest, error) {
var req request.SignUpRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
return nil, err
}
return &req, nil
}
В конструкторе UserController
мы регистрируем соответствующие маршруты для функции регистрации,
используя пакет github.com/gin-gonic/gin
.
Например, мы настраиваем конечную точку /api/v1/signup
для обработки запросов на регистрацию.
Функция signUp
в UserController
анализирует входящие запросы и отправляет ответ клиенту.
Если запрос недействителен, он возвращает соответствующий ответ об ошибке.
Создание HTTP-сервера
Следующий шаг — создание HTTP-сервера для обработки входящих запросов. HTTP-сервер будет служить точкой входа для нашего приложения, направляя запросы к соответствующим контроллерам для обработки.
// ./internal/core/server/http_server.go
package server
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"user-service/internal/infra/config"
)
const defaultHost = "0.0.0.0"
type HttpServer interface {
Start()
Stop()
}
type httpServer struct {
Port uint
server *http.Server
}
func NewHttpServer(router *gin.Engine, config config.HttpServerConfig) HttpServer {
return &httpServer{
Port: config.Port,
server: &http.Server{
Addr: fmt.Sprintf("%s:%d", defaultHost, config.Port),
Handler: router,
},
}
}
func (httpServer httpServer) Start() {
go func() {
if err := httpServer.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf(
"failed to start HttpServer on port %d, err=%s",
httpServer.Port, err.Error(),
)
}
}()
log.Printf("Service started on port %d", httpServer.Port)
}
func (httpServer httpServer) Stop() {
ctx, cancel := context.WithTimeout(
context.Background(), time.Duration(3)*time.Second,
)
defer cancel()
if err := httpServer.server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown, err=%s", err.Error())
}
}
В файле internal/core/server/http_server.go
мы определяем интерфейс HttpServer
с двумя методами:
Start()
- запускает сервер для прослушивания входящих запросовStop()
- gracefully shuts down сервер при необходимости.
Объединение всего вместе — функция main()
Собрав все части нашей функции регистрации, мы готовы подготовить главную функцию нашей программы.
main()
объединит все компоненты и запустит HTTP-сервер,
что позволит сделать функцию регистрации доступной для пользователей.
// ./cmd/runner.go
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/gin-gonic/gin"
"user-service/internal/controller"
"user-service/internal/core/server"
"user-service/internal/core/service"
"user-service/internal/infra/config"
"user-service/internal/infra/repository"
)
func main() {
// Создаем новый экземпляр маршрутизатора Gin
instance := gin.New()
instance.Use(gin.Recovery())
// Инициализируем подключение к базе данных
db, err := repository.NewDB(
config.DatabaseConfig{
Driver: "mysql",
Url: "user:password@tcp(127.0.0.1:3306)/your_database_name?charset=utf8mb4&parseTime=true&loc=UTC&tls=false&readTimeout=3s&writeTimeout=3s&timeout=3s&clientFoundRows=true",
ConnMaxLifetimeInMinute: 3,
MaxOpenConns: 10,
MaxIdleConns: 1,
},
)
if err != nil {
log.Fatalf("failed to connect to database, err=%s\n", err.Error())
}
// Создаем UserRepository
userRepo := repository.NewUserRepository(db)
// Создаем UserService
userService := service.NewUserService(userRepo)
// Создаем UserController
userController := controller.NewUserController(instance, userService)
// Инициализируем маршруты для UserController
userController.InitRouter()
// Создаем HTTP-сервер
httpServer := server.NewHttpServer(
instance,
config.HttpServerConfig{
Port: 8000,
},
)
// Запускаем HTTP-сервер
httpServer.Start()
defer httpServer.Stop()
// Ожидание сигналов ОС для выполнения плавного завершения работы
log.Println("Listening for signals...")
c := make(chan os.Signal, 1)
signal.Notify(
c,
os.Interrupt,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGQUIT,
syscall.SIGTERM,
)
<-c
log.Println("Graceful shutdown...")
}
В main()
мы создаем новый экземпляр маршрутизатора Gin с использованием gin.New()
.
Мы также добавляем middleware для обработки восстановления после паники с помощью gin.Recovery()
.
Затем мы инициализируем подключение к базе данных, используя драйвер mysql
и соответствующие данные конфигурации.
Функция repository.NewDB()
создает новый экземпляр подключения к базе данных,
который будет использоваться для взаимодействия с базой данных.
Используя подключение к базе данных,
мы создаем новый экземпляр UserRepository
с помощью repository.NewUserRepository(db)
.
Этот репозиторий будет отвечать за обработку взаимодействий с базой данных, связанных с пользователями.
Затем мы создаем UserService
, передавая UserRepository
в service.NewUserService(userRepo)
.
Этот сервис будет обрабатывать основную бизнес-логику функции регистрации.
Мы настраиваем HTTP-сервер с использованием server.NewHttpServer(instance, config.HttpServerConfig{Port: 8000})
.
Сервер будет слушать на порту 8000.
Функция httpServer.Start()
запускает сервер для начала обработки входящих запросов.
Наконец, мы используем канал для ожидания сигналов ОС (например, SIGINT, SIGTERM)
для gracefully shutdown сервера при необходимости.
Когда сигнал получен, мы вызываем httpServer.Stop()
для остановки сервера и выполнения чистого завершения работы.
Диаграмма кода
Эта диаграмма иллюстрирует поток данных и управление между различными компонентами User Service.
UserController
получает HTTP-запросы и передает их в UserService
,
который обрабатывает бизнес-логику и взаимодействует с UserRepository
для доступа к базе данных.
+------------------------+
| UserController |
+------------------------+
| - UserService |
+------------------------+
| + SignUp(request) |
+------------------------+
|
| HTTP Requests/Responses
|
V
+------------------------+
| UserService |
+------------------------+
| - UserRepository |
+------------------------+
| + SignUp(request) |
+------------------------+
|
| Business Logic
|
V
+------------------------+
| UserRepository |
+------------------------+
| - Database |
+------------------------+
| + Insert(user) |
+------------------------+
Запуск
Мы можем написать script для запуска User Service следующим образом:
Перед запуском сервиса нам нужно подготовить схему базы данных.
Создайте файл с именем schema.sql
в каталоге schema/
со следующим содержимым:
-- ./schema/schema.sql
create table User
(
username varchar(20) not null primary key,
password varchar(64) not null,
display_name varchar(20) not null,
created_at bigint not null,
updated_at bigint not null
)
Чтобы запустить User Service, выполните следующую команду ./run.sh
в корневом каталоге проекта.
Сервис начнет работу на порту 8000
.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /api/v1/signup --> user-service/internal/controller.UserController.signUp-fm (2 handlers)
2023/07/31 20:23:41 Start Service with port 8000
2023/07/31 20:23:41 listening signals...
Тестирование API регистрации
Теперь давайте протестируем API /api/v1/signup
с использованием curl
или любого другого HTTP-клиента.
Например, будем использовать команду curl
.
# Выполнение команды curl
curl --location 'http://localhost:8000/api/v1/signup' \
--header 'Content-Type: application/json' \
--data '{
"username": "test_abc",
"password": "12345"
}'
Результат:
{
"data": {
"displayName": "test_abc1690810036744"
},
"status": true,
"errorCode": "SUCCESS",
"errorMessage": "success"
}
Если выполнить команду curl
снова, вы получите следующий результат с errorCode DUPLICATE_USER
:
# Выполнение команды curl
curl --location 'http://localhost:8000/api/v1/signup' \
--header 'Content-Type: application/json' \
--data '{
"username": "test_abc",
"password": "12345"
}'
Получите следующий результат с errorCode
DUPLICATE_USER
:
Написание unit test
Давайте добавим файл модульного теста для компонента UserService
.
Мы создадим файл с именем user_service_test.go
в каталоге internal/core/service
.
// ./internal/core/service/user_service
package service
import (
"testing"
"user-service/internal/core/dto"
"user-service/internal/core/entity/error_code"
"user-service/internal/core/model/request"
"user-service/internal/core/model/response"
"user-service/internal/core/port/repository"
)
// Определяем мок-репозиторий для тестирования
type mockUserRepository struct{}
func (m *mockUserRepository) Insert(user dto.UserDTO) error {
// Имитируем случай дублирования пользователя
if user.UserName == "test_user" {
return repository.DuplicateUser
}
// Имитируем успешную вставку
return nil
}
func TestUserService_SignUp_Success(t *testing.T) {
// Создаем мок-репозиторий для тестирования
userRepo := &mockUserRepository{}
// Создаем UserService, используя мок-репозиторий
userService := NewUserService(userRepo)
// Тестовый случай: успешная регистрация
req := &request.SignUpRequest{
Username: "test_abc",
Password: "12345",
}
res := userService.SignUp(req)
if !res.Status {
t.Errorf("expected status to be true, got false")
}
data := res.Data.(response.SignUpDataResponse)
if data.DisplayName == "" {
t.Errorf("expected non-empty display name, got empty")
}
}
func TestUserService_SignUp_InvalidUsername(t *testing.T) {
// Создаем мок-репозиторий для тестирования
userRepo := &mockUserRepository{}
// Создаем UserService, используя мок-репозиторий
userService := NewUserService(userRepo)
// Тестовый случай: недействительный запрос с пустым именем пользователя
req := &request.SignUpRequest{
Username: "",
Password: "12345",
}
res := userService.SignUp(req)
if res.Status {
t.Errorf("expected status to be false, got true")
}
if res.ErrorCode != error_code.InvalidRequest {
t.Errorf("expected error code to be InvalidRequest, got %s", res.ErrorCode)
}
}
func TestUserService_SignUp_InvalidPassword(t *testing.T) {
// Создаем мок-репозиторий для тестирования
userRepo := &mockUserRepository{}
// Создаем UserService, используя мок-репозиторий
userService := NewUserService(userRepo)
// Тестовый случай: недействительный запрос с пустым паролем
req := &request.SignUpRequest{
Username: "test_user",
Password: "",
}
res := userService.SignUp(req)
if res.Status {
t.Errorf("expected status to be false, got true")
}
if res.ErrorCode != error_code.InvalidRequest {
t.Errorf("expected error code to be InvalidRequest, got %s", res.ErrorCode)
}
}
func TestUserService_SignUp_DuplicateUser(t *testing.T) {
// Создаем мок-репозиторий для тестирования
userRepo := &mockUserRepository{}
// Создаем UserService, используя мок-репозиторий
userService := NewUserService(userRepo)
// Тестовый случай: дублирование пользователя
req := &request.SignUpRequest{
Username: "test_user",
Password: "12345",
}
res := userService.SignUp(req)
if res.Status {
t.Errorf("expected status to be false, got true")
}
if res.ErrorCode != error_code.DuplicateUser {
t.Errorf("expected error code to be DuplicateUser, got %s", res.ErrorCode)
}
}
Каждая тестовая функция создает новый экземпляр UserService
с мок-репозиторием
и затем вызывает метод SignUp
с соответствующим запросом.
Затем она проверяет ответ, чтобы убедиться, что сервис ведет себя так, как ожидается.
Чтобы запустить модульные тесты, выполните следующую команду в корневом каталоге проекта:
Результат:
=== RUN TestUserService_SignUp_Success
--- PASS: TestUserService_SignUp_Success (0.00s)
=== RUN TestUserService_SignUp_InvalidUsername
--- PASS: TestUserService_SignUp_InvalidUsername (0.00s)
=== RUN TestUserService_SignUp_InvalidPassword
--- PASS: TestUserService_SignUp_InvalidPassword (0.00s)
=== RUN TestUserService_SignUp_DuplicateUser
--- PASS: TestUserService_SignUp_DuplicateUser (0.00s)
PASS
ok user-service/internal/core/service 0.203s
Заключение
На этой странице успешно разработан User Service с использованием Golang и соблюдены принципы Hexagonal Architecture, что достаточно для демонстрации того, как каждая часть модуля связана друг с другом. Hexagonal Architecture позволила нам создать гибкую и поддерживаемую структуру приложения, заложив основу для масштабируемости.