Как протестировать сопоставленные объекты в golang

Я пытаюсь реализовать некоторые модульные тесты в своем коде go и считаю тему насмешливого метода довольно сложной. У меня есть следующий пример, где я надеюсь, что вы можете мне помочь :)

На первом слое у меня есть следующий код:

package api

import (
    "fmt"
    "core"
)

type createUserDTO struct {
    Id string
}

func ApiMethod() {
    fmt.Println("some incoming api call wit user")
    incomingUserData := &createUserDTO{Id: "testId"}
    mapedUser := incomingUserData.mapUser()
    mapedUser.Create()
}

func (createUserDTO *createUserDTO) mapUser() core.User {
    return &core.UserCore{Id: createUserDTO.Id}
}

Второй слой имеет следующий код:

package core

import (
    "fmt"
)

type CoreUser struct{ Id string }

type User interface {
    Create()
}

func (user CoreUser) Create() {
    fmt.Println("Do Stuff")
}

Теперь мой вопрос: как мне протестировать каждый общедоступный метод в пакете API без тестирования основного пакета. Особенно метод Create().

На это очень трудно ответить; как написано, ваш код ничего не делает, кроме побочных эффектов, которые очень сложно проверить (и, возможно, вам даже не следует пытаться проверять). В этом случае еще сложнее, потому что побочные эффекты являются полностью внутренними для функций, не предоставляя внешней поверхности, которую можно было бы протестировать. Я подозреваю, что вы попытались упростить свой вариант использования, чтобы задать вопрос, но, возможно, вы зашли слишком далеко?

Deltics 14.11.2022 00:20

да, это очень упрощенное представление. Цель состояла в том, чтобы максимально разделить слои архитектуры. Поэтому каждый уровень имеет свой собственный пользовательский объект. Например, UserDTO, UserCore, UserDB. Возможно, мне придется по-другому думать об архитектуре go. Если это так, не могли бы вы привести пример того, как написать этот вариант использования для создания пользователя.

TheBohne 14.11.2022 09:14
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
1
2
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

На основе комментариев я собрал тривиальный репозиторий GitHub, чтобы показать, как я обычно работаю со структурированием проектов в Go. Репозиторий пока не учитывает тестовую часть, но вставить их в эту структуру проекта должно быть довольно легко.
Начнем с общего расположения папок:

  • controllers
  • services
  • db
  • dto
  • models

Теперь давайте посмотрим на соответствующие файлы.

models/user.go
package models

import "gorm.io/gorm"

type User struct {
    *gorm.Model
    Id string `gorm:"primaryKey"`
}

func NewUser(id string) *User {
    return &User{Id: id}
}

Простое определение структуры здесь.

dto/user.go
package dto

import "time"

type UserDTO struct {
    Id      string    `json:"id"`
    AddedOn time.Time `json:"added_on"`
}

func NewUserDTO(id string) *UserDTO {
    return &UserDTO{Id: id}
}

Мы обогащаем структуру модели фиктивным полем AddedOn, которое нужно только для демонстрации.

db/user.go
package db

import (
    "gorm.io/gorm"

    "userapp/models"
)

type UserDb struct {
    Conn *gorm.DB
}

type UserDbInterface interface {
    SaveUser(user *models.User) error
}

func (u *UserDb) SaveUser(user *models.User) error {
    if dbTrn := u.Conn.Create(user); dbTrn.Error != nil {
        return dbTrn.Error
    }
    return nil
}

Здесь мы определяем интерфейс для использования репозитория User. В наших тестах мы можем предоставить макет вместо экземпляра структуры UserDb.

services/user.go
package services

import (
    "time"

    "userapp/db"
    "userapp/dto"
    "userapp/models"
)

type UserService struct {
    DB db.UserDbInterface
}

type UserServiceInterface interface {
    AddUser(inputReq *dto.UserDTO) (*dto.UserDTO, error)
}

func NewUserService(db db.UserDbInterface) *UserService {
    return &UserService{
        DB: db,
    }
}

func (u *UserService) AddUser(inputReq *dto.UserDTO) (*dto.UserDTO, error) {
    // here you can write complex logic
    user := models.NewUser(inputReq.Id)

    // invoke db repo
    if err := u.DB.SaveUser(user); err != nil {
        return nil, err
    }

    inputReq.AddedOn = time.Now()

    return inputReq, nil
}

Это уровень, который связывает соединения между уровнем представления и нижележащими репозиториями.

controllers/user.go
package controllers

import (
    "encoding/json"
    "io"
    "net/http"

    "userapp/dto"
    "userapp/services"
)

type UserController struct {
    US services.UserServiceInterface
}

func NewUserController(userService services.UserServiceInterface) *UserController {
    return &UserController{
        US: userService,
    }
}

func (u *UserController) Save(w http.ResponseWriter, r *http.Request) {
    reqBody, err := io.ReadAll(r.Body)
    if err != nil {
        panic(err)
    }
    r.Body.Close()

    var userReq dto.UserDTO

    json.Unmarshal(reqBody, &userReq)

    userRes, err := u.US.AddUser(&userReq)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode(err)
        return
    }

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(userRes)
}

Здесь мы определили контроллер, который (через внедрение зависимостей) использует структуру UserService.

Вы можете найти все в моем репозитории на GitHub Дайте мне знать, если это прояснит немного.

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

TheBohne 14.11.2022 14:13

пожалуйста! На сервисном уровне я обычно выполняю логику, например связываюсь со сторонними системами, сопоставляю вещи и так далее. Каждый определенный здесь метод будет действовать в своем собственном DTO, который может сильно отличаться, даже если они «основаны» на одной и той же модели. GetUserReq может иметь поля, отличные от PostUserReq, и так далее. Здесь вы можете добиться разделения, которое вас беспокоит. Более того, вы можете ограничить доступ к некоторым данным конечному пользователю.

Ivan Pesenti 14.11.2022 14:43

@TheBohne, если это решит вашу проблему, можете ли вы пометить это как принятый ответ? В противном случае, если вам нужно уточнить что-то еще, дайте мне знать! спасибо

Ivan Pesenti 15.11.2022 10:54

В настоящее время лучший ответ на вопрос. Я еще не уверен на 100% из-за не совсем разделенных слоев. Но подходит :) спасибо.

TheBohne 15.11.2022 18:31

Я думаю, что в определенный момент один из ваших слоев должен знать о DTO. Этот уровень обычно является сервисным уровнем. Довольно часто используется слой сопоставления, который сопоставляет экземпляры «DTO» с экземплярами «модели». Этот уровень внедряется в сервисный уровень. Таким образом, вы можете отделить свои объекты от DTO, которым нужно только переносить данные с одного уровня на другой. В качестве примера вы можете увидеть эту статью, в которой объясняется роль DTO в решении с чистой архитектурой: nurcahyaari.medium.com/…

Ivan Pesenti 16.11.2022 09:04

Другие вопросы по теме