after @zahra-keshtkar gave the solution i come up the solution with abstract the usersService's depedency
so i did this :
this is the usersService's sruct :
type UsersServices struct {
Repository repository.Repository
Token utils.Token
Transaction db.Transactioner
}
i created transactioner interfaces like this :
type Transactioner interface {
Begin() (*sql.Tx, error)
Rollback() error
Commit() error
WithTx(ctx context.Context, fn func(tx *sql.Tx) error) error
}
then in testing i did like this :
type tokenInvitation struct {
token string
}
func (t tokenInvitation) Generate() string {
return t.token
}
type statusTransaction int
func (s *statusTransaction) string(stat statusTransaction) string {
return []string{"init", "begin", "open", "failed"}[stat]
}
const (
initial statusTransaction = iota
begin
open
failed
)
type transactionMock struct {
status statusTransaction
}
func (t *transactionMock) Begin() (*sql.Tx, error) {
t.status = begin
if t.status.string(begin) != "begin" {
return nil, errors.New("transaction not begin")
}
return nil, nil
}
func (t *transactionMock) Rollback() error {
t.status = failed
if t.status.string(failed) != "failed" {
return errors.New("transaction rollback errors")
}
return nil
}
func (t *transactionMock) Commit() error {
t.status = open
if t.status.string(open) != "open" {
return errors.New("transaction errors")
}
return nil
}
func (t *transactionMock) WithTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
_, err := t.Begin()
if err != nil {
return err
}
err = fn(nil)
if err != nil {
if err := t.Rollback(); err != nil {
return err
}
return err
}
return t.Commit()
}
func TestRegisterAccount(t *testing.T) {
tkn := tokenInvitation{token: "this is test token"}
transMock := transactionMock{status: initial}
usersSrv := setupUsersServiceTest(tkn, &transMock)
request := RegisterRequest{
Username: "username",
Email: "[email protected]",
Password: "HelloWorld$123",
}
want := &RegisterResponse{Token: tkn.token}
got, err := usersSrv.RegisterAccount(context.Background(), request)
if err != nil {
t.Errorf("got error %q but want none", err)
}
if !reflect.DeepEqual(want, got) {
t.Errorf("want to equal %v, but got: %v", want, got)
}
}
func setupUsersServiceTest(token tokenInvitation, transx db.Transactioner) UsersServices {
return UsersServices{
Repository: repository.Repository{
Users: &mock.UsersRepositoryMock{},
Invitation: &mock.InvitationRepositoryMock{},
},
Transaction: transx,
Token: token,
}
}
then in the implementation i just change the transaction function like this :
func (us *UsersServices) RegisterAccount(ctx context.Context, req RegisterRequest) (*RegisterResponse, error) {
var response = new(RegisterResponse)
err := utils.IsPasswordValid(req.Password)
if err != nil {
//Todo: handle error client
return nil, errorService.New(err, err)
}
err = us.Transaction.WithTx(ctx, func(tx *sql.Tx) error {
var newAccount repository.UserModel
newAccount.Email = req.Email
newAccount.Username = req.Username
if err = newAccount.Password.ParseFromPassword(req.Password); err != nil {
//Todo: handle error client
return errorService.New(err, err)
}
usrId, err := us.Repository.Users.Insert(ctx, tx, newAccount)
if err != nil {
switch {
case strings.Contains(err.Error(), CONFLICT_CODE):
return errorService.New(ErrUserAlreadyExist, err)
default:
//Todo: handle error client
return errorService.New(err, err)
}
}
tokenIvt := us.Token.Generate()
invt := repository.InvitationModel{
UserId: usrId,
Token: tokenIvt,
ExpireAt: time.Hour * 24,
}
err = us.Repository.Invitation.Insert(ctx, tx, invt)
if err != nil {
//Todo: handle error client
return errorService.New(err, err)
}
// register and invite success, send to response
response.Token = tokenIvt
return nil
})
if err != nil {
return nil, err
}
return response, nil
}
but my concern is i still need to pass the *sql.Tx
in the intefaces then ignore it in the test, im not sure it is good to just leave it like that