visit
Problem - How many times have we faced this problem of worrying about cleaning up after running test cases. When some of our test cases run, they might add data to our database or add files to our directory which we don’t want to worry about every time.
Solution -We can easily solve this using Docker. Imagine spawning a Docker container just to run test cases and killing it after it’s purpose is solved. Poof! our problem is solved, no need to worry about the collateral damage done by our test cases since killing that container handles that for us.
func main() {
// Initialize a connextion to DB. Sourcename contains the auth information, host information
// obtained from the env.
db, err := sql.Open("mysql", sourceName)
if err != nil {
panic(err)
}
Init(db)
}
// Init initializes the dependecies and boots up the server.
func Init(db *sql.DB) {
// Initialize our model
repo := todo.NewListRepository(db)
err := repo.Init()
if err != nil {
panic(err)
}
// Pass our model to our service which will handle business logic
listService := todo.NewListService(repo)
// start server
http.HandleFunc("/save", listService.AddItem)
http.HandleFunc("/delete", listService.DeleteItem)
http.HandleFunc("/find", listService.FindItem)
http.ListenAndServe(":8080", nil)
}
type ListService struct {
repo ListRepository
}
func NewListService(repo ListRepository) (service ListService) {
return ListService{
repo: repo,
}
}
func (service ListService) AddItem(w http.ResponseWriter, r *http.Request) {
param1 := r.URL.Query().Get("item")
if param1 != "" {
ID := uuid.New().String()
err := service.repo.SaveItem(ID, param1)
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(400)
return
}
w.Write([]byte(ID))
} else {
w.WriteHeader(500)
}
}
func (service ListService) DeleteItem(w http.ResponseWriter, r *http.Request) {
ID := r.URL.Query().Get("ID")
if ID != "" {
err := service.repo.DeleteItem(ID)
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(400)
return
}
w.Write([]byte("delete success"))
} else {
w.WriteHeader(500)
}
}
...
// ListRepository is an interface
type ListRepository interface {
SaveItem(ID string, name string) (err error)
FindItem(ID string) (res string, err error)
DeleteItem(ID string) (err error)
Init() (err error)
}
// MySQLListRepository impl of Listrepo
type MySQLListRepository struct {
// conn sql.Conn
db *sql.DB
}
// NewListRepository is a constructor
func NewListRepository(db *sql.DB) (repo ListRepository) {
repo = MySQLListRepository{db: db}
return repo
}
func (repo MySQLListRepository) SaveItem(ID string, name string) (err error) {
_, err = repo.db.Exec("Insert into `items` (`ID`, `name`) values (?, ?)", ID, name)
return
}
func (repo MySQLListRepository) FindItem(ID string) (res string, err error) {
var row *sql.Row
row = repo.db.QueryRow("SELECT `name` FROM `items` WHERE `ID` = ?; ", ID)
err = row.Scan(&res)
return
}
func (repo MySQLListRepository) DeleteItem(ID string) (err error) {
var res sql.Result
var affected int64
res, err = repo.db.Exec("DELETE FROM `items` WHERE `ID` = ?; ", ID)
affected, err = res.RowsAffected()
if err != nil {
return
}
if affected == 0 {
return errors.New("invalid ID")
}
return
}
func (repo MySQLListRepository) Init() (err error) {
// var result sql.Result
_, err = repo.db.Exec(`CREATE TABLE IF NOT EXISTS items (ID VARCHAR(255), name VARCHAR(255)) ENGINE=INNODB;`)
return err
}
Starting and stopping the docker containers via code
There are essentially docker clients available for each languagefunc TestMain(m *testing.M) {
// Create a new pool of connections
pool, err := dockertest.NewPool("myPool")
// Connect to docker on local machine
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
// Spawning a new MySQL docker container. We pass desired credentials for accessing it.
resource, err := pool.Run("mysql", "5.7", []string{"MYSQL_ROOT_PASSWORD=secret"})
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
connected := false
// Try connecting for 200secs
for i := 0; i < 20; i++ {
// Try establishing MySQL connection.
Conn, err = sql.Open("mysql", fmt.Sprintf("root:secret@(localhost:%s)/mysql?parseTime=true", resource.GetPort("3306/tcp")))
if err != nil {
panic(err)
}
err = Conn.Ping()
if err != nil {
// connection established success
connected = true
break
}
// Sleep for 10 sec and try again.
}
if !connected {
fmt.Println("Couldnt connect to SQL")
pool.Purge(resource)
}
// Run our unit test cases
code := m.Run()
// Purge our docker containers
if err := pool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
os.Exit(code)
}
func TestRepo(t *testing.T) {
assert := assert.New(t)
listRepo := todo.NewListRepository(Conn)
err := listRepo.Init()
assert.NoError(err, "error while initializing repo")
ID := uuid.New().String()
name := "itemName 1"
// Save Item
err = listRepo.SaveItem(ID, name)
assert.NoError(err, "error while saving item")
// Find Item
foundName, err := listRepo.FindItem(ID)
assert.NoError(err, "error while saving item")
assert.Equal(foundName, name)
// Delete Item
err = listRepo.DeleteItem(ID)
assert.NoError(err, "error while saving item")
foundName, err = listRepo.FindItem(ID)
assert.Error(err, "delete unsuccessful")
}
Setup
FROM golang:latest
WORKDIR $GOPATH/src/todo
ENV GO111MODULE=on
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ./main
services:
mysql_db:
image: mysql:5.7
environment: # Set up mysql database name and password
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: todo
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- 3306:3306
networks:
- my-network
todo_app:
image: todo_app
container_name: todo_app
environment: # Set up mysql database name and password
host: 0.0.0.0:3306 # Passing MySQL host
build:
context: .
dockerfile: Dockerfile
depends_on:
- mysql_db
ports:
- "8080:8080"
networks:
- my-network
networks:
my-network:
driver: bridge
docker-compose up -d
func TestE2E(t *testing.T) {
assert := assert.New(t)
client := &http.Client{}
req, err := http.NewRequest("GET", "localhost:8080/save?item=test122", nil)
assert.NoError(err, "error while initializing request")
res, err := client.Do(req)
assert.NoError(err, "error while making request")
assert.Equal(res.StatusCode, 200)
... // Other E2E requests
}
After we have completed the test we will have to manually shutdown todo server and SQL containers using `docker-compose down`. In this case, we have to manually start and stop the docker service but this can be automated as well by using commands via code to start and stop the containers.
Hence by doing this, we ensure that if our app makes changes in our system or our database those are not persisted and we don't have to clean those up.Conclusion:
Hence we saw using docker we can run e2e and unit test without creating a mess of our system rendered by our application. Full code available .Previously published at