Я сейчас вхожу в юнит-тесты на Go. Оказывается, мне нужно пересмотреть свой способ написания кода на go, поскольку (я чувствую и понимаю), мне нужно, чтобы все мои функции были частью данного интерфейса {} / структурированы, а не были объявлены в пакете.
Допустим, у меня есть 3 слоя для моего сервера микро-услуг gRPC
handlers
потребуется что-то вроде этого:
type IUsersServiceHandlers interface {
AddUserHandler(ctx context.Context, in *pb.AddUserRequest) (*pb.AddUserResponse, error)
GetUserByIDHandler(ctx context.Context, in *pb.GetUserByIDRequest) (*pb.GetUserByIDResponse, error)
GetUserByIDsHandler(ctx context.Context, in *pb.GetUserByIDsRequest) (*pb.GetUserByIDsResponse, error)
GetAllUsersHandler(ctx context.Context, in *pb.GetAllUsersRequest) (*pb.GetAllUsersResponse, error)
}
type UsersServiceHandlers struct{
DAL dal.IUsersServiceDAL
}
// AddUserHandler adds an user into the database.
func (h *UsersServiceHandlers) AddUserHandler(ctx context.Context, in *pb.AddUserRequest) (*pb.AddUserResponse, error) { /**/ }
// GetUserByIDHandler returns an user from the database.
func (h *UsersServiceHandlers) GetUserByIDHandler(ctx context.Context, in *pb.GetUserByIDRequest) (*pb.GetUserByIDResponse, error) { /**/ }
// GetUserByIDsHandler returns a slice of users from the database.
func (h *UsersServiceHandlers) GetUserByIDsHandler(ctx context.Context, in *pb.GetUserByIDsRequest) (*pb.GetUserByIDsResponse, error) { /**/ }
// GetAllUsersHandler returns all the users from the database.
func (h *UsersServiceHandlers) GetAllUsersHandler(ctx context.Context, in *pb.GetAllUsersRequest) (*pb.GetAllUsersResponse, error) { /**/ }
Если я хочу создать модульный тест, я просто высмеиваю dal.IUsersServiceDAL и напишу свой тест.
// UsersServiceDAL Mock
type UsersServiceDALMock struct {
mock.Mock
}
/* Mock implementation funcs ... */
func TestAddUserHandler(t *testing.T) {
usersServiceDALMock := new(UsersServiceDALMock)
var user = &models.User{
ID: mongodb.GenerateNewObjectID(),
UserName: "user",
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
usersServiceDALMock.On("InsertOne", user).Return(user)
// next we want to define the service we wish to test
usersServiceHandlers := UsersServiceHandlers{usersServiceDALMock}
// and call said method
a := assert.New(t)
response, err := usersServiceHandlers.AddUserHandler(ctx, &pb.AddUserRequest{
ID: user.ID.Hex(),
UserName: user.UserName,
})
a.Nil(err, "AddUserHandler should've worked.")
a.NotNil(response.User, "The response's User shouldn't be nil.")
usersServiceDALMock.AssertExpectations(t)
}
Однако,возникает проблема (которая имеет смысл для меня). Я даю пользователю только 2 данных, идентификатор и имя пользователя. Тем не менее, это поле содержит другие поля, которые обновляются при необходимости из следующего метода:
func (h *UsersServiceHandlers) initBaseUserForCreation(user *models.User) *models.User {
// Check if the ID is nil or not initialized. If so, we generate one.
if user.ID == nil || user.ID.IsZero() {
user.ID = mongodb.GenerateNewObjectID()
}
// We set the date to now if they aren't set.
if user.CreatedAt.IsZero() {
user.CreatedAt = time.Now().UTC()
}
if user.UpdatedAt.IsZero() {
user.UpdatedAt = time.Now().UTC()
}
/*
We don't set `IsArchived` since by default a bool is false. If it needs to be true, then the field might be already set or will be.
*/
return user
}
Меня беспокоит, что AddUserHandler
вызывает эту функцию по умолчанию, но эта функция находится в том же пакете (фактически записанавыше текущей реализации AddUserHandler). Я хотел бы иметь возможность mock
этой функции (которая находится в том же пакете), чтобы она возвращала все остальные поля, не инициализированные, чтобы мой тест мог пройти.
Реализация AddUserHandler
// AddUserHandler adds an user into the database.
func (h *UsersServiceHandlers) AddUserHandler(ctx context.Context, in *pb.AddUserRequest) (*pb.AddUserResponse, error) {
if in.ID == "" {
return nil, status.Error(codes.InvalidArgument, stacktracer.NewStackTraceAsString(pb.InvalidArgument.ToInt32(), "Invalid argument. ID cannot be null or empty", nil))
} else if in.UserName == "" {
return nil, status.Error(codes.InvalidArgument, stacktracer.NewStackTraceAsString(pb.InvalidArgument.ToInt32(), "Invalid argument. UserName cannot be null or empty.", nil))
}
user := h.initBaseUserForCreation(&models.User{
ID: mongodb.ConvertStringIdToObjectID(in.ID),
UserName: in.UserName,
})
insertedID, trace := h.DAL.InsertOne(user)
if trace != nil {
return nil, status.Error(codes.Internal, stacktracer.NewStackTraceAsString(pb.UserInsertionError.ToInt32(), "An error happened while inserting the user.", trace))
}
if *insertedID != *mongodb.ConvertStringIdToObjectID(in.ID) {
return nil, status.Error(codes.Aborted, stacktracer.NewStackTraceAsString(pb.InsertedDocumentIDMismatchRequirements.ToInt32(), "Inserted document ID mismatch base ID.", nil))
}
return &pb.AddUserResponse{User: mappers.FromUserModelToUserPb(user, nil)}, nil
}