github.com/lonnblad/godog@v0.7.14-0.20200306004719-1b0cb3259847/_examples/api/README.md (about)

     1  # An example of API feature
     2  
     3  The following example demonstrates steps how we describe and test our API using **godog**.
     4  
     5  ### Step 1
     6  
     7  Describe our feature. Imagine we need a REST API with **json** format. Lets from the point, that
     8  we need to have a **/version** endpoint, which responds with a version number. We also need to manage
     9  error responses.
    10  
    11  ``` gherkin
    12  # file: version.feature
    13  Feature: get version
    14    In order to know godog version
    15    As an API user
    16    I need to be able to request version
    17  
    18    Scenario: does not allow POST method
    19      When I send "POST" request to "/version"
    20      Then the response code should be 405
    21      And the response should match json:
    22        """
    23        {
    24          "error": "Method not allowed"
    25        }
    26        """
    27  
    28    Scenario: should get version number
    29      When I send "GET" request to "/version"
    30      Then the response code should be 200
    31      And the response should match json:
    32        """
    33        {
    34          "version": "v0.5.3"
    35        }
    36        """
    37  ```
    38  
    39  Save it as **version.feature**.
    40  Now we have described a success case and an error when the request method is not allowed.
    41  
    42  ### Step 2
    43  
    44  Run **godog version.feature**. You should see the following result, which says that all of our
    45  steps are yet undefined and provide us with the snippets to implement them.
    46  
    47  ![Screenshot](https://raw.github.com/cucumber/godog/master/examples/api/screenshots/undefined.png)
    48  
    49  ### Step 3
    50  
    51  Lets copy the snippets to **api_test.go** and modify it for our use case. Since we know that we will
    52  need to store state within steps (a response), we should introduce a structure with some variables.
    53  
    54  ``` go
    55  // file: api_test.go
    56  package main
    57  
    58  import (
    59  	"github.com/cucumber/gherkin-go/v9"
    60  	"github.com/cucumber/godog"
    61  )
    62  
    63  type apiFeature struct {
    64  }
    65  
    66  func (a *apiFeature) iSendrequestTo(method, endpoint string) error {
    67  	return godog.ErrPending
    68  }
    69  
    70  func (a *apiFeature) theResponseCodeShouldBe(code int) error {
    71  	return godog.ErrPending
    72  }
    73  
    74  func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
    75  	return godog.ErrPending
    76  }
    77  
    78  func FeatureContext(s *godog.Suite) {
    79  	api := &apiFeature{}
    80  	s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo)
    81  	s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
    82  	s.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
    83  }
    84  ```
    85  
    86  ### Step 4
    87  
    88  Now we can implemented steps, since we know what behavior we expect:
    89  
    90  ``` go
    91  // file: api_test.go
    92  package main
    93  
    94  import (
    95  	"bytes"
    96  	"encoding/json"
    97  	"fmt"
    98  	"net/http"
    99  	"net/http/httptest"
   100  
   101  	"github.com/cucumber/gherkin-go/v9"
   102  	"github.com/cucumber/godog"
   103  )
   104  
   105  type apiFeature struct {
   106  	resp *httptest.ResponseRecorder
   107  }
   108  
   109  func (a *apiFeature) resetResponse(interface{}) {
   110  	a.resp = httptest.NewRecorder()
   111  }
   112  
   113  func (a *apiFeature) iSendrequestTo(method, endpoint string) (err error) {
   114  	req, err := http.NewRequest(method, endpoint, nil)
   115  	if err != nil {
   116  		return
   117  	}
   118  
   119  	// handle panic
   120  	defer func() {
   121  		switch t := recover().(type) {
   122  		case string:
   123  			err = fmt.Errorf(t)
   124  		case error:
   125  			err = t
   126  		}
   127  	}()
   128  
   129  	switch endpoint {
   130  	case "/version":
   131  		getVersion(a.resp, req)
   132  	default:
   133  		err = fmt.Errorf("unknown endpoint: %s", endpoint)
   134  	}
   135  	return
   136  }
   137  
   138  func (a *apiFeature) theResponseCodeShouldBe(code int) error {
   139  	if code != a.resp.Code {
   140  		return fmt.Errorf("expected response code to be: %d, but actual is: %d", code, a.resp.Code)
   141  	}
   142  	return nil
   143  }
   144  
   145  func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
   146  	var expected, actual []byte
   147  	var data interface{}
   148  	if err = json.Unmarshal([]byte(body.Content), &data); err != nil {
   149  		return
   150  	}
   151  	if expected, err = json.Marshal(data); err != nil {
   152  		return
   153  	}
   154  	actual = a.resp.Body.Bytes()
   155  	if !bytes.Equal(actual, expected) {
   156  		err = fmt.Errorf("expected json, does not match actual: %s", string(actual))
   157  	}
   158  	return
   159  }
   160  
   161  func FeatureContext(s *godog.Suite) {
   162  	api := &apiFeature{}
   163  
   164  	s.BeforeScenario(api.resetResponse)
   165  
   166  	s.Step(`^I send "(GET|POST|PUT|DELETE)" request to "([^"]*)"$`, api.iSendrequestTo)
   167  	s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
   168  	s.Step(`^the response should match json:$`, api.theResponseShouldMatchJSON)
   169  }
   170  ```
   171  
   172  **NOTE:** the `getVersion` handler call on **/version** endpoint. We actually need to implement it now.
   173  If we made some mistakes in step implementations, we will know about it when we run the tests.
   174  
   175  Though, we could also improve our **JSON** comparison function to range through the interfaces and
   176  match their types and values.
   177  
   178  In case if some router is used, you may search the handler based on the endpoint. Current example
   179  uses a standard http package.
   180  
   181  ### Step 5
   182  
   183  Finally, lets implement the **api** server:
   184  
   185  ``` go
   186  // file: api.go
   187  // Example - demonstrates REST API server implementation tests.
   188  package main
   189  
   190  import (
   191  	"encoding/json"
   192  	"fmt"
   193  	"net/http"
   194  
   195  	"github.com/cucumber/godog"
   196  )
   197  
   198  func getVersion(w http.ResponseWriter, r *http.Request) {
   199  	if r.Method != "GET" {
   200  		fail(w, "Method not allowed", http.StatusMethodNotAllowed)
   201  		return
   202  	}
   203  	data := struct {
   204  		Version string `json:"version"`
   205  	}{Version: godog.Version}
   206  
   207  	ok(w, data)
   208  }
   209  
   210  func main() {
   211  	http.HandleFunc("/version", getVersion)
   212  	http.ListenAndServe(":8080", nil)
   213  }
   214  
   215  // fail writes a json response with error msg and status header
   216  func fail(w http.ResponseWriter, msg string, status int) {
   217  	w.Header().Set("Content-Type", "application/json")
   218  
   219  	data := struct {
   220  		Error string `json:"error"`
   221  	}{Error: msg}
   222  
   223  	resp, _ := json.Marshal(data)
   224  	w.WriteHeader(status)
   225  
   226  	fmt.Fprintf(w, string(resp))
   227  }
   228  
   229  // ok writes data to response with 200 status
   230  func ok(w http.ResponseWriter, data interface{}) {
   231  	w.Header().Set("Content-Type", "application/json")
   232  
   233  	if s, ok := data.(string); ok {
   234  		fmt.Fprintf(w, s)
   235  		return
   236  	}
   237  
   238  	resp, err := json.Marshal(data)
   239  	if err != nil {
   240  		w.WriteHeader(http.StatusInternalServerError)
   241  		fail(w, "oops something evil has happened", 500)
   242  		return
   243  	}
   244  
   245  	fmt.Fprintf(w, string(resp))
   246  }
   247  ```
   248  
   249  The implementation details are clearly production ready and the imported **godog** package is only
   250  used to respond with the correct constant version number.
   251  
   252  ### Step 6
   253  
   254  Run our tests to see whether everything is happening as we have expected: `godog version.feature`
   255  
   256  ![Screenshot](https://raw.github.com/cucumber/godog/master/examples/api/screenshots/passed.png)
   257  
   258  ### Conclusions
   259  
   260  Hope you have enjoyed it like I did.
   261  
   262  Any developer (who is the target of our application) can read and remind himself about how API behaves.