fix(examples): update api example (#532)
Этот коммит содержится в:
родитель
3e0f9026f3
коммит
75d5cab90d
5 изменённых файлов: 104 добавлений и 69 удалений
|
@ -4,8 +4,8 @@ The following example demonstrates steps how we describe and test our API using
|
||||||
|
|
||||||
### Step 1
|
### Step 1
|
||||||
|
|
||||||
Describe our feature. Imagine we need a REST API with **json** format. Lets from the point, that
|
Describe our feature. Imagine we need a REST API with `json` format. Lets from the point, that
|
||||||
we need to have a **/version** endpoint, which responds with a version number. We also need to manage
|
we need to have a `/version` endpoint, which responds with a version number. We also need to manage
|
||||||
error responses.
|
error responses.
|
||||||
|
|
||||||
``` gherkin
|
``` gherkin
|
||||||
|
@ -31,24 +31,24 @@ Feature: get version
|
||||||
And the response should match json:
|
And the response should match json:
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"version": "v0.5.3"
|
"version": "v0.0.0-dev"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
Save it as **version.feature**.
|
Save it as `features/version.feature`.
|
||||||
Now we have described a success case and an error when the request method is not allowed.
|
Now we have described a success case and an error when the request method is not allowed.
|
||||||
|
|
||||||
### Step 2
|
### Step 2
|
||||||
|
|
||||||
Run **godog version.feature**. You should see the following result, which says that all of our
|
Execute `godog run`. You should see the following result, which says that all of our
|
||||||
steps are yet undefined and provide us with the snippets to implement them.
|
steps are yet undefined and provide us with the snippets to implement them.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Step 3
|
### Step 3
|
||||||
|
|
||||||
Lets copy the snippets to **api_test.go** and modify it for our use case. Since we know that we will
|
Lets copy the snippets to `api_test.go` and modify it for our use case. Since we know that we will
|
||||||
need to store state within steps (a response), we should introduce a structure with some variables.
|
need to store state within steps (a response), we should introduce a structure with some variables.
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
|
@ -74,6 +74,21 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error {
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFeatures(t *testing.T) {
|
||||||
|
suite := godog.TestSuite{
|
||||||
|
ScenarioInitializer: InitializeScenario,
|
||||||
|
Options: &godog.Options{
|
||||||
|
Format: "pretty",
|
||||||
|
Paths: []string{"features"},
|
||||||
|
TestingT: t, // Testing instance that will run subtests.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if suite.Run() != 0 {
|
||||||
|
t.Fatal("non-zero status returned, failed to run feature tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func InitializeScenario(s *godog.ScenarioContext) {
|
func InitializeScenario(s *godog.ScenarioContext) {
|
||||||
api := &apiFeature{}
|
api := &apiFeature{}
|
||||||
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo)
|
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo)
|
||||||
|
@ -84,7 +99,7 @@ func InitializeScenario(s *godog.ScenarioContext) {
|
||||||
|
|
||||||
### Step 4
|
### Step 4
|
||||||
|
|
||||||
Now we can implemented steps, since we know what behavior we expect:
|
Now we can implement steps, since we know what behavior we expect:
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
// file: api_test.go
|
// file: api_test.go
|
||||||
|
@ -92,11 +107,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
)
|
)
|
||||||
|
@ -105,7 +121,7 @@ type apiFeature struct {
|
||||||
resp *httptest.ResponseRecorder
|
resp *httptest.ResponseRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiFeature) resetResponse(interface{}) {
|
func (a *apiFeature) resetResponse(*godog.Scenario) {
|
||||||
a.resp = httptest.NewRecorder()
|
a.resp = httptest.NewRecorder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,40 +157,59 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error {
|
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) {
|
||||||
var expected, actual []byte
|
var expected, actual interface{}
|
||||||
var data interface{}
|
|
||||||
if err = json.Unmarshal([]byte(body.Content), &data); err != nil {
|
// re-encode expected response
|
||||||
return
|
if err = json.Unmarshal([]byte(body.Content), &expected); err != nil {
|
||||||
}
|
|
||||||
if expected, err = json.Marshal(data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
actual = a.resp.Body.Bytes()
|
|
||||||
if !bytes.Equal(actual, expected) {
|
|
||||||
err = fmt.Errorf("expected json, does not match actual: %s", string(actual))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeScenario(s *godog.ScenarioContext) {
|
// re-encode actual response too
|
||||||
|
if err = json.Unmarshal(a.resp.Body.Bytes(), &actual); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// the matching may be adapted per different requirements.
|
||||||
|
if !reflect.DeepEqual(expected, actual) {
|
||||||
|
return fmt.Errorf("expected JSON does not match actual, %v vs. %v", expected, actual)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeatures(t *testing.T) {
|
||||||
|
suite := godog.TestSuite{
|
||||||
|
ScenarioInitializer: InitializeScenario,
|
||||||
|
Options: &godog.Options{
|
||||||
|
Format: "pretty",
|
||||||
|
Paths: []string{"features"},
|
||||||
|
TestingT: t, // Testing instance that will run subtests.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if suite.Run() != 0 {
|
||||||
|
t.Fatal("non-zero status returned, failed to run feature tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||||
api := &apiFeature{}
|
api := &apiFeature{}
|
||||||
|
|
||||||
ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
|
ctx.Before(func(ctx context.Context, sc *godog.Scenario) (context.Context, error) {
|
||||||
api.resetResponse(sc)
|
api.resetResponse(sc)
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
})
|
})
|
||||||
|
ctx.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
|
||||||
s.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
|
ctx.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
|
||||||
s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
|
ctx.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
|
||||||
s.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE:** the `getVersion` handler call on **/version** endpoint. We actually need to implement it now.
|
**NOTE:** the `getVersion` handler is called on `/version` endpoint.
|
||||||
|
Executing `godog run` or `go test -v` will provide `undefined: getVersion` error, so we actually need to implement it now.
|
||||||
If we made some mistakes in step implementations, we will know about it when we run the tests.
|
If we made some mistakes in step implementations, we will know about it when we run the tests.
|
||||||
|
|
||||||
Though, we could also improve our **JSON** comparison function to range through the interfaces and
|
Though, we could also improve our `JSON` comparison function to range through the interfaces and
|
||||||
match their types and values.
|
match their types and values.
|
||||||
|
|
||||||
In case if some router is used, you may search the handler based on the endpoint. Current example
|
In case if some router is used, you may search the handler based on the endpoint. Current example
|
||||||
|
@ -182,7 +217,7 @@ uses a standard http package.
|
||||||
|
|
||||||
### Step 5
|
### Step 5
|
||||||
|
|
||||||
Finally, lets implement the **api** server:
|
Finally, lets implement the `API` server:
|
||||||
|
|
||||||
``` go
|
``` go
|
||||||
// file: api.go
|
// file: api.go
|
||||||
|
@ -191,17 +226,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getVersion(w http.ResponseWriter, r *http.Request) {
|
func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}{Version: godog.Version}
|
}{Version: godog.Version}
|
||||||
|
@ -209,42 +244,34 @@ func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
ok(w, data)
|
ok(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
|
||||||
http.HandleFunc("/version", getVersion)
|
|
||||||
http.ListenAndServe(":8080", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail writes a json response with error msg and status header
|
// fail writes a json response with error msg and status header
|
||||||
func fail(w http.ResponseWriter, msg string, status int) {
|
func fail(w http.ResponseWriter, msg string, status int) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.WriteHeader(status)
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}{Error: msg}
|
}{Error: msg}
|
||||||
|
|
||||||
resp, _ := json.Marshal(data)
|
resp, _ := json.Marshal(data)
|
||||||
w.WriteHeader(status)
|
|
||||||
|
|
||||||
fmt.Fprintf(w, string(resp))
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok writes data to response with 200 status
|
// ok writes data to response with 200 status
|
||||||
func ok(w http.ResponseWriter, data interface{}) {
|
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)
|
resp, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
fail(w, "Oops something evil has happened", http.StatusInternalServerError)
|
||||||
fail(w, "oops something evil has happened", 500)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, string(resp))
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/version", getVersion)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -253,7 +280,7 @@ used to respond with the correct constant version number.
|
||||||
|
|
||||||
### Step 6
|
### Step 6
|
||||||
|
|
||||||
Run our tests to see whether everything is happening as we have expected: `godog version.feature`
|
Run our tests to see whether everything is happening as we have expected: `go test -v`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getVersion(w http.ResponseWriter, r *http.Request) {
|
func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}{Version: godog.Version}
|
}{Version: godog.Version}
|
||||||
|
@ -21,40 +21,32 @@ func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
ok(w, data)
|
ok(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
|
||||||
http.HandleFunc("/version", getVersion)
|
|
||||||
http.ListenAndServe(":8080", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail writes a json response with error msg and status header
|
// fail writes a json response with error msg and status header
|
||||||
func fail(w http.ResponseWriter, msg string, status int) {
|
func fail(w http.ResponseWriter, msg string, status int) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.WriteHeader(status)
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}{Error: msg}
|
}{Error: msg}
|
||||||
|
|
||||||
resp, _ := json.Marshal(data)
|
resp, _ := json.Marshal(data)
|
||||||
w.WriteHeader(status)
|
|
||||||
|
|
||||||
fmt.Fprintf(w, string(resp))
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ok writes data to response with 200 status
|
// ok writes data to response with 200 status
|
||||||
func ok(w http.ResponseWriter, data interface{}) {
|
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)
|
resp, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
fail(w, "Oops something evil has happened", http.StatusInternalServerError)
|
||||||
fail(w, "oops something evil has happened", 500)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(w, string(resp))
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/version", getVersion)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/cucumber/godog"
|
"github.com/cucumber/godog"
|
||||||
)
|
)
|
||||||
|
@ -71,6 +72,21 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFeatures(t *testing.T) {
|
||||||
|
suite := godog.TestSuite{
|
||||||
|
ScenarioInitializer: InitializeScenario,
|
||||||
|
Options: &godog.Options{
|
||||||
|
Format: "pretty",
|
||||||
|
Paths: []string{"features"},
|
||||||
|
TestingT: t, // Testing instance that will run subtests.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if suite.Run() != 0 {
|
||||||
|
t.Fatal("non-zero status returned, failed to run feature tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func InitializeScenario(ctx *godog.ScenarioContext) {
|
func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||||
api := &apiFeature{}
|
api := &apiFeature{}
|
||||||
|
|
||||||
|
|
Двоичные данные
_examples/api/screenshots/passed.png
Двоичные данные
_examples/api/screenshots/passed.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 75 КиБ После Ширина: | Высота: | Размер: 39 КиБ |
Двоичные данные
_examples/api/screenshots/undefined.png
Двоичные данные
_examples/api/screenshots/undefined.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 99 КиБ После Ширина: | Высота: | Размер: 58 КиБ |
Загрузка…
Создание таблицы
Сослаться в новой задаче