api server implementation tests example
Этот коммит содержится в:
родитель
b2e7b20045
коммит
42524a12f8
12 изменённых файлов: 426 добавлений и 0 удалений
|
@ -124,6 +124,11 @@ See [godoc][godoc] for general API details.
|
||||||
See **.travis.yml** for supported **go** versions.
|
See **.travis.yml** for supported **go** versions.
|
||||||
See `godog -h` for general command options.
|
See `godog -h` for general command options.
|
||||||
|
|
||||||
|
See implementation examples:
|
||||||
|
|
||||||
|
- [rest API server](https://github.com/DATA-DOG/godog/tree/master/examples/api) implementation and tests
|
||||||
|
- [ls command](https://github.com/DATA-DOG/godog/tree/master/examples/ls) implementation and tests
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
**Q:** Where can I configure common options globally?
|
**Q:** Where can I configure common options globally?
|
||||||
|
|
257
examples/api/README.md
Обычный файл
257
examples/api/README.md
Обычный файл
|
@ -0,0 +1,257 @@
|
||||||
|
# An example of API feature
|
||||||
|
|
||||||
|
The following example demonstrates steps how we describe and test our API using **godog**.
|
||||||
|
|
||||||
|
### Step 1
|
||||||
|
|
||||||
|
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
|
||||||
|
error responses.
|
||||||
|
|
||||||
|
``` gherkin
|
||||||
|
# file: version.feature
|
||||||
|
Feature: get version
|
||||||
|
In order to know API version
|
||||||
|
As an API user
|
||||||
|
I need to be able to request version
|
||||||
|
|
||||||
|
Scenario: does not allow POST method
|
||||||
|
When I send "POST" request to "/version"
|
||||||
|
Then the response code should be 405
|
||||||
|
And the response should match json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"error": "Method not allowed"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: should get version number
|
||||||
|
When I send "GET" request to "/version"
|
||||||
|
Then the response code should be 200
|
||||||
|
And the response should match json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"version": "v0.2.0"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Save it as **version.feature**.
|
||||||
|
Now we have described a success case and an error when the request method is not allowed.
|
||||||
|
|
||||||
|
### Step 2
|
||||||
|
|
||||||
|
Run **godog version.feature**. 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Step 3
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
``` go
|
||||||
|
// file: api_test.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/DATA-DOG/godog"
|
||||||
|
"github.com/cucumber/gherkin-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiFeature struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) iSendrequestTo(method, endpoint string) error {
|
||||||
|
return godog.ErrPending
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) theResponseCodeShouldBe(code int) error {
|
||||||
|
return godog.ErrPending
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) error {
|
||||||
|
return godog.ErrPending
|
||||||
|
}
|
||||||
|
|
||||||
|
func featureContext(s godog.Suite) {
|
||||||
|
api := &apiFeature{}
|
||||||
|
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo)
|
||||||
|
s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
|
||||||
|
s.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4
|
||||||
|
|
||||||
|
Now we can implemented steps, since we know what behavior we expect:
|
||||||
|
|
||||||
|
``` go
|
||||||
|
// file: api_test.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/godog"
|
||||||
|
"github.com/cucumber/gherkin-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiFeature struct {
|
||||||
|
resp *httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) resetResponse(interface{}) {
|
||||||
|
a.resp = httptest.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "/version":
|
||||||
|
getVersion(a.resp, req)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown endpoint: %s", endpoint)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) theResponseCodeShouldBe(code int) error {
|
||||||
|
if code != a.resp.Code {
|
||||||
|
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, does not match actual: %s", string(actual))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** the `getVersion` handler call on **/version** endpoint. 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.
|
||||||
|
|
||||||
|
In case if some router is used, you may search the handler based on the endpoint. Current example
|
||||||
|
uses a standard http package.
|
||||||
|
|
||||||
|
### Step 5
|
||||||
|
|
||||||
|
Finally, lets implement the **api** server:
|
||||||
|
|
||||||
|
``` go
|
||||||
|
// file: api.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/godog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}{Version: godog.Version}
|
||||||
|
|
||||||
|
ok(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/version", getVersion)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The implementation details are clearly production ready and the imported **godog** package is only
|
||||||
|
used to respond with the correct constant version number.
|
||||||
|
|
||||||
|
### Step 6
|
||||||
|
|
||||||
|
Run our tests to see whether everything is happening as we have expected: `godog version.feature`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Conclusions
|
||||||
|
|
||||||
|
Hope you have enjoyed it like I did.
|
||||||
|
|
||||||
|
Any developer (who is the target of our application) can read and remind himself about how API behaves.
|
60
examples/api/api.go
Обычный файл
60
examples/api/api.go
Обычный файл
|
@ -0,0 +1,60 @@
|
||||||
|
// Example - demonstrates REST API server implementation tests.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/godog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
fail(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}{Version: godog.Version}
|
||||||
|
|
||||||
|
ok(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/version", getVersion)
|
||||||
|
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))
|
||||||
|
}
|
77
examples/api/api_test.go
Обычный файл
77
examples/api/api_test.go
Обычный файл
|
@ -0,0 +1,77 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/godog"
|
||||||
|
"github.com/cucumber/gherkin-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiFeature struct {
|
||||||
|
resp *httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) resetResponse(interface{}) {
|
||||||
|
a.resp = httptest.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "/version":
|
||||||
|
getVersion(a.resp, req)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown endpoint: %s", endpoint)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiFeature) theResponseCodeShouldBe(code int) error {
|
||||||
|
if code != a.resp.Code {
|
||||||
|
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, does not match actual: %s", string(actual))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
Двоичные данные
examples/api/screenshots/passed.png
Обычный файл
Двоичные данные
examples/api/screenshots/passed.png
Обычный файл
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 124 КиБ |
Двоичные данные
examples/api/screenshots/undefined.png
Обычный файл
Двоичные данные
examples/api/screenshots/undefined.png
Обычный файл
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 151 КиБ |
25
examples/api/version.feature
Обычный файл
25
examples/api/version.feature
Обычный файл
|
@ -0,0 +1,25 @@
|
||||||
|
# file: version.feature
|
||||||
|
Feature: get version
|
||||||
|
In order to know godog version
|
||||||
|
As an API user
|
||||||
|
I need to be able to request version
|
||||||
|
|
||||||
|
Scenario: does not allow POST method
|
||||||
|
When I send "POST" request to "/version"
|
||||||
|
Then the response code should be 405
|
||||||
|
And the response should match json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"error": "Method not allowed"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: should get version number
|
||||||
|
When I send "GET" request to "/version"
|
||||||
|
Then the response code should be 200
|
||||||
|
And the response should match json:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"version": "v0.2.0"
|
||||||
|
}
|
||||||
|
"""
|
1
examples/doc.go
Обычный файл
1
examples/doc.go
Обычный файл
|
@ -0,0 +1 @@
|
||||||
|
package examples
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Example - demonstrates ls command implementation tests.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
Загрузка…
Создание таблицы
Сослаться в новой задаче