Go Golang

11 out, 2022

Como fazer teste unitário no GORM com testify e sqlmock

Publicidade

No último final de semana, eu passei praticamente a tarde toda do domingo tentando escrever testes unitários para o GORM utilizando sqlmock.

O problema é que todos os tutoriais que eu encontrei eram de versões antigas, tanto do GORM, quanto do sqlmock.

Somente na segunda-feira, depois de mais umas 2h tentando entender como eles funcionavam e o que os erros estavam me dizendo foi que consegui fazer os testes funcionarem.

Por causa desse trabalho todo, resolvi fazer esse post mostrando como escrever testes unitários para GORM com sqlmock e testify.

Se você nunca utilizou testify ou quer saber um pouco mais sobre, recomendo a leitura do nosso post “Como usar testify para escrever testes“. Além de um exemplo básico, explicamos qual a finalidade de cada um dos 4 packages que compõem a suite do testify.

Continuando… para facilitar, vou separar o post em testes para INSERT, UPDATE, DELETE e SELECT.

Antes de começar a escrever os testes, vamos criar um arquivo person.go com uma struct, um repository e uma função para fazer migration e retornar uma nova instância do repository.

package db
import (
“gorm.io/gorm”
“github.com/google/uuid”
)
type Person struct {
ID uuid.UUID
Name string
Age uint8
}
type repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *repository {
db.AutoMigrate(&Person{})
return &repository{db}
}

No arquivo de testes, o person_test.go, vamos precisar de uma suite de testes do testify. Para isso, precisamos criar uma struct e um método SetupSuite.

package db
import (
“database/sql”
“regexp”
“testing”
“github.com/google/uuid”
“github.com/DATA-DOG/go-sqlmock”
“gorm.io/driver/postgres”
“gorm.io/gorm”
“github.com/stretchr/testify/assert”
“github.com/stretchr/testify/suite”
)
type RepositorySuite struct {
suite.Suite
conn *sql.DB
DB *gorm.DB
mock sqlmock.Sqlmock
repo *repository
person *Person
}
func (rs *RepositorySuite) SetupSuite() {
var (
err error
)
rs.conn, rs.mock, err = sqlmock.New()
assert.NoError(rs.T(), err)
dialector := postgres.New(postgres.Config{
DSN: “sqlmock_db_0”,
DriverName: “postgres”,
Conn: rs.conn,
PreferSimpleProtocol: true,
})
rs.DB, err = gorm.Open(dialector, &gorm.Config{})
assert.NoError(rs.T(), err)
rs.repo = NewRepository(rs.DB)
assert.IsType(rs.T(), &repository{}, rs.repo)
rs.person = &Person{
ID: uuid.New(),
Name: “Tiago”,
Age: 32,
}
}

Se você prestar atenção, vai ver que o import desse arquivo tem mais packages do que usamos até agora. Não se preocupe, todos esses packages serão usados ao longo da implementação dos testes. Preferi adicioná-los logo no começo para que possamos focar na escrita dos testes.

Por último, antes de iniciar, vamos adicionar mais um método à nossa suite de testes.

 

func (rs *RepositorySuite) AfterTest(_, _ string) {
assert.NoError(rs.T(), rs.mock.ExpectationsWereMet())
}

Esse método irá checar após cada teste se todas as expectativas foram atendidas corretamente.

Insert

Para o insert, vamos considerar o seguinte método.

 

func (r *repository) Insert(p *Person) (*Person, error) {
result := r.db.Create(&p)
if result.Error != nil {
return nil, result.Error
}
return p, nil
}

Como ao executar o método Create o GORM executa uma transação, para escrever nosso teste precisamos seguir os seguintes passo:

  • Iniciar uma transação
  • Executar a transação
  • Avaliar o retorno
  • Commitar a transação
func (rs *RepositorySuite) TestInsert() {
rs.mock.ExpectBegin() // inicia a transação
rs.mock.ExpectExec( // executa a transação
regexp.QuoteMeta(`INSERT INTO “people” (“id”,”name”,”age”) VALUES ($1,$2,$3)`)).
WithArgs( // adiciona os argumentos referentes ao $1,$2,$3
rs.person.ID,
rs.person.Name,
rs.person.Age).
WillReturnResult(sqlmock.NewResult(1, 1)) // avalia o resultado
rs.mock.ExpectCommit() // commita a transação
p, err := rs.repo.Insert(rs.person) // chama o método Insert do repository
assert.NoError(rs.T(), err) // avalia se não houve nenhum erro na execução
assert.Equal(rs.T(), rs.person, p) // verificar se as struts são iguais
}

Se você olhar na documentação do sqlmock verá que a função NewResult espera os parâmetros ID e número de linhas afetadas.

Como ambos os parâmetros são do tipo int e nosso id é um UUID, não conseguimos passar o UUID como parâmetro. No entanto, utilizando o valor 1 como ID ele funciona corretamente.

Update

No repository, vamos adicionar o seguinte método.

 

func (r *repository) Update(p *Person) (*Person, error) {
result := r.db.Model(&p).Updates(Person{
Name: p.Name,
})
if result.Error != nil {
return nil, result.Error
}
return p, nil
}

Assim como o método Create, o método Updates também realiza uma transação com o banco de dados. Sendo assim, os passos necessários para testá-lo são praticamente os mesmos do insert.

 

func (rs *RepositorySuite) TestUpdate() {
rs.person.Name = “Tiago Temporin”
rs.mock.ExpectBegin() // inicia a transação
rs.mock.ExpectExec( // executa a transação
regexp.QuoteMeta(`UPDATE “people” SET “name”=$1 WHERE “id” = $2`)).
WithArgs( // inicia os argumentos referentes a $1 e $2
rs.person.Name,
rs.person.ID).
WillReturnResult(sqlmock.NewResult(1, 1)) // avalia o resultado
rs.mock.ExpectCommit() // commita a transação
p, err := rs.repo.Update(rs.person) // chama o método Update do repository
assert.NoError(rs.T(), err) // avalia se não houve nenhum erro na execução
assert.Equal(rs.T(), rs.person, p) // verificar se as struts são iguais
}

Um ponto que vale a pena salientar aqui é que você não consegue mudar a order dos parâmetros na query. Em outras palavras, escrever a query assim UPDATE "people" SET "name"=$2 WHERE "id" = $1 não funciona.

Delete

Voltando para o nosso repository, vamos implementar o método Delete.

 

func (r *repository) Delete(id uuid.UUID) error {
p := Person{ID: id}
result := r.db.Delete(&p)
return result.Error
}

E adivinha?!?!?! O método Delete também realiza uma transação com o banco de dados com a função Exec do package database/sql!

E como já conhecemos bem a ordem lógica, bora para o person_test.go implementar esse teste.

 

func (rs *RepositorySuite) TestDelete() {
rs.mock.ExpectBegin() // inicia a transação
rs.mock.ExpectExec( // executa a transação
regexp.QuoteMeta(`DELETE FROM “people” WHERE “people”.”id” = $1`)).
WithArgs(rs.person.ID). // inicia o argumento $1
WillReturnResult(sqlmock.NewResult(0, 1)) // valida o resultado
rs.mock.ExpectCommit() // commita a transação
err := rs.repo.Delete(rs.person.ID) // chama o método Delete do repository
assert.NoError(rs.T(), err) // avalia se não houve nenhum erro na execução
}

Note que diferente do update, o where no delete precisa ter o nome da tabela antes do nome do campo.

Select

Para o select, vamos implementar dois métodos. O primeiro será para buscar todos os registros, enquanto o segundo irá filtrar os registros pelo ID.

 

func (r *repository) Find() ([]Person, error) { // busca todos
var people []Person
result := r.db.Find(&people)
if result.Error != nil {
return nil, result.Error
}
return people, nil
}
func (r *repository) FindByID(id uuid.UUID) (*Person, error) { // filtra
var p Person
result := r.db.First(&p, id)
if result.Error != nil {
return nil, result.Error
}
return &p, nil
}

Para testar uma função do tipo select, vamos precisar mudar um pouco a sequência lógica que utilizamos até aqui.

Pelo fato dos select ser implementado em cima da função Query do package database/sql os passos serão o seguintes:

  • Adicionar uma lista de registros
  • Adicionar a query que esperamos ser gerada
  • Validar os registros retornados
func (rs *RepositorySuite) TestFind() {
rows := sqlmock.NewRows([]string{“id”, “name”, “age”}). // adiciona o nome das colunas
AddRow( // adiciona o primeiro registro
rs.person.ID,
rs.person.Name,
rs.person.Age).
AddRow( // adiciona o segundo registro
uuid.New(),
“Maria Silva”,
27)
rs.mock.ExpectQuery( // adiciona a query que esperamos ser gerada
regexp.QuoteMeta(`SELECT * FROM “people”`)).
WithArgs().
WillReturnRows(rows) // valida os registros retornados
people, err := rs.repo.Find() // chama o método Find do repository
assert.NoError(rs.T(), err) // valida se houve algum erro
assert.Contains(rs.T(), people, *rs.person) // valida se o registro está no slice do resultado
}

Para filtrar um registro, só precisamos mudar a query que esperamos e adicionar os argumentos para realizar o filtro.

func (rs *RepositorySuite) TestFindyByID() {
rows := sqlmock.NewRows([]string{“id”, “name”, “age”}). // adiciona o nome das colunas
AddRow( // adiciona o primeiro registro
rs.person.ID,
rs.person.Name,
rs.person.Age)
rs.mock.ExpectQuery( // adiciona a query que esperamos ser gerada
regexp.QuoteMeta(`SELECT * FROM “people” WHERE “people”.”id” = $1`)).
WithArgs(rs.person.ID).
WillReturnRows(rows)
p, err := rs.repo.FindByID(rs.person.ID) // chama o método FindByID do repository
assert.NoError(rs.T(), err) // valida se houve algum erro
assert.Equal(rs.T(), rs.person, p) // valida se as structs são iguais
}

Calma que ainda não acabou!!!!

Se você tentar executar os testes agora, verá que nada irá acontecer. Isso por que não implementamos nenhuma função de teste do Go, tudo que fizemos até agora foi configurar a suite de testes do testify.

Para que essa suite de testes seja executada, precisamos adicionar a seguinte função.

 

func TestSuite(t *testing.T) {
suite.Run(t, new(RepositorySuite))
}

Agora sim!!!! Se você executar um go test ./... verá que todos os testes irão passar.

Deixem suas dúvidas nos comentários.

Até a próxima!