Skip to content

Commit

Permalink
doc: add readme
Browse files Browse the repository at this point in the history
  • Loading branch information
hedon954 committed Aug 22, 2024
1 parent ea8f4a0 commit b9cd37a
Show file tree
Hide file tree
Showing 15 changed files with 737 additions and 564 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: ['1.22']
go-version: ['1.20']
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
Expand All @@ -31,7 +31,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.59
version: v1.54
- name: Cache Go modules
uses: actions/cache@v4
with:
Expand Down
33 changes: 0 additions & 33 deletions .github/workflows/gen-plantuml.yml

This file was deleted.

29 changes: 0 additions & 29 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,11 @@ linters-settings:
line-length: 140
misspell:
locale: US
mnd:
checks: # "argument", "case", "condition", "operation", "return", "assign"
- case
- return
- assign
- condition
- operation
stylecheck:
checks:
- ST1000
- ST1001
- ST1002
# 不要启用 ST1003
- ST1004
- ST1005
- ST1006
Expand Down Expand Up @@ -73,7 +65,6 @@ linters:
- gocyclo
- gofmt
- goimports
- mnd
- goprintffuncname
- gosec
- gosimple
Expand All @@ -91,23 +82,3 @@ linters:
- unparam
- unused
- whitespace

# don't enable:
# - gochecknoglobals
# - gocognit
# - godox
# - maligned
# - prealloc

issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- gomnd
- funlen
exclude-dirs:
- vendor
new: true
new-from-rev: "HEAD~1"
whole-files: false
6 changes: 1 addition & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@ repos:
files: \.go$
- id: check-yaml # fix yaml format file.
- id: check-added-large-files # let you know which file has large file size.
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-mod-tidy # run go mod tidy -v.
- repo: local
hooks:
- id: golangci-lint
name: golangci-lint
description: run golangci-lint
entry: golangci-lint run -v ./...
entry: golangci-lint run -v
language: golang
files: \.*$
pass_filenames: false
Expand Down
99 changes: 98 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,100 @@
# gmm(go-mysql-mocker)

`gmm(go-mysql-mocker)` is a simple but useful tool to mock mysql server in Go project, it is especially useful in unit testing. This tool uses dolthub/go-mysql-server as memory server and gorm to auto create table and init data.
[![Go Report Card](https://goreportcard.com/badge/github.com/hedon954/gmm)](https://goreportcard.com/report/github.com/hedon954/gmm)
[![codecov](https://codecov.io/gh/hedon954/gmm/graph/badge.svg?token=RtwHYWTrso)](https://codecov.io/gh/hedon954/gmm)
[![CI](https://github.com/hedon954/gmm/workflows/build/badge.svg)](https://github.com/hedon954/gmm/actions)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/hedon954/gmm?sort=semver)](https://github.com/hedon954/gmm/releases)


`gmm(go-mysql-mocker)` was created to provide developers with a reliable and straightforward tool for mocking MySQL interactions in Go applications, particularly for automated testing environments. By simulating a MySQL server, `gmm` allows developers to conduct integration tests without the overhead of setting up a real database, thus speeding up test cycles and reducing external dependencies. This is especially useful in continuous integration (CI) environments where test reliability and speed are paramount.

![architecture](./design/img/architecture.png)

## Features

- Easy setup and integration with Go projects.
- Uses in-memory storage to speed up tests.
- Automatic table creation and data seeding using GORM models or SQL statements.

## Installation

```bash
go get -u github.com/hedon954/gmm
```

## Usage

- `Port(port int)`: Sets the MySQL server port to the specified value. This method allows you to customize the port on which the MySQL server listens.
- `CreateTable(model)`: Creates a database table based on the schema defined in the provided model. This method facilitates the automatic creation of tables directly from GORM models.
- `InitData(data)`: Populates the database with test data using instances of Go structs. This method supports initializing data from single instances or slices of instances that implement the schema.Tabler interface.
- `SQLStmts(stmts)`: Executes the provided SQL statements to generate test data. This method allows for direct insertion or modification of data using raw SQL commands.
- `SQLFiles(files)`: Reads SQL statements from specified files and executes them to generate test data. This method is useful for initializing the database with a larger set of pre-defined SQL operations.
- `Build()`: Initializes and starts the MySQL server with all specified configurations and initializes the data. This method must be called to execute the configurations set up by the preceding methods.


## Quick Start

### prepare a data model and implement `TableName()` interface
```go
type UserState struct {
UID string `gorm:"primaryKey;column:uid"`
State string `gorm:"column:state"`
}

func (u UserState) TableName() string {
return "user_state"
}
```

### write a business logic
```go
func ChangeUserStateToMatch(db *sql.DB, uid string) (int64, error) {
res, err := db.Exec("UPDATE user_state SET state = 'match' WHERE uid = ?", uid)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
```

### use `gmm` to test it
```go
func TestChangeUserStateToMatch(t *testing.T) {
t.Run("no data should have no affect row", func(t *testing.T) {
db, _, shutdown, err := gmm.Builder("db-name").Port(20201).CreateTable(UserState{}).Build()
assert.Nil(t, err)
defer shutdown()
res, err := ChangeUserStateToMatch(db, "1")
assert.Nil(t, err)
assert.Equal(t, int64(0), res)
})

t.Run("has uid 1 should affect 1 row and change state to `match`", func(t *testing.T) {
// prepare db and init data
origin := UserState{State: "no-match", UID: "1"}
db, gDB, shutdown, err := gmm.Builder().CreateTable(UserState{}).InitData(&origin).Build()
assert.Nil(t, err)
defer shutdown()

// check before change state
var before = UserState{}
assert.Nil(t, gDB.Select("uid", "state").Where("uid=?", "1").Find(&before).Error)
assert.Equal(t, origin, before)

// run biz logic
res, err := ChangeUserStateToMatch(db, "1")
assert.Nil(t, err)
assert.Equal(t, int64(1), res)

// check after change state
var after = UserState{}
assert.Nil(t, gDB.Select("uid", "state").Where("uid=?", "1").Find(&after).Error)
assert.Equal(t, UserState{
UID: "1",
State: "match",
}, after)
})
}
```

For more detail, please can see [example](./examples) folder.
31 changes: 23 additions & 8 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@ import (
"database/sql"
"errors"
"fmt"
"log/slog"
"os"
"reflect"
"strconv"
"sync/atomic"

"github.com/dolthub/go-mysql-server/server"
"github.com/google/uuid"
"golang.org/x/exp/slog"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)

// port is an atomic variable used to generate unique port numbers for tests.
// By incrementing the port number atomically, we aim to minimize the risk of port conflicts
// when running multiple tests concurrently.
var port atomic.Int32

func init() {
// Initialize the default port number.
// The initial value is set to 19527, and subsequent tests will use this value as a base,
// incrementing it atomically to ensure each test gets a unique port.
//
// Why 19527? Hhh, just for 9527~
port.Store(19527)
}

// GMMBuilder struct for building and managing the mock MySQL server
type GMMBuilder struct {
dbName string
port int
Expand All @@ -36,6 +45,8 @@ type GMMBuilder struct {
sqlFiles []string
}

// Builder initializes a new GMMBuilder instance with db name,
// if db name is not provided, gmm would generate a random db name.
func Builder(db ...string) *GMMBuilder {
b := &GMMBuilder{
port: int(port.Add(1)),
Expand All @@ -52,6 +63,8 @@ func Builder(db ...string) *GMMBuilder {
return b
}

// Port sets the port for the MySQL server,
// if not set, gmm would generate a port start from 19527
func (b *GMMBuilder) Port(port int) *GMMBuilder {
if b.err != nil {
return b
Expand All @@ -60,13 +73,14 @@ func (b *GMMBuilder) Port(port int) *GMMBuilder {
return b
}

// Build initializes and starts the MySQL server, returns handles to SQL and Gorm DB
func (b *GMMBuilder) Build() (sDB *sql.DB, gDB *gorm.DB, shutdown func(), err error) {
b.initServer()
if b.err != nil {
return nil, nil, nil, b.err
}

// start server
// Start server
slog.Info("start go mysql mocker server, listening at 0.0.0.0:" + strconv.Itoa(b.port))
go func() {
if err := b.server.Start(); err != nil {
Expand All @@ -78,14 +92,14 @@ func (b *GMMBuilder) Build() (sDB *sql.DB, gDB *gorm.DB, shutdown func(), err er
_ = b.server.Close()
}

// create client and connect to server
// Create client and connect to server
b.sqlDB, b.gormDB, err = createMySQLClient(b.port, b.dbName)
if err != nil {
b.err = fmt.Errorf("failed to create sql client: %w", err)
return nil, nil, nil, b.err
}

// prepare init data
// Initialize tables and data
b.initTables()
b.initWithModels()
b.initWithStmts()
Expand All @@ -97,6 +111,7 @@ func (b *GMMBuilder) Build() (sDB *sql.DB, gDB *gorm.DB, shutdown func(), err er
return b.sqlDB, b.gormDB, shutdown, nil
}

// initServer initializes the mock MySQL server
func (b *GMMBuilder) initServer() *GMMBuilder {
if b.err != nil {
return b
Expand All @@ -105,6 +120,7 @@ func (b *GMMBuilder) initServer() *GMMBuilder {
return b
}

// CreateTable adds a table to be created upon initialization
func (b *GMMBuilder) CreateTable(table schema.Tabler) *GMMBuilder {
if b.err != nil {
return b
Expand All @@ -113,8 +129,7 @@ func (b *GMMBuilder) CreateTable(table schema.Tabler) *GMMBuilder {
return b
}

// InitData adds init data, if data is a struct/pointer, should implement schema.Tabler.
// if data is a slice, each item should implement schema.Tabler, otherwise will return error.
// InitData adds initialization data to the mock database
func (b *GMMBuilder) InitData(data interface{}) *GMMBuilder {
if b.err != nil {
return b
Expand Down Expand Up @@ -143,7 +158,7 @@ func (b *GMMBuilder) InitData(data interface{}) *GMMBuilder {
return b
}

// SQLStmts adds sql statements, each statement should be a valid sql statement.
// SQLStmts adds SQL statements to be executed upon initialization
func (b *GMMBuilder) SQLStmts(stmts ...string) *GMMBuilder {
if b.err != nil {
return b
Expand All @@ -152,7 +167,7 @@ func (b *GMMBuilder) SQLStmts(stmts ...string) *GMMBuilder {
return b
}

// SQLFiles adds sql files, each file should be a valid sql file.
// SQLFiles adds SQL files whose contents are to be executed upon initialization
func (b *GMMBuilder) SQLFiles(files ...string) *GMMBuilder {
if b.err != nil {
return b
Expand Down
18 changes: 15 additions & 3 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func Test_GMMBuilder_Port(t *testing.T) {
})
}

func Test_GMMBuilder_File(t *testing.T) {
func Test_GMMBuilder_SQLFiles(t *testing.T) {
t.Run("non existing file should return err", func(t *testing.T) {
_, _, _, err := Builder().SQLFiles("fixtures/not-exist.sql").Build()
assert.NotNil(t, err)
Expand All @@ -51,7 +51,7 @@ func Test_GMMBuilder_File(t *testing.T) {
}

//nolint:lll
func Test_GMMBuilder_Stmt(t *testing.T) {
func Test_GMMBuilder_SQLStmts(t *testing.T) {
t.Run("invalid sql statement should return err", func(t *testing.T) {
_, _, _, err := Builder().SQLStmts("invalid sql statement").Build()
assert.NotNil(t, err)
Expand Down Expand Up @@ -82,7 +82,7 @@ INSERT INTO users (username, email) VALUES
})
}

func TestGMMBuilder_Data(t *testing.T) {
func Test_GMMBuilder_InitData(t *testing.T) {
t.Run("invalid data type should return err", func(t *testing.T) {
_, _, _, err := Builder().InitData(1).Build()
assert.NotNil(t, err)
Expand Down Expand Up @@ -121,3 +121,15 @@ func TestGMMBuilder_Data(t *testing.T) {
assert.Nil(t, err)
})
}

func TestGMMBuilder_CreateTable(t *testing.T) {
t.Run("valid type should ok", func(t *testing.T) {
_, _, _, err := Builder().CreateTable(CertificationInfo{}).Build()
assert.Nil(t, err)
})

t.Run("create multi times should ok", func(t *testing.T) {
_, _, _, err := Builder().CreateTable(CertificationInfo{}).CreateTable(CertificationInfo{}).Build()
assert.Nil(t, err)
})
}
Loading

0 comments on commit b9cd37a

Please sign in to comment.