Web Application
Features
- *Web MVC (Model-View-Controller).
- Auto Configuration, pre-created instance with properties configs for dependency injection.
- Dependency injection with the struct tag `inject:“”` or the constructor.
Introduction to Hiboot MVC
Hiboot prefers to hide the business-independent code, so that the developers will concentrate the business logic.
unlike most of the Go web frameworks, Hiboot does not need to setup routes. Hiboot use reflection to construct the routes.
Project structure
.
├── config
│ ├── application-gorm.yml
│ ├── application-local.yml
│ ├── application.yml
│ ├── i18n
│ │ ├── en-US.ini
│ │ └── zh-CN.ini
│ ├── keygen.sh
│ └── ssl
│ ├── app.rsa
│ └── app.rsa.pub
├── main.go
├── main_test.go
├── controller
│ ├── user.go
│ └── user_test.go
├── entity
│ ├── user.go
│ └── user_test.go
└── service
├── user.go
└── user_test.go
Hiboot application include source code and config
Application properties
Hiboot lets you externalize your configuration so that you can work with the same application code in different environments.
Property values can be injected directly into the struct using the `value:“”` tag.
To provide a concrete example, suppose you develop a struct that uses a app.name
property, as shown in the following example:
type MyService struct {
AppName string `value:"${app.name}"`
}
Common application properties
Various properties can be specified inside your application.yml file. Hiboot loads properties form application.yml in config folder of working directory.
config/application.yml
app:
project: examples
name: gorm-demo
profiles:
include:
- actuator
- locale
- logging
- gorm
logging:
level: info
application.yml field description
Field | Description | Options | Example |
---|---|---|---|
app.project | project name | any string | examples |
app.name | application name | any string | gorm-demo |
app.profiles.active | application active profile | local,dev,test,staging,prod | dev |
app.profiles.include | we use this field as starter switcher,⚠️ if the starter is imported in your source file but it’s not included here, the starter will not be initialized | pakcage name of the starter | actuator, locale, logging, gorm |
logging.level | set the logging level | debug,info,warn,error,fatal | info |
Profile-specific properties
In addition to application.yml
file, profile-specific properties can also be defined by using the following naming convention: application-${profile}.yml
, active profiles can be set in application.yml
.
Profile-specific properties are loaded from the same locations as standard application.yml
, with profile-specific files always overriding the non-specific ones. application-${app.profiles.active}.yml
is always be the highest priority.
config/application-local.yml
server:
port: 8081
logging:
level: debug
application-local.yml field description
Field | Description | Options | Example |
---|---|---|---|
server.port | service port that the application is listening on, it will overwrite the server.port in application.yml | any number | 8081 |
logging.level | set logging level | debug,info,warn,error,fatal | info |
config/application-gorm.yml
gorm:
type: mysql
host: mysql-${app.profiles.active:dev}
port: 3306
database: ${app.name:test}
username: demo
password: fafUJCsVXf2Thj0d4n6UqNdX2PfI08fMyaNlrZhbJVVkghnZ+Zc/WCdITXflJpHZjYH5+LbLviy/6j9etPNwtdyAOXiqKI62itC6nDgp0Xlzu0qX8MwMIIAosUwaYpnflg23hRZnueKrq6SrEpkx4X+LWluDgHb2O5VfGGvHliE=
charset: utf8
parseTime: true
loc: Asia/Shanghai
config:
decrypt: true
application-gorm.yml field description
Field | Description | Options | Example |
---|---|---|---|
gorm.type | database type | mysql,postgres,sqlite3,mssql | mysql |
gorm.host | database host, can be IP address or DNS name, mysql-${app.profiles.active} if environment variable APP_PROFILES_ACTIVE is set to local ,then gorm.host will be mysql-local ) |
mysql-${app.profiles.active} |
|
gorm.port | database port | any port | 3306 |
gorm.database | database name | string or variable | ${app.name:test} |
gorm.username | username for databse connection | valid username in string | dbuser |
gorm.password | password for database connnection, if gorm.config.decrypt is true,then it will be the password that encrypted by crypto | string | fafUJ … O5VfGGvHliE= |
gorm.charset | character set | utf8,ascii | utf8 |
gorm.parseTime | parse time or not | true,false | true |
gorm.loc | timezone | see world timezone | Asia/Shanghai |
gorm.config.decrypt | decrypt or not | true,false | true |
Writing the Code
To write Hiboot application, as we know, the executable commands must always use package main, so we need to create the main package first.
See Effective GO to learn more about Go’s naming conventions.
main.go
There are two parts inside the main package:imports and the entrance of the web application。
In order to decouple each packages, hiboot use registeration, auto configuration and dependency injection, as we need to import some packages solely for their side effects, for exampole:
_ "hidevops.io/hiboot-data/examples/gorm/controller"
, if you used hiboot starter you may need import them in this way as well:_ "hidevops.io/hiboot/pkg/starter/actuator"
,_ "hidevops.io/hiboot/pkg/starter/locale"
,_ "hidevops.io/hiboot/pkg/starter/logging"
the function main is extremely simple, that is
web.NewApplication().Run()
, the packageweb
is imported fromhidevops.io/hiboot/pkg/app/web
package main import ( _ "hidevops.io/hiboot-data/examples/gorm/controller" "hidevops.io/hiboot/pkg/app/web" _ "hidevops.io/hiboot/pkg/starter/actuator" _ "hidevops.io/hiboot/pkg/starter/locale" _ "hidevops.io/hiboot/pkg/starter/logging" ) func main() { web.NewApplication().Run() }
Alternatively, you can set property
app.ProfilesInclude
in main function. In this approach, you don’t have to import starter packages solely for their side effects.
package main
import (
"hidevops.io/hiboot/pkg/app/web"
"hidevops.io/hiboot/pkg/app"
"hidevops.io/hiboot/pkg/starter/actuator"
"hidevops.io/hiboot/pkg/starter/logging"
)
func main() {
web.NewApplication().
SetProperty(app.ProfilesInclude,
actuator.Profile,
logging.Profile).
Run()
}
Controller - controller/user.go
Now lets see how does the Hiboot controller works, the controller userController
is embedded a struct at.RestController
, it tells Hiboot that this is a web controller。
newUserController
is the constructor of struct userController
, the dependency userService service.UserService
is injecting through the argument of the constructor during the initialization of newUserController
。
For the simplicity purpose, Hiboot does not need to config route, the method name is the route.
Here is the list of the HTTP methods,
Method | Description | Options | Example |
---|---|---|---|
Get | GET | Get or GetById which in camel case | func (c *userController) GetById(id unit64) |
Post | POST | Post PostUser which in camel case | func (c *userController) Post(request *userRequest) |
Put | PUT | Put or PutUser which in camel case | func (c *userController) Post(request *userRequest) |
Delete | DELETE | Delete DeleteById which in camel case | func (c *userController) DeleteById(id unit64) |
⚠️ Note that the constructor newUserController of the controller must be registered in the func init()
.
package controller
import (
"hidevops.io/hiboot-data/examples/gorm/entity"
"hidevops.io/hiboot-data/examples/gorm/service"
"hidevops.io/hiboot/pkg/app"
"hidevops.io/hiboot/pkg/app/web"
"hidevops.io/hiboot/pkg/model"
"net/http"
)
// RestController
type userController struct {
at.RestController
userService service.UserService
}
func init() {
app.Register(newUserController)
}
// newUserController inject userService automatically
func newUserController(userService service.UserService) *userController {
return &userController{
userService: userService,
}
}
// Post POST /user
func (c *userController) Post(request *entity.User) (model.Response, error) {
err := c.userService.AddUser(request)
response := new(model.BaseResponse)
response.SetData(request)
return response, err
}
// GetById GET /id/{id}
func (c *userController) GetById(id uint64) (response model.Response, err error) {
user, err := c.userService.GetUser(id)
response = new(model.BaseResponse)
if err != nil {
response.SetCode(http.StatusNotFound)
} else {
response.SetData(user)
}
return
}
// GetById GET /id/{id}
func (c *userController) GetAll() (response model.Response, err error) {
users, err := c.userService.GetAll()
response = new(model.BaseResponse)
response.SetData(users)
return
}
// DeleteById DELETE /id/{id}
func (c *userController) DeleteById(id uint64) (response model.Response, err error) {
err = c.userService.DeleteUser(id)
response = new(model.BaseResponse)
return
}
entity/user.go
User entity is the business model.
type User struct {
model.RequestBody
Id uint64 `json:"id"`
Name string `json:"name" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age uint `json:"age" validate:"gte=0,lte=130"`
Gender uint `json:"gender" validate:"gte=0,lte=2"`
}
// Here we specify the table name as user instead of users by default.
func (u *User) TableName() string {
return "user"
}
Model - service/user.go
Service implements the business logic. We declared an interface UserService
,which includes method AddUser
, GetUser
, GetAll
, and DeleteUser
, userServiceImpl is the implementation of UserService。
By importing hidevops.io/hiboot-data/starter/gorm
,the instance of repository gorm.Repository
will be injectable, it will be injected to userServiceImpl
through the constructor newUserService.
⚠️ Note that the constructor newUserService must be registered in the func init()
.
package service
import (
"errors"
"hidevops.io/hiboot-data/examples/gorm/entity"
"hidevops.io/hiboot-data/starter/gorm"
"hidevops.io/hiboot/pkg/app"
"hidevops.io/hiboot/pkg/utils/idgen"
)
type UserService interface {
AddUser(user *entity.User) (err error)
GetUser(id uint64) (user *entity.User, err error)
GetAll() (user *[]entity.User, err error)
DeleteUser(id uint64) (err error)
}
type userServiceImpl struct {
// add UserService, it means that the instance of UserServiceImpl can be found by UserService
UserService
repository gorm.Repository
}
func init() {
// register UserServiceImpl
app.Register(newUserService)
}
// will inject BoltRepository that configured in hidevops.io/hiboot/pkg/starter/data/bolt
func newUserService(repository gorm.Repository) UserService {
repository.AutoMigrate(&entity.User{})
return &userServiceImpl{
repository: repository,
}
}
func (s *userServiceImpl) AddUser(user *entity.User) (err error) {
if user == nil {
return errors.New("user is not allowed nil")
}
if user.Id == 0 {
user.Id, _ = idgen.Next()
}
err = s.repository.Create(user).Error()
return
}
func (s *userServiceImpl) GetUser(id uint64) (user *entity.User, err error) {
user = &entity.User{}
err = s.repository.Where("id = ?", id).First(user).Error()
return
}
func (s *userServiceImpl) GetAll() (users *[]entity.User, err error) {
users = &[]entity.User{}
err = s.repository.Find(users).Error()
return
}
func (s *userServiceImpl) DeleteUser(id uint64) (err error) {
err = s.repository.Where("id = ?", id).Delete(entity.User{}).Error()
return
}
Run the sample code
go run main.go
Output:
______ ____________ _____
___ / / /__(_)__ /_______________ /_
__ /_/ /__ /__ __ \ __ \ __ \ __/
_ __ / _ / _ /_/ / /_/ / /_/ / /_ Hiboot Application Framework
/_/ /_/ /_/ /_.___/\____/\____/\__/ https://hidevops.io/hiboot
[INFO] 2018/10/23 23:37 Starting Hiboot web application gorm-demo on localhost with PID 28423
[INFO] 2018/10/23 23:37 Working directory: /Users/johnd/.gvm/pkgsets/go1.10/hidevops/src/hidevops.io/hiboot-data/examples/gorm
[INFO] 2018/10/23 23:37 The following profiles are active: local, [actuator locale logging gorm]
[INFO] 2018/10/23 23:37 Auto configure gorm starter
[INFO] 2018/10/23 23:37 Auto configure locale starter
[INFO] 2018/10/23 23:37 Auto configure logging starter
[INFO] 2018/10/23 23:37 The dependency graph resolved successfully
[INFO] 2018/10/23 23:37 connected to dataSource demo@mysql-local:3306/gorm_demo
[DBUG] 2018/10/23 23:36 GET: /health -> github.com/hidevops io/hiboot-data/vendor/hidevops.io/hiboot/pkg/starter/actuator/controller/healthController.Get() and 2 more
[DBUG] 2018/10/23 23:36 DELETE: /user/id/{id} -> hidevops.io/hiboot-data/examples/gorm/controller/userController.DeleteById() and 2 more
[DBUG] 2018/10/23 23:36 GET: /user/id/{id} -> hidevops.io/hiboot-data/examples/gorm/controller/userController.GetById() and 2 more
[DBUG] 2018/10/23 23:36 GET: /user/all -> hidevops.io/hiboot-data/examples/gorm/controller/userController.GetAll() and 2 more
[DBUG] 2018/10/23 23:36 POST: /user -> hidevops.io/hiboot-data/examples/gorm/controller/userController.Post() and 2 more
Now listening on: http://localhost:8080
Application started. Press CMD+C to shut down.
Make a request
Let’s make a request on the RESTful API GET /user/all
http GET localhost:8080/user/all
HTTP/1.1 200 OK
Content-Length: 307
Content-Type: application/json; charset=UTF-8
Date: Tue, 23 Oct 2018 15:38:41 GMT
Set-Cookie: app.language=; Path=/; Expires=Tue, 23 Oct 2018 17:38:41 GMT; Max-Age=7200; HttpOnly
{
"code": 200,
"data": [
{
"age": 18,
"email": "john.doe@gmail.com",
"gender": 0,
"id": 209536579658580081,
"name": "John Doe",
"password": "poi321",
"username": "johnd"
},
{
"age": 25,
"email": "mike.phil@gmail.com",
"gender": 0,
"id": 209536656246571121,
"name": "Mike Phil",
"password": "iutg039",
"username": "mikep"
}
],
"message": "Success"
}
Unit test
As I always say, Hiboot is built for production ready, we care about the quality so much, here is the realtime code coverage . So how do we do the unit testing?
First, let’s see the unit test for main.go
mian_test.go
As shown below, the unit test for func main,we have go main()
inside the test case TestRunMain
just for the sake of main func testing。
package main
import (
"testing"
"time"
)
func TestRunMain(t *testing.T) {
go main()
time.Sleep(200 * time.Millisecond)
}
Mock up the testing - controller/user_test.go
We want to test the userController
, but the userController depends on userSerivce
which will connect to the real database through gorm repository. In order to do so, we must mock the userService
.
We use Mockery to generate the some of the test code, Mockery is a tool that automatically generates mock implementations of interfaces UserService
.
First, install mockery,
go get github.com/vektra/mockery/.../
Then, generate mocks for the interface
# go to the directory where the UserService is.
cd $GOPATH/src/hidevops.io/hiboot-data/examples/gorm/service
# generate mocks for the interface UserService
mockery -name UserService
After that, you will see mocks/UserService is generated under $GOPATH/src/hidevops.io/hiboot-data/examples/gorm/service.
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import entity "hidevops.io/hiboot-data/examples/gorm/entity"
import mock "github.com/stretchr/testify/mock"
// UserService is an autogenerated mock type for the UserService type
type UserService struct {
mock.Mock
}
// AddUser provides a mock function with given fields: user
func (_m *UserService) AddUser(user *entity.User) error {
ret := _m.Called(user)
var r0 error
if rf, ok := ret.Get(0).(func(*entity.User) error); ok {
r0 = rf(user)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteUser provides a mock function with given fields: id
func (_m *UserService) DeleteUser(id uint64) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(uint64) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetAll provides a mock function with given fields:
func (_m *UserService) GetAll() (*[]entity.User, error) {
ret := _m.Called()
var r0 *[]entity.User
if rf, ok := ret.Get(0).(func() *[]entity.User); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*[]entity.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUser provides a mock function with given fields: id
func (_m *UserService) GetUser(id uint64) (*entity.User, error) {
ret := _m.Called(id)
var r0 *entity.User
if rf, ok := ret.Get(0).(func(uint64) *entity.User); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*entity.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(uint64) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
Writing the unit test cases
Below are the unit test cases, we tested POST, GET, DELETE method to user controller with the mock interface mocks.UserService.
package controller
import (
"hidevops.io/hiboot-data/examples/gorm/entity"
"hidevops.io/hiboot/pkg/app/web"
"hidevops.io/hiboot/pkg/log"
"hidevops.io/hiboot/pkg/utils/idgen"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"hidevops.io/hiboot-data/examples/gorm/service/mocks"
"errors"
)
func init() {
log.SetLevel(log.DebugLevel)
}
func TestCrdRequest(t *testing.T) {
mockUserService := new(mocks.UserService)
userController := newUserController(mockUserService)
testApp := web.RunTestApplication(t, userController)
id, err := idgen.Next()
assert.Equal(t, nil, err)
testUser := &entity.User{
Id: id,
Name: "Bill Gates",
Username: "billg",
Password: "3948tdaD",
Email: "bill.gates@microsoft.com",
Age: 60,
Gender: 1,
}
// first, call mocks.UserService.AddUser
mockUserService.On("AddUser", testUser).Return(nil)
// then run the test that will call UserService.AddUser
t.Run("should add user with POST request", func(t *testing.T) {
// First, let's Post User
testApp.Post("/user").
WithJSON(testUser).
Expect().Status(http.StatusOK)
})
mockUserService.On("GetUser", id).Return(testUser, nil)
t.Run("should get user with GET request", func(t *testing.T) {
// Then Get User
// e.g. GET /user/id/123456
testApp.Get("/user/id/{id}").
WithPath("id", id).
Expect().Status(http.StatusOK)
})
mockUserService.On("GetAll").Return(&[]entity.User{*testUser}, nil)
t.Run("should get user with GET request", func(t *testing.T) {
// Then Get User
// e.g. GET /user/id/123456
testApp.Get("/user/all").
Expect().Status(http.StatusOK)
})
// assert that the expectations were met
mockUserService.AssertExpectations(t)
unknownId, err := idgen.Next()
assert.Equal(t, nil, err)
mockUserService.On("GetUser", unknownId).Return((*entity.User)(nil), errors.New("not found"))
t.Run("should return 404 if trying to find a record that does not exist", func(t *testing.T) {
// Then Get User
testApp.Get("/user/id/{id}").
WithPath("id", unknownId).
Expect().Status(http.StatusNotFound)
})
// assert that the expectations were met
mockUserService.AssertExpectations(t)
mockUserService.On("DeleteUser", id).Return(nil)
t.Run("should delete the record with DELETE request", func(t *testing.T) {
// Finally Delete User
testApp.Delete("/user/id/{id}").
WithPath("id", id).
Expect().Status(http.StatusOK)
})
}