github.com/agilebits/godog@v0.7.9/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/DATA-DOG/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/DATA-DOG/godog" 60 "github.com/DATA-DOG/godog/gherkin" 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 *gherkin.DocString) 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/DATA-DOG/godog" 102 "github.com/DATA-DOG/godog/gherkin" 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 *gherkin.DocString) (err 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/DATA-DOG/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/DATA-DOG/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.