Skip to content

Commit

Permalink
doc(HMS-5031): update TESTING.md
Browse files Browse the repository at this point in the history
Signed-off-by: Alejandro Visiedo <[email protected]>
  • Loading branch information
avisiedo committed Dec 18, 2024
1 parent 3118f27 commit 10c5ef7
Showing 1 changed file with 82 additions and 127 deletions.
209 changes: 82 additions & 127 deletions docs/dev/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,77 @@ launch are the expected ones, given the input data.
To check that, `sqlmock` is used. This allow to launch unit
tests without a database up and running.

The boilerplate generated by the database layer is huge, and to
make possible to mock the test execution, it is splited the sql
statments mock at `internal/test/sql/<first_table>_sql.go`, and
the helper function to prepare the scenario.

```golang
// Prepare the SQL query mock
func PrepSqlSelectSomething(mock, withError bool, expectedErr error, ...) {
// TIP A deterministic way takes more time than let the
// unit test just fails, and copy the current statement.
expectQuery := mock.ExpectedQuery(regexp.QuoteMeta(`SELECT ... FROM <first_table> ...`)).
withArgs(
data.OrgId,
data.DomainUuid,
1,
)
if withError {
expectQuery.WillReturnError(expectedErr)
} else {
expectQuery.WillReturnRows(sqlmock.NewRows([]string{
"id", "created_at", "updated_at", "deleted_at",

...
}).
AddRow(
domainID,
data.CreatedAt,
data.UpdatedAt,
nil,

...
))
}
}

// Name the function as the one we are testing to prepare the scenario
func FindByID(stage int, mock sqlmock.Sqlmock, expectedEr error, domainID uint, data *model.Domain) {
for i := 1; i <= stage; i++ {
switch i {
case 1:
PrepSqlSomething(mock, WithPredicateExpectedError(i, stage, expectedErr), expectedErr, domainID, data)
default:
panic(fmt.Sprintf("scenario %d/%d is not supported", i, stage))
}
}
}
```

Finally the unit test is reduced to call the helper that prepare the scenario
for the sql mock `FindByID` on this case. See at `internal/usecase/repository/domain_repository_test.go`:

```golang
func (s *DomainRepositorySuite) TestFindByID() {
t := s.T()
r := &domainRepository{}
s.mock.MatchExpectationsInOrder(true)

// ... Prepare data: TIP use here helpers at `internal/test/builder/model/`

expectedErr = fmt.Errorf(`...`)
test_sql.FindByID(1, s.mock, expectedErr, ...)
domain, err = r.FindByID(s.Ctx, data.OrgId, data.DomainUuid)
require.NoError(t, s.mockExpectationsWereMet())
assert.EqualError(t, err, expectedErr.Error())
assert.Nil(t, domain)
}
```

As the complexity grow, we can compose the helper scenarios as we need, that
would match with the same composition implemented in the repository layer.

Below an example preparing a mock which envolve a dynamic time.Time field (at: `internal/usecase/repository/domain_repository.go`)

```golang
Expand All @@ -142,132 +213,16 @@ s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "ipas" ("created_at","updated_a

---

Sometimes we could need more tracing; the below it was used to troubleshoot
some unit tests providing more traces without change the code.

```golang
func (s *Suite) SetupTest() {
var err error

logger.InitLogger(&config.Config{
Logging: config.Logging{
Console: true,
Level: "trace",
},
})

s.mock, s.DB, err = test.NewSqlMock(&gorm.Session{
SkipHooks: true,
Logger: logger.NewGormLog(false),
})
if err != nil {
s.Suite.FailNow("Error calling gorm.Open: %s", err.Error())
return
}
s.repository = &domainRepository{}
}
```
Sometimes we could need more tracing; in that case, update your
`confis/config.yaml` file to set the `"trace"` level; this will
print the SQL statement that is generated by gorm.

---

One last tip when creating unit tests for the repository layer, we
could duplicate code very quickly trying to cover all the different
paths for the repository layer. One thing that have been used is
a helper function that prepare the database mock in several stages
in an incremental way; for hms-1937 it was used to reduce code
duplication.

```golang
func (s *Suite) helperTestFindByID(stage int, data *model.Domain, mock sqlmock.Sqlmock, expectedErr error) {
for i := 1; i <= stage; i++ {
switch i {
case 1:
expectQuery := s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "domains" WHERE (org_id = $1 AND domain_uuid = $2) AND "domains"."deleted_at" IS NULL ORDER BY "domains"."id" LIMIT 1`)).
WithArgs(
data.OrgId,
data.DomainUuid,
)
if i == stage && expectedErr != nil {
expectQuery.WillReturnError(expectedErr)
} else {
autoenrollment := false
if data.AutoEnrollmentEnabled != nil {
autoenrollment = *data.AutoEnrollmentEnabled
}
expectQuery.WillReturnRows(sqlmock.NewRows([]string{
"id", "created_at", "updated_at", "deletet_at",

"org_id", "domain_uuid", "domain_name",
"title", "description", "type",
"auto_enrollment_enabled",
}).
AddRow(
data.ID,
data.CreatedAt,
data.UpdatedAt,
nil,

data.OrgId,
data.DomainUuid,
data.DomainName,
data.Title,
data.Description,
data.Type,
autoenrollment,
))
}
// ... Additional stages here
case 5:
expectedQuery := s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "ipa_servers" WHERE "ipa_servers"."ipa_id" = $1 AND "ipa_servers"."deleted_at" IS NULL`)).
WithArgs(1)
if i == stage && expectedErr != nil {
expectedQuery.WillReturnError(expectedErr)
} else {
expectedQuery.WillReturnRows(sqlmock.NewRows([]string{
"id", "created_at", "updated_at", "deletet_at",

"ipa_id", "fqdn", "rhsm_id", "location",
"ca_server", "hcc_enrollment_server", "hcc_update_server",
"pk_init_server",
}).
AddRow(
data.IpaDomain.Servers[0].Model.ID,
data.IpaDomain.Servers[0].Model.CreatedAt,
data.IpaDomain.Servers[0].Model.UpdatedAt,
data.IpaDomain.Servers[0].Model.DeletedAt,

data.IpaDomain.Servers[0].IpaID,
data.IpaDomain.Servers[0].FQDN,
data.IpaDomain.Servers[0].RHSMId,
data.IpaDomain.Servers[0].Location,
data.IpaDomain.Servers[0].CaServer,
data.IpaDomain.Servers[0].HCCEnrollmentServer,
data.IpaDomain.Servers[0].HCCUpdateServer,
data.IpaDomain.Servers[0].PKInitServer,
))
}
default:
panic(fmt.Sprintf("scenario %d/%d is not supported", i, stage))
}
}
}
```

Later in the unit test we just do:

```golang
// Check for 'ipas' record not found
expectedErr = gorm.ErrRecordNotFound
s.helperTestFindByID(2, data, s.mock, expectedErr)
domain, err = r.FindByID(s.DB, data.OrgId, data.DomainUuid.String())
require.NoError(t, s.mock.ExpectationsWereMet())
assert.EqualError(t, err, expectedErr.Error())
assert.Nil(t, domain)
```

The above could not be valid for all our scenarios, but is an
option to keep in mind when we are implementing the unit tests
for the repository layer.
paths for the repository layer. See if the SQL statement that you
are mocking already exists, and reuse that.

---

Expand All @@ -279,11 +234,11 @@ References:

## Unit test for presenters

Presenters are the opposite to interactors, and they
translate the resulting business model into the API
output. They are tested in a similar way to the
interactors. They don't store any state, and gather
a set of methods for the transformations.
Presenters in this repository are translating the
resulting business model into the API output. They
are tested in a similar way to the interactors.
They don't store any state, and gather a set of
methods for the transformations.

We validate the transformation and errors returned
are the expected in a similar way as the interactor.
Expand Down Expand Up @@ -526,7 +481,7 @@ Once we have defined the test case into the testCase slice, we run
all the test cases by calling to s.RunTestCases(testCases) which is
a method defined into the SuiteBase structure.

Finally add your suite test at `internal/test/smoke/suite_test.go`,
Finally add your suite test `internal/test/smoke/something_suite_test.go`,
at `TestSuite` function by:

```golang
Expand Down

0 comments on commit 10c5ef7

Please sign in to comment.