add a database interaction example

Этот коммит содержится в:
gedi 2015-07-02 16:18:04 +03:00
родитель d25440860b
коммит e99804d43f
8 изменённых файлов: 458 добавлений и 3 удалений

Просмотреть файл

@ -1,14 +1,15 @@
.PHONY: test
.PHONY: test deps
# runs all necessary tests
test:
@echo "running all tests"
@sh -c 'if [ ! -z "$(go fmt ./...)" ]; then exit 1; else echo "go fmt OK"; fi'
@sh -c 'if [ ! -z "$(golint ./...)" ]; then exit 1; else echo "golint OK"; fi'
go vet ./...
go test ./...
go run cmd/godog/main.go -f progress
# updates dependencies
deps:
@echo "updating all dependencies"
go get -u github.com/cucumber/gherkin-go
go get -u github.com/shiena/ansicolor

8
examples/db/Makefile Обычный файл
Просмотреть файл

@ -0,0 +1,8 @@
.PHONY: test
test:
mysql -u root -e 'DROP DATABASE IF EXISTS `godog_test`'
mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `godog_test`'
mysql -u root godog_test < db.sql
godog users.feature

17
examples/db/README.md Обычный файл
Просмотреть файл

@ -0,0 +1,17 @@
# An example of API with DB
The following example demonstrates steps how we describe and test our API with DB using **godog**.
To start with, see [API example](https://github.com/DATA-DOG/godog/tree/master/examples/api) before.
We have extended it to be used with database.
The interesting point is, that we have **txdb.go** which has an implementation of custom sql.driver
to allow execute every and each scenario within a **transaction**. After it completes, transaction
is rolled back so the state could be clean for the next scenario.
To run **users.feature** you need MySQL installed on your system with an anonymous root password.
Then run:
make test
The json comparisom function should be improved and we should also have placeholders for primary
keys when comparing a json result.

95
examples/db/api.go Обычный файл
Просмотреть файл

@ -0,0 +1,95 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
type server struct {
db *sql.DB
}
type User struct {
ID int64 `json:"-"`
Username string `json:"username"`
Email string `json:"-"`
}
func (s *server) users(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
users := make([]*User, 0)
rows, err := s.db.Query("SELECT id, email, username FROM users")
defer rows.Close()
switch err {
case sql.ErrNoRows:
case nil:
for rows.Next() {
user := &User{}
if err := rows.Scan(&user.ID, &user.Email, &user.Username); err != nil {
fail(w, fmt.Sprintf("failed to scan an user: %s", err), http.StatusInternalServerError)
return
}
users = append(users, user)
}
default:
fail(w, fmt.Sprintf("failed to fetch users: %s", err), http.StatusInternalServerError)
return
}
data := struct {
Users []*User `json:"users"`
}{Users: users}
ok(w, data)
}
func main() {
db, err := sql.Open("mysql", "root@/godog")
if err != nil {
panic(err)
}
s := &server{db: db}
http.HandleFunc("/users", s.users)
http.ListenAndServe(":8080", nil)
}
// fail writes a json response with error msg and status header
func fail(w http.ResponseWriter, msg string, status int) {
w.Header().Set("Content-Type", "application/json")
data := struct {
Error string `json:"error"`
}{Error: msg}
resp, _ := json.Marshal(data)
w.WriteHeader(status)
fmt.Fprintf(w, string(resp))
}
// ok writes data to response with 200 status
func ok(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
if s, ok := data.(string); ok {
fmt.Fprintf(w, s)
return
}
resp, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fail(w, "oops something evil has happened", 500)
return
}
fmt.Fprintf(w, string(resp))
}

129
examples/db/api_test.go Обычный файл
Просмотреть файл

@ -0,0 +1,129 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/DATA-DOG/godog"
"github.com/cucumber/gherkin-go"
)
func init() {
// we register an sql driver txdb
Register("mysql", "root@/godog_test")
}
type apiFeature struct {
server
resp *httptest.ResponseRecorder
}
func (a *apiFeature) resetResponse(interface{}) {
a.resp = httptest.NewRecorder()
if a.db != nil {
a.db.Close()
}
db, err := sql.Open("txdb", "")
if err != nil {
panic(err)
}
a.db = db
}
func (a *apiFeature) iSendrequestTo(method, endpoint string) (err error) {
req, err := http.NewRequest(method, endpoint, nil)
if err != nil {
return
}
// handle panic
defer func() {
switch t := recover().(type) {
case string:
err = fmt.Errorf(t)
case error:
err = t
}
}()
switch endpoint {
case "/users":
a.users(a.resp, req)
default:
err = fmt.Errorf("unknown endpoint: %s", endpoint)
}
return
}
func (a *apiFeature) theResponseCodeShouldBe(code int) error {
if code != a.resp.Code {
if a.resp.Code >= 400 {
return fmt.Errorf("expected response code to be: %d, but actual is: %d, response message: %s", code, a.resp.Code, string(a.resp.Body.Bytes()))
}
return fmt.Errorf("expected response code to be: %d, but actual is: %d", code, a.resp.Code)
}
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err error) {
var expected, actual []byte
var data interface{}
if err = json.Unmarshal([]byte(body.Content), &data); err != nil {
return
}
if expected, err = json.Marshal(data); err != nil {
return
}
actual = a.resp.Body.Bytes()
if string(actual) != string(expected) {
err = fmt.Errorf("expected json %s, does not match actual: %s", string(expected), string(actual))
}
return
}
func (a *apiFeature) thereAreUsers(users *gherkin.DataTable) error {
var fields []string
var marks []string
head := users.Rows[0].Cells
for _, cell := range head {
fields = append(fields, cell.Value)
marks = append(marks, "?")
}
stmt, err := a.db.Prepare("INSERT INTO users (" + strings.Join(fields, ", ") + ") VALUES(" + strings.Join(marks, ", ") + ")")
if err != nil {
return err
}
for i := 1; i < len(users.Rows); i++ {
var vals []interface{}
for n, cell := range users.Rows[i].Cells {
switch head[n].Value {
case "username":
vals = append(vals, cell.Value)
case "email":
vals = append(vals, cell.Value)
default:
return fmt.Errorf("unexpected column name: %s", head[n].Value)
}
}
if _, err = stmt.Exec(vals...); err != nil {
return err
}
}
return nil
}
func featureContext(s godog.Suite) {
api := &apiFeature{}
s.BeforeScenario(api.resetResponse)
s.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
s.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
s.Step(`^there are users:$`, api.thereAreUsers)
}

7
examples/db/db.sql Обычный файл
Просмотреть файл

@ -0,0 +1,7 @@
CREATE TABLE users (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
`username` VARCHAR(32) NOT NULL,
`email` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `uniq_email` (`email`)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

146
examples/db/txdb.go Обычный файл
Просмотреть файл

@ -0,0 +1,146 @@
package main
import (
"database/sql"
"database/sql/driver"
"io"
"sync"
)
// Register a txdb sql driver which can be used to open
// a single transaction based database connection pool
func Register(drv, dsn string) {
sql.Register("txdb", &txDriver{dsn: dsn, drv: drv})
}
// txDriver is an sql driver which runs on single transaction
// when the Close is called, transaction is rolled back
type txDriver struct {
sync.Mutex
tx *sql.Tx
drv string
dsn string
db *sql.DB
}
func (d *txDriver) Open(dsn string) (driver.Conn, error) {
// first open a real database connection
var err error
if d.db == nil {
db, err := sql.Open(d.drv, d.dsn)
if err != nil {
return d, err
}
d.db = db
}
if d.tx == nil {
d.tx, err = d.db.Begin()
}
return d, err
}
func (d *txDriver) Close() error {
err := d.tx.Rollback()
d.tx = nil
return err
}
func (d *txDriver) Begin() (driver.Tx, error) {
return d, nil
}
func (d *txDriver) Commit() error {
return nil
}
func (d *txDriver) Rollback() error {
return nil
}
func (d *txDriver) Prepare(query string) (driver.Stmt, error) {
return &stmt{drv: d, query: query}, nil
}
type stmt struct {
query string
drv *txDriver
}
func (s *stmt) Exec(args []driver.Value) (driver.Result, error) {
s.drv.Lock()
defer s.drv.Unlock()
st, err := s.drv.tx.Prepare(s.query)
if err != nil {
return nil, err
}
defer st.Close()
var iargs []interface{}
for _, arg := range args {
iargs = append(iargs, arg)
}
return st.Exec(iargs...)
}
func (s *stmt) NumInput() int {
return -1
}
func (s *stmt) Close() error {
return nil
}
func (s *stmt) Query(args []driver.Value) (driver.Rows, error) {
s.drv.Lock()
defer s.drv.Unlock()
st, err := s.drv.tx.Prepare(s.query)
if err != nil {
return nil, err
}
// do not close the statement here, Rows need it
var iargs []interface{}
for _, arg := range args {
iargs = append(iargs, arg)
}
rs, err := st.Query(iargs...)
return &rows{rs: rs}, err
}
type rows struct {
err error
rs *sql.Rows
}
func (r *rows) Columns() (cols []string) {
cols, r.err = r.rs.Columns()
return
}
func (r *rows) Next(dest []driver.Value) error {
if r.err != nil {
return r.err
}
if r.rs.Err() != nil {
return r.rs.Err()
}
if !r.rs.Next() {
return io.EOF
}
values := make([]interface{}, len(dest))
for i := range values {
values[i] = new(interface{})
}
if err := r.rs.Scan(values...); err != nil {
return err
}
for i, val := range values {
dest[i] = *(val.(*interface{}))
}
return r.rs.Err()
}
func (r *rows) Close() error {
return r.rs.Close()
}

52
examples/db/users.feature Обычный файл
Просмотреть файл

@ -0,0 +1,52 @@
Feature: users
In order to use users api
As an API user
I need to be able to manage users
Scenario: should get empty users
When I send "GET" request to "/users"
Then the response code should be 200
And the response should match json:
"""
{
"users": []
}
"""
Scenario: should get users
Given there are users:
| username | email |
| john | john.doe@mail.com |
| jane | jane.doe@mail.com |
When I send "GET" request to "/users"
Then the response code should be 200
And the response should match json:
"""
{
"users": [
{
"username": "john"
},
{
"username": "jane"
}
]
}
"""
Scenario: should get users when there is only one
Given there are users:
| username | email |
| gopher | gopher@mail.com |
When I send "GET" request to "/users"
Then the response code should be 200
And the response should match json:
"""
{
"users": [
{
"username": "gopher"
}
]
}
"""