From c2255e90c4b51c96cf6c69fe7931d6484f095968 Mon Sep 17 00:00:00 2001 From: kenji-yamane <62660978+kenji-yamane@users.noreply.github.com> Date: Sun, 10 Jul 2022 13:32:32 -0300 Subject: [PATCH] Feat/add integration test (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * expose docker manager and fix testing suite name * add apply up integration test * add test driver * implement more of apply integration test * finish apply integration test * add automatic config file creation on tests * remove unnecessary mysql connection * remove error assessment * fix mock config file creation error * separate integration tests * attempt to add coverage reports to github actions * Update Makefile Co-authored-by: Alexandre Maranhão <56287238+alexandremr01@users.noreply.github.com> * change codecov reports Co-authored-by: Alexandre Maranhão <56287238+alexandremr01@users.noreply.github.com> --- .github/workflows/pr.yml | 8 +- .github/workflows/tag.yml | 8 +- .gitignore | 1 + Makefile | 19 +++- cmd/apply_test.go | 111 +++++++++++++++++++++++ cmd/command_suite_test.go | 89 +++++++++++++++++++ testing/dockermanager.go | 24 ++--- testing/dockermanager_test.go | 7 +- testing/fixtures/fixtures.go | 41 +++++++++ testing/fixtures/migrations_fixture.go | 112 ++++++++++++++++++++++++ testing/testdriver.go | 116 +++++++++++++++++++++++++ 11 files changed, 514 insertions(+), 22 deletions(-) create mode 100644 cmd/apply_test.go create mode 100644 cmd/command_suite_test.go create mode 100644 testing/fixtures/fixtures.go create mode 100644 testing/fixtures/migrations_fixture.go create mode 100644 testing/testdriver.go diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fced814..7cd9896 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -15,6 +15,10 @@ jobs: - name: Build run: make build - name: Run tests - run: make test + run: make check - name: Upload coverage reports to Codecov with GitHub Action - run: curl -Os https://uploader.codecov.io/latest/linux/codecov && chmod +x codecov && ./codecov -t ${CODECOV_TOKEN} + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} -f coverage.txt -F unit + ./codecov -t ${CODECOV_TOKEN} -f integration_coverage.txt -F integration diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index b971c9c..8f5965a 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -15,9 +15,13 @@ jobs: - name: Build run: make build - name: Run tests - run: make test + run: make check - name: Upload coverage reports to Codecov with GitHub Action - run: curl -Os https://uploader.codecov.io/latest/linux/codecov && chmod +x codecov && ./codecov -t ${CODECOV_TOKEN} + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} -f coverage.txt -F unit + ./codecov -t ${CODECOV_TOKEN} -f integration_coverage.txt -F integration tag: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 9b01109..0ff57b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin/ .idea .vscode coverage.txt +integration_coverage.txt dist .release-env diff --git a/Makefile b/Makefile index d1bca14..443ed0d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ install-tools: .PHONY: test test: - go test ./... -coverprofile=coverage.txt + go test --short ./... -coverprofile=coverage.txt .PHONY: coverage-report coverage-report: @@ -22,6 +22,23 @@ UNIT_COVERAGE:= $(shell go tool cover -func=coverage.txt | tail -n 1 | cut -d ' display-coverage: @echo "Unit Coverage: $(UNIT_COVERAGE)" +.PHONY: integration-test +integration-test: + go test -run Integration ./... -coverprofile=integration_coverage.txt + +.PHONY: integration-coverage-report +integration-coverage-report: + go tool cover -html=integration_coverage.txt + +INTEGRATION_COVERAGE:= $(shell go tool cover -func=integration_coverage.txt | tail -n 1 | cut -d ' ' -f 3 | rev | cut -c 1-5 | rev) + +.PHONY: integration-display-coverage +integration-display-coverage: + @echo "Integration Coverage: $(INTEGRATION_COVERAGE)" + +.PHONY: check +check: test integration-test + .PHONY: release release: @if [ ! -f ".release-env" ]; then \ diff --git a/cmd/apply_test.go b/cmd/apply_test.go new file mode 100644 index 0000000..51afe7e --- /dev/null +++ b/cmd/apply_test.go @@ -0,0 +1,111 @@ +package cmd + +import ( + "github.com/migratemgr8/mgr8/global" + "github.com/migratemgr8/mgr8/infrastructure" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Apply integration test", func() { + var ( + subject CommandExecutor = &apply{} + ) + + executeApply := func(args []string) { + err := subject.execute( + args, + dm.GetConnectionString(global.Postgres), + testMigrationsFolder, + postgresDriver, + infrastructure.CriticalLogLevel, + ) + Expect(err).To(BeNil()) + } + + Context("execute", func() { + When("up with 3", func() { + It("executes all three files that increase migration in folder", func() { + AssertStateBeforeAllMigrations() + executeApply([]string{"up", "3"}) + AssertStateAfterAllMigrations() + }) + }) + When("down with no number specified", func() { + It("decreases one", func() { + executeApply([]string{"down"}) + AssertStateAfterMigration0002AndBefore0003() + executeApply([]string{"down"}) + AssertStateAfterMigration0001AndBefore0002() + executeApply([]string{"down"}) + AssertStateBeforeAllMigrations() + }) + }) + When("different commands are executed sequentially", func() { + It("should work as expected", func() { + executeApply([]string{"up", "2"}) + AssertStateAfterMigration0002AndBefore0003() + executeApply([]string{"down", "1"}) + AssertStateAfterMigration0001AndBefore0002() + executeApply([]string{"up"}) + AssertStateAfterMigration0002AndBefore0003() + executeApply([]string{"down", "2"}) + AssertStateBeforeAllMigrations() + }) + }) + }) +}) + +func AssertStateBeforeAllMigrations() { + exists, err := postgresTestDriver.AssertTableExistence(userFixture0001.TableName) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + exists, err = postgresTestDriver.AssertViewExistence(userViewFixture0002.ViewName) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) +} + +func AssertStateAfterMigration0001AndBefore0002() { + exists, err := postgresTestDriver.AssertFixtureExistence(userFixture0001) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, firstNewColumnFixture0002) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + exists, err = postgresTestDriver.AssertViewExistence(userViewFixture0002.ViewName) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, secondNewColumnFixture0003) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) +} + +func AssertStateAfterMigration0002AndBefore0003() { + exists, err := postgresTestDriver.AssertFixtureExistence(userFixture0001) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, firstNewColumnFixture0002) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertViewFixtureExistence(userViewFixture0002) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, secondNewColumnFixture0003) + Expect(err).To(BeNil()) + Expect(exists).To(BeFalse()) +} + +func AssertStateAfterAllMigrations() { + exists, err := postgresTestDriver.AssertFixtureExistence(userFixture0001) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, firstNewColumnFixture0002) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertViewFixtureExistence(userViewFixture0002) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + exists, err = postgresTestDriver.AssertVarcharExistence(userFixture0001.TableName, secondNewColumnFixture0003) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) +} diff --git a/cmd/command_suite_test.go b/cmd/command_suite_test.go new file mode 100644 index 0000000..fe26f81 --- /dev/null +++ b/cmd/command_suite_test.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "github.com/migratemgr8/mgr8/applications" + "github.com/migratemgr8/mgr8/infrastructure" + "github.com/migratemgr8/mgr8/testing/fixtures" + "os" + "testing" + + "github.com/migratemgr8/mgr8/domain" + "github.com/migratemgr8/mgr8/drivers" + "github.com/migratemgr8/mgr8/global" + mgr8testing "github.com/migratemgr8/mgr8/testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _t *testing.T +var dm *mgr8testing.DockerManager + +var ( + postgresTestDriver mgr8testing.TestDriver + postgresDriver domain.Driver + postgresMigrations fixtures.MigrationsFixture + userFixture0001 fixtures.Fixture + firstNewColumnFixture0002 fixtures.VarcharFixture + userViewFixture0002 fixtures.ViewFixture + secondNewColumnFixture0003 fixtures.VarcharFixture +) + +var ( + testMigrationsFolder = "apply-test-migrations" + mockUser = "mock-user" +) + +func TestCommandIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration tests") + } + _t = t + RegisterFailHandler(Fail) + RunSpecs(t, "Command Test Suite") +} + +var _ = BeforeSuite(func() { + createConfigFileIfNotExists() + dm = mgr8testing.NewDockerManager() + + postgresTestDriver = mgr8testing.NewTestDriver(global.Postgres) + postgresDriver = getDriverSuccessfully(global.Postgres) + postgresMigrations = fixtures.NewMigrationsFixture(testMigrationsFolder, + infrastructure.NewFileService(), + postgresDriver.Deparser(), + ) + userFixture0001 = postgresMigrations.AddMigration0001() + firstNewColumnFixture0002, userViewFixture0002 = postgresMigrations.AddMigration0002() + secondNewColumnFixture0003 = postgresMigrations.AddMigration0003() +}) + +var _ = AfterSuite(func() { + err := dm.CloseAll() + Expect(err).To(BeNil()) + postgresMigrations.TearDown() +}) + +func createConfigFileIfNotExists() { + configPath, err := applications.GetConfigFilePath() + Expect(err).To(BeNil()) + config, err := os.Open(configPath) + if err == nil { + return + } + config, err = os.Create(configPath) + Expect(err).To(BeNil()) + username := mockUser + hostname, err := os.Hostname() + Expect(err).To(BeNil()) + err = applications.InsertUserDetails(username, hostname, config) + Expect(err).To(BeNil()) + err = config.Close() + Expect(err).To(BeNil()) +} + +func getDriverSuccessfully(d global.Database) domain.Driver { + driver, err := drivers.GetDriver(d) + Expect(err).To(BeNil()) + return driver +} diff --git a/testing/dockermanager.go b/testing/dockermanager.go index d9fe087..94f1ea3 100644 --- a/testing/dockermanager.go +++ b/testing/dockermanager.go @@ -4,7 +4,6 @@ import ( "database/sql" "log" "sync" - "sync/atomic" "time" _ "github.com/go-sql-driver/mysql" @@ -16,25 +15,23 @@ import ( "github.com/migratemgr8/mgr8/testing/databaseconfigs" ) -type dockerManager struct { +type DockerManager struct { pool *dockertest.Pool configs map[global.Database]DatabaseConfig resources map[global.Database]*dockertest.Resource - calls int64 } -var m *dockerManager +var m *DockerManager var initializeDockerManagerOnce sync.Once -func NewDockerManager() *dockerManager { +func NewDockerManager() *DockerManager { initializeDockerManagerOnce.Do(initializeDockerManager) - atomic.AddInt64(&m.calls, 1) return m } func initializeDockerManager() { - m = &dockerManager{calls: 0} + m = &DockerManager{} var err error m.pool, err = dockertest.NewPool("") if err != nil { @@ -77,17 +74,14 @@ func initializeDockerManager() { } } -func (m *dockerManager) GetConnectionString(d global.Database) string { +func (m *DockerManager) GetConnectionString(d global.Database) string { return m.configs[d].DatabaseUrl(m.resources[d]) } -func (m *dockerManager) CloseAll() error { - atomic.AddInt64(&m.calls, -1) - if m.calls == 0 { - for _, r := range m.resources { - if err := m.pool.Purge(r); err != nil { - return err - } +func (m *DockerManager) CloseAll() error { + for _, r := range m.resources { + if err := m.pool.Purge(r); err != nil { + return err } } return nil diff --git a/testing/dockermanager_test.go b/testing/dockermanager_test.go index 895771b..69d67e4 100644 --- a/testing/dockermanager_test.go +++ b/testing/dockermanager_test.go @@ -11,7 +11,10 @@ import ( var _t *testing.T -func TestApplication(t *testing.T) { +func TestTestingIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration tests") + } _t = t RegisterFailHandler(Fail) RunSpecs(t, "Testing Test Suite") @@ -19,7 +22,7 @@ func TestApplication(t *testing.T) { var _ = Describe("Check Command", func() { var ( - subject *dockerManager + subject *DockerManager ) BeforeSuite(func() { diff --git a/testing/fixtures/fixtures.go b/testing/fixtures/fixtures.go new file mode 100644 index 0000000..5731ac2 --- /dev/null +++ b/testing/fixtures/fixtures.go @@ -0,0 +1,41 @@ +package fixtures + +import "github.com/migratemgr8/mgr8/domain" + +type VarcharFixture struct { + Name string + Cap int64 +} + +func (f *VarcharFixture) ToDomainColumn() *domain.Column { + return &domain.Column{ + Parameters: map[string]interface{}{ + "size": f.Cap, + }, + Datatype: "varchar", + } +} + +type Fixture struct { + TableName string + VarcharColumns []VarcharFixture + TextColumns []string +} + +func (f *Fixture) ToDomainTable() *domain.Table { + t := domain.NewTable(f.TableName, map[string]*domain.Column{}) + for _, varchar := range f.VarcharColumns { + t.Columns[varchar.Name] = varchar.ToDomainColumn() + } + for _, text := range f.TextColumns { + t.Columns[text] = &domain.Column{Datatype: "text"} + } + return t +} + +type ViewFixture struct { + ViewName string + TextColumns []string + VarcharColumns []VarcharFixture + Statement string +} diff --git a/testing/fixtures/migrations_fixture.go b/testing/fixtures/migrations_fixture.go new file mode 100644 index 0000000..eb1676e --- /dev/null +++ b/testing/fixtures/migrations_fixture.go @@ -0,0 +1,112 @@ +package fixtures + +import ( + "fmt" + "os" + + "github.com/migratemgr8/mgr8/domain" + "github.com/migratemgr8/mgr8/infrastructure" + + . "github.com/onsi/gomega" +) + +type MigrationsFixture interface { + AddRawFile(filename, content string) + AddMigration0001() Fixture + AddMigration0002() (VarcharFixture, ViewFixture) + AddMigration0003() VarcharFixture + TearDown() +} + +type migrationsFixture struct { + folderPath string + fileService infrastructure.FileService + usersTable Fixture + usersView ViewFixture + deparser domain.Deparser +} + +func NewMigrationsFixture(folderPath string, fileService infrastructure.FileService, deparser domain.Deparser) MigrationsFixture { + err := fileService.CreateFolderIfNotExists(folderPath) + Expect(err).To(BeNil()) + return &migrationsFixture{ + folderPath: folderPath, + fileService: fileService, + usersTable: Fixture{TableName: "users"}, + deparser: deparser, + } +} + +func (f *migrationsFixture) AddRawFile(filename, content string) { + err := f.fileService.Write(f.folderPath, filename, content) + Expect(err).To(BeNil()) +} + +func (f *migrationsFixture) AddMigration0001() Fixture { + f.usersTable.VarcharColumns = []VarcharFixture{ + {"social_number", 9}, + {"name", 15}, + {"phone", 11}, + } + upStatements := []string{ + f.deparser.CreateTable(f.usersTable.ToDomainTable()), + } + f.AddRawFile(f.migrationUpMockName(1), f.deparser.WriteScript(upStatements)) + downStatements := []string{ + f.deparser.DropTable(f.usersTable.TableName), + } + f.AddRawFile(f.migrationDownMockName(1), f.deparser.WriteScript(downStatements)) + return f.usersTable +} + +func (f *migrationsFixture) AddMigration0002() (VarcharFixture, ViewFixture) { + f.usersView = ViewFixture{ + ViewName: "user_phones", + VarcharColumns: []VarcharFixture{f.usersTable.VarcharColumns[2]}, + TextColumns: []string{"full_phone"}, + } + newVarcharFixture := VarcharFixture{Name: "ddi", Cap: 3} + f.usersTable.VarcharColumns = append(f.usersTable.VarcharColumns, newVarcharFixture) + f.usersView.Statement = fmt.Sprintf(`SELECT %s, CONCAT(%s, %s) AS %s FROM %s`, + f.usersView.VarcharColumns[0].Name, + f.usersTable.VarcharColumns[2].Name, f.usersTable.VarcharColumns[3].Name, + f.usersView.TextColumns[0], f.usersTable.TableName) + upStatements := []string{ + f.deparser.AddColumn(f.usersTable.TableName, newVarcharFixture.Name, newVarcharFixture.ToDomainColumn()), + fmt.Sprintf(`CREATE VIEW %s AS %s`, f.usersView.ViewName, f.usersView.Statement), + } + f.AddRawFile(f.migrationUpMockName(2), f.deparser.WriteScript(upStatements)) + downStatements := []string{ + fmt.Sprintf("DROP VIEW IF EXISTS %s", f.usersView.ViewName), + f.deparser.DropColumn(f.usersTable.TableName, newVarcharFixture.Name), + } + f.AddRawFile(f.migrationDownMockName(2), f.deparser.WriteScript(downStatements)) + return newVarcharFixture, f.usersView +} + +func (f *migrationsFixture) AddMigration0003() VarcharFixture { + newVarcharFixture := VarcharFixture{Name: "abc", Cap: 3} + f.usersTable.VarcharColumns = append(f.usersTable.VarcharColumns, newVarcharFixture) + upStatements := []string{ + f.deparser.AddColumn(f.usersTable.TableName, newVarcharFixture.Name, newVarcharFixture.ToDomainColumn()), + } + f.AddRawFile(f.migrationUpMockName(3), f.deparser.WriteScript(upStatements)) + downStatements := []string{ + f.deparser.DropColumn(f.usersTable.TableName, newVarcharFixture.Name), + } + f.AddRawFile(f.migrationDownMockName(3), f.deparser.WriteScript(downStatements)) + return newVarcharFixture +} + +func (f *migrationsFixture) TearDown() { + err := os.RemoveAll(f.folderPath) + Expect(err).To(BeNil()) +} + +func (f *migrationsFixture) migrationUpMockName(n int) string { + return fmt.Sprintf("%04d_test_migration.up.sql", n) +} + +func (f *migrationsFixture) migrationDownMockName(n int) string { + return fmt.Sprintf("%04d_test_migration.down.sql", n) +} diff --git a/testing/testdriver.go b/testing/testdriver.go new file mode 100644 index 0000000..19ffa0e --- /dev/null +++ b/testing/testdriver.go @@ -0,0 +1,116 @@ +package testing + +import ( + "github.com/jmoiron/sqlx" + "github.com/migratemgr8/mgr8/global" + "github.com/migratemgr8/mgr8/testing/fixtures" + "log" +) + +type TestDriver interface { + AssertTableExistence(tableName string) (bool, error) + AssertViewExistence(viewName string) (bool, error) + AssertTextExistence(tableName string, columnName string) (bool, error) + AssertVarcharExistence(tableName string, varchar fixtures.VarcharFixture) (bool, error) + AssertFixtureExistence(fixture fixtures.Fixture) (bool, error) + AssertViewFixtureExistence(fixture fixtures.ViewFixture) (bool, error) +} + +type testDriver struct { + db *sqlx.DB +} + +func NewTestDriver(d global.Database) TestDriver { + if d == global.MySql { + // TODO + log.Fatalf("not implemented") + } + dm := NewDockerManager() + url := dm.GetConnectionString(d) + conn, err := sqlx.Connect(d.String(), url) + if err != nil { + log.Fatalf("%v", err) + } + return &testDriver{ + db: conn, + } +} + +func (d *testDriver) AssertTableExistence(tableName string) (bool, error) { + var exists bool + err := d.db.QueryRow(` + SELECT EXISTS ( + SELECT FROM information_schema.tables WHERE table_name = $1 + )`, tableName).Scan(&exists) + return exists, err +} + +func (d *testDriver) AssertViewExistence(viewName string) (bool, error) { + var exists bool + err := d.db.QueryRow(` + SELECT EXISTS ( + SELECT FROM information_schema.views WHERE table_name = $1 + )`, viewName).Scan(&exists) + return exists, err +} + +func (d *testDriver) AssertTextExistence(tableName string, columnName string) (bool, error) { + var exists bool + err := d.db.QueryRow(` + SELECT EXISTS ( + SELECT FROM information_schema.columns WHERE table_name = $1 + AND column_name = $2 AND data_type = 'text' + )`, tableName, columnName).Scan(&exists) + return exists, err +} + +func (d *testDriver) AssertVarcharExistence(tableName string, varchar fixtures.VarcharFixture) (bool, error) { + var exists bool + err := d.db.QueryRow(` + SELECT EXISTS ( + SELECT FROM information_schema.columns WHERE table_name = $1 + AND column_name = $2 AND data_type = 'character varying' + AND character_maximum_length = $3 + )`, tableName, varchar.Name, varchar.Cap).Scan(&exists) + return exists, err +} + +func (d *testDriver) AssertFixtureExistence(fixture fixtures.Fixture) (bool, error) { + exists, err := d.AssertTableExistence(fixture.TableName) + if err != nil || !exists { + return false, err + } + for _, text := range fixture.TextColumns { + exists, err = d.AssertTextExistence(fixture.TableName, text) + if err != nil || !exists { + return false, err + } + } + for _, varchar := range fixture.VarcharColumns { + exists, err = d.AssertVarcharExistence(fixture.TableName, varchar) + if err != nil || !exists { + return false, err + } + } + return true, nil +} + +func (d *testDriver) AssertViewFixtureExistence(fixture fixtures.ViewFixture) (bool, error) { + exists, err := d.AssertViewExistence(fixture.ViewName) + if err != nil || !exists { + return false, err + } + for _, text := range fixture.TextColumns { + exists, err = d.AssertTextExistence(fixture.ViewName, text) + if err != nil || !exists { + return false, err + } + } + for _, varchar := range fixture.VarcharColumns { + exists, err = d.AssertVarcharExistence(fixture.ViewName, varchar) + if err != nil || !exists { + return false, err + } + } + return true, nil +}