github.com/System-Glitch/goyave/v2@v2.10.3-0.20200819142921-51011e75d504/docs_src/src/guide/advanced/testing.md (about) 1 --- 2 meta: 3 - name: "og:title" 4 content: "Testing - Goyave" 5 - name: "twitter:title" 6 content: "Testing - Goyave" 7 - name: "title" 8 content: "Testing - Goyave" 9 --- 10 11 # Testing <Badge text="Since v2.2.0"/> 12 13 [[toc]] 14 15 ## Introduction 16 17 Goyave provides an API to ease the unit and functional testing of your application. This API is an extension of [testify](https://github.com/stretchr/testify). `goyave.TestSuite` inherits from testify's `suite.Suite`, and sets up the environment for you. That means: 18 19 - `GOYAVE_ENV` environment variable is set to `test` and restored to its original value when the suite is done. 20 - All tests are run using your project's root as working directory. This directory is determined by the presence of a `go.mod` file. 21 - Config and language files are loaded before the tests start. As the environment is set to `test`, you **need** a `config.test.json` in the root directory of your project. 22 23 This setup is done by the function `goyave.RunTest`, so you shouldn't run your test suites using testify's `suite.Run()` function. 24 25 The following example is a **functional** test and would be located in the `test` package. 26 27 ``` go 28 import ( 29 "my-project/http/route" 30 "github.com/System-Glitch/goyave/v2" 31 ) 32 33 type CustomTestSuite struct { 34 goyave.TestSuite 35 } 36 37 func (suite *CustomTestSuite) TestHello() { 38 suite.RunServer(route.Register, func() { 39 resp, err := suite.Get("/hello", nil) 40 suite.Nil(err) 41 suite.NotNil(resp) 42 if resp != nil { 43 defer resp.Body.Close() 44 suite.Equal(200, resp.StatusCode) 45 suite.Equal("Hi!", string(suite.GetBody(resp))) 46 } 47 }) 48 } 49 50 func TestCustomSuite(t *testing.T) { 51 goyave.RunTest(t, new(CustomTestSuite)) 52 } 53 ``` 54 55 We will explain in more details what this test does in the following sections, but in short, this test runs the server, registers all your application routes and executes the second parameter as a server startup hook. The test requests the `/hello` route with the method `GET` and checks the content of the response. The server automatically shuts down after the hook is executed and before `RunServer` returns. See the available assertions in the [testify's documentation](https://pkg.go.dev/github.com/stretchr/testify). 56 57 This test is a **functional** test. Therefore, it requires route registration and should be located in the `test` package. 58 59 ::: warning 60 Because tests using `goyave.TestSuite` are using the global config, are changing environment variables and working directory and often bind a port, they are **not run in parallel** to avoid conflicts. You don't have to use `-p 1` in your test command, test suites execution is locked by a mutex. 61 ::: 62 63 ## HTTP Tests 64 65 As shown in the example in the introduction, you can easily run a test server and send requests to it using the `suite.RunServer()` function. 66 67 This function takes two parameters. 68 - The first is a route registrer function. You should always use your main route registrer to avoid unexpected problems with inherited middleware and route groups. 69 - The second parameter is a startup hook for the server, in which you will be running your test procedure. 70 71 This function is the equivalent of `goyave.Start`, but doesn't require a `goyave.Stop()` call: the server stops automatically when the startup hook is finished. All startup hooks are then cleared. The function returns when the server is properly shut down. 72 73 If you registered startup hooks in your main function, these won't be executed. If you need them for your tests, you need to register them before calling `suite.RunServer()`. 74 75 ### Request and response 76 77 There are helper functions for the following HTTP methods: 78 - GET 79 - POST 80 - PUT 81 - PATCH 82 - DELETE 83 84 | Parameters | Return | 85 |-----------------------------|------------------| 86 | `route string` | `*http.Response` | 87 | `headers map[string]string` | | 88 | `body io.Reader` | | 89 90 *Note*: The `Get` function doesn't have a third parameter as GET requests shouldn't have a body. The headers and body are optional, and can be `nil`. 91 92 The response body can be retrieved easily using [`suite.GetBody(response)`](#suite-getbody). 93 94 ``` go 95 resp, err := suite.Get("/get", nil) 96 suite.Nil(err) 97 if err == nil { 98 defer resp.Body.Close() 99 suite.Equal("response content", string(suite.GetBody(resp))) 100 } 101 ``` 102 103 #### URL-encoded requests 104 105 ``` go 106 headers := map[string]string{"Content-Type": "application/x-www-form-urlencoded; param=value"} 107 resp, err := suite.Post("/product", headers, strings.NewReader("field=value")) 108 suite.Nil(err) 109 if err == nil { 110 defer resp.Body.Close() 111 suite.Equal("response content", string(suite.GetBody(resp))) 112 } 113 ``` 114 115 #### JSON requests 116 117 ``` go 118 headers := map[string]string{"Content-Type": "application/json"} 119 body, _ := json.Marshal(map[string]interface{}{"name": "Pizza", "price": 12.5}) 120 resp, err := suite.Post("/product", headers, bytes.NewReader(body)) 121 suite.Nil(err) 122 if err == nil { 123 defer resp.Body.Close() 124 suite.Equal("response content", string(suite.GetBody(resp))) 125 } 126 ``` 127 128 :::tip 129 If you need to test another method, you can use the [`suite.Request()`](#testsuite-request) function. 130 ::: 131 132 ### Timeout 133 134 `goyave.TestSuite` has a default timeout value of **5 seconds**. This timeout is used for the `RunServer` function as well as for the request functions(`Get`, `Post`, etc.). If the timeout expires, the test fails. This prevents your test from freezing if something goes wrong. 135 136 The timeout can be modified as needed using `suite.SetTimeout()`: 137 ``` go 138 suite.SetTimeout(10 * time.Second) 139 ``` 140 141 ### Testing JSON reponses 142 143 It is very likely that you will need to check the content of a JSON response when testing your application. Instead of unmarshaling the JSON yourself, Goyave provides the [`suite.GetJSONBody`](#suite-getjsonbody) function. This function decodes the raw body of the request. If the data cannot be decoded, or is invalid JSON, the test fails and the function returns `nil`. 144 145 ``` go 146 suite.RunServer(route.Register, func() { 147 resp, err := suite.Get("/product", nil) 148 suite.Nil(err) 149 if err == nil { 150 defer resp.Body.Close() 151 json := map[string]interface{}{} 152 err := suite.GetJSONBody(resp, &json) 153 suite.Nil(err) 154 if err == nil { // You should always check parsing error before continuing. 155 suite.Equal("value", json["field"]) 156 suite.Equal(float64(42), json["number"]) 157 } 158 } 159 }) 160 ``` 161 162 ### Multipart and file upload 163 164 You may need to test requests requiring file uploads. The best way to do this is using Go's `multipart.Writer`. Goyave provides functions to help you create a Multipart form. 165 166 ``` go 167 suite.RunServer(route.Register, func() { 168 const path = "profile.png" 169 body := &bytes.Buffer{} 170 writer := multipart.NewWriter(body) 171 suite.WriteField(writer, "email", "johndoe@example.org") 172 suite.WriteFile(writer, path, "profile_picture", filepath.Base(path)) 173 if err := writer.Close(); err != nil { 174 panic(err) 175 } 176 177 // Don't forget to set the "Content-Type" header! 178 headers := map[string]string{"Content-Type": writer.FormDataContentType()} 179 180 resp, err := suite.Post("/register", headers, body) 181 suite.Nil(err) 182 if err == nil { 183 defer resp.Body.Close() 184 suite.Equal("Welcome!", string(suite.GetBody(resp))) 185 } 186 }) 187 ``` 188 189 ::: tip 190 You can write a multi-file upload by calling `suite.WriteFile` successively using the same field name. 191 ::: 192 193 ## Testing middleware 194 195 You can unit-test middleware using the [`suite.Middleware`](#suite-middleware) function. This function passes a `*goyave.Request` to your middlware and returns the `*http.Response`. This function also takes a test procedure function as a parameter. This function will simulate a **controller handler**, so you can test if the middleware alters the request. 196 197 ``` go 198 rawRequest := httptest.NewRequest("GET", "/test-route", nil) 199 rawRequest.Header.Set("Content-Type", "application/json") 200 request := suite.CreateTestRequest(rawRequest) 201 request.Data = map[string]interface{}{"text": " \n test \t"} 202 203 result := suite.Middleware(middleware.Trim, request, func(response *Response, request *Request) { 204 suite.Equal("application/json", request.Header().Get("Content-Type")) 205 suite.Equal("test", request.String("text")) 206 }) 207 208 suite.Equal(200, result.StatusCode) 209 ``` 210 211 If you want to test a blocking middleware, flag the test as failed in the test procedure. Indeed, the procedure shouldn't be executed if your middleware doesn't pass to the next handler. 212 213 ``` go 214 request := suite.CreateTestRequest(nil) 215 suite.Middleware(middleware.Auth, request, func(response *Response, request *Request) { 216 suite.Fail("Auth middleware passed") 217 }) 218 ``` 219 220 ## TestSuite reference 221 222 ::: table 223 [RunServer](#testsuite-runserver) 224 [Timeout](#testsuite-timeout) 225 [SetTimeout](#testsuite-settimeout) 226 [Middleware](#testsuite-middleware) 227 [Get](#testsuite-get) 228 [Post](#testsuite-post) 229 [Put](#testsuite-put) 230 [Patch](#testsuite-patch) 231 [Delete](#testsuite-delete) 232 [Request](#testsuite-request) 233 [GetBody](#testsuite-getbody) 234 [GetJSONBody](#testsuite-getjsonbody) 235 [CreateTestFiles](#testsuite-createtestfiles) 236 [CreateTestRequest](#testsuite-createtestrequest) 237 [CreateTestResponse](#testsuite-createtestresponse) 238 [CreateTestResponseWithRequest](#testsuite-createtestresponsewithrequest) 239 [WriteFile](#testsuite-writefile) 240 [WriteField](#testsuite-writefield) 241 [ClearDatabase](#testsuite-cleardatabase) 242 [ClearDatabaseTables](#testsuite-cleardatabasetables) 243 [RunTest](#goyave-runtest) 244 ::: 245 246 #### TestSuite.RunServer 247 248 RunServer start the application and run the given functional test procedure. 249 250 This function is the equivalent of `goyave.Start()`. 251 The test fails if the suite's timeout is exceeded. 252 The server automatically shuts down when the function ends. 253 This function is synchronized, that means that the server is properly stopped when the function returns. 254 255 256 | Parameters | Return | 257 |---------------------------------------|--------| 258 | `routeRegistrer func(*goyave.Router)` | `void` | 259 | `procedure func()` | | 260 261 #### TestSuite.Timeout 262 263 Get the timeout for test failure when using RunServer or requests. 264 265 | Parameters | Return | 266 |------------|-----------------| 267 | | `time.Duration` | 268 269 #### TestSuite.SetTimeout 270 271 Set the timeout for test failure when using RunServer or requests. 272 273 | Parameters | Return | 274 |-----------------|--------| 275 | `time.Duration` | | 276 277 278 #### TestSuite.Middleware 279 280 Executes the given middleware and returns the HTTP response. Core middleware (recovery, parsing and language) is not executed. 281 282 | Parameters | Return | 283 |--------------------------------|------------------| 284 | `middleware goyave.Middleware` | `*http.Response` | 285 | `request *goyave.Request` | | 286 | `procedure goyave.Handler` | | 287 288 #### TestSuite.Get 289 290 Execute a GET request on the given route. Headers are optional. 291 292 | Parameters | Return | 293 |-----------------------------|------------------| 294 | `route string` | `*http.Response` | 295 | `headers map[string]string` | `error` | 296 297 #### TestSuite.Post 298 299 Execute a POST request on the given route. Headers and body are optional. 300 301 | Parameters | Return | 302 |-----------------------------|------------------| 303 | `route string` | `*http.Response` | 304 | `headers map[string]string` | `error` | 305 | `body io.Reader` | | 306 307 #### TestSuite.Put 308 309 Execute a PUT request on the given route. Headers and body are optional. 310 311 | Parameters | Return | 312 |-----------------------------|------------------| 313 | `route string` | `*http.Response` | 314 | `headers map[string]string` | `error` | 315 | `body io.Reader` | | 316 317 #### TestSuite.Patch 318 319 Execute a PATCH request on the given route. Headers and body are optional. 320 321 | Parameters | Return | 322 |-----------------------------|------------------| 323 | `route string` | `*http.Response` | 324 | `headers map[string]string` | `error` | 325 | `body io.Reader` | | 326 327 #### TestSuite.Delete 328 329 Execute a DELETE request on the given route. Headers and body are optional. 330 331 | Parameters | Return | 332 |-----------------------------|------------------| 333 | `route string` | `*http.Response` | 334 | `headers map[string]string` | `error` | 335 | `body io.Reader` | | 336 337 #### TestSuite.Request 338 339 Execute a request on the given route. Headers and body are optional. 340 341 | Parameters | Return | 342 |-----------------------------|------------------| 343 | `method string` | `*http.Response` | 344 | `route string` | `error` | 345 | `headers map[string]string` | | 346 | `body io.Reader` | | 347 348 #### TestSuite.GetBody 349 350 Read the whole body of a response. If read failed, test fails and return empty byte slice. 351 352 | Parameters | Return | 353 |---------------------------|----------| 354 | `response *http.Response` | `[]byte` | 355 356 #### TestSuite.GetJSONBody 357 358 Read the whole body of a response and decode it as JSON. If read or decode failed, test fails. The `data` parameter should be a pointer. 359 360 | Parameters | Return | 361 |---------------------------|---------| 362 | `response *http.Response` | `error` | 363 | `data interface{}` | | 364 365 #### TestSuite.CreateTestFiles 366 367 Create a slice of `filesystem.File` from the given paths. Files are passed to a temporary http request and parsed as Multipart form, to reproduce the way files are obtained in real scenarios. 368 369 | Parameters | Return | 370 |-------------------|---------------------| 371 | `paths ...string` | `[]filesystem.File` | 372 373 #### TestSuite.CreateTestRequest 374 375 Create a `*goyave.Request` from the given raw request. This function is aimed at making it easier to unit test Requests. 376 377 If passed request is `nil`, a default `GET` request to `/` is used. 378 379 | Parameters | Return | 380 |----------------------------|-------------------| 381 | `rawRequest *http.Request` | `*goyave.Request` | 382 383 **Example:** 384 ``` go 385 rawRequest := httptest.NewRequest("GET", "/test-route", nil) 386 rawRequest.Header.Set("Content-Type", "application/json") 387 request := suite.CreateTestRequest(rawRequest) 388 request.Lang = "en-US" 389 request.Data = map[string]interface{}{"field": "value"} 390 ``` 391 392 #### TestSuite.CreateTestResponse 393 394 Create an empty response with the given response writer. This function is aimed at making it easier to unit test Responses. 395 396 | Parameters | Return | 397 |--------------------------------|--------------------| 398 | `recorder http.ResponseWriter` | `*goyave.Response` | 399 400 **Example:** 401 ``` go 402 writer := httptest.NewRecorder() 403 response := suite.CreateTestResponse(writer) 404 response.Status(http.StatusNoContent) 405 result := writer.Result() 406 fmt.Println(result.StatusCode) // 204 407 ``` 408 409 #### TestSuite.CreateTestResponseWithRequest 410 411 Create an empty response with the given response writer and HTTP request. This function is aimed at making it easier to unit test Responses needing the raw request's information, such as redirects. 412 413 | Parameters | Return | 414 |--------------------------------|--------------------| 415 | `recorder http.ResponseWriter` | `*goyave.Response` | 416 | `rawRequest *http.Request` | | 417 418 **Example:** 419 ``` go 420 writer := httptest.NewRecorder() 421 rawRequest := httptest.NewRequest("POST", "/test-route", strings.NewReader("body")) 422 response := suite.CreateTestResponseWithRequest(writer, rawRequest) 423 response.Status(http.StatusNoContent) 424 result := writer.Result() 425 fmt.Println(result.StatusCode) // 204 426 ``` 427 428 #### TestSuite.WriteFile 429 430 Write a file to the given writer. This function is handy for file upload testing. The test fails if an error occurred. 431 432 | Parameters | Return | 433 |---------------------------|--------| 434 | `write *multipart.Writer` | `void` | 435 | `path string` | | 436 | `fieldName string` | | 437 | `fileName string` | | 438 439 #### TestSuite.WriteField 440 441 Create and write a new multipart form field. The test fails if the field couldn't be written. 442 443 | Parameters | Return | 444 |---------------------------|--------| 445 | `write *multipart.Writer` | `void` | 446 | `fieldName string` | | 447 | `value string` | | 448 449 #### TestSuite.ClearDatabase 450 451 Delete all records in all tables. This function only clears the tables of registered models. 452 453 | Parameters | Return | 454 |------------|--------| 455 | | `void` | 456 457 #### TestSuite.ClearDatabaseTables 458 459 Drop all tables. This function only clears the tables of registered models. 460 461 | Parameters | Return | 462 |------------|--------| 463 | | `void` | 464 465 #### goyave.RunTest 466 467 Run a test suite with prior initialization of a test environment. The GOYAVE_ENV environment variable is automatically set to "test" and restored to its original value at the end of the test run. 468 469 All tests are run using your project's root as working directory. This directory is determined by the presence of a `go.mod` file. 470 471 The function returns true if the test passed. 472 473 | Parameters | Return | 474 |--------------------|--------| 475 | `t *testing.T` | `bool` | 476 | `suite ITestSuite` | | 477 478 ::: tip 479 `ITestSuite` is the interface `TestSuite` is implementing. 480 ::: 481 482 ## Database testing 483 484 You may need to test features interacting with your database. Goyave provides a handy way to generate and save records in your database: **factories**. 485 486 **All registered models records are automatically deleted from the database when each test suite completes.** 487 488 It is a good practice to use a separate database dedicated for testing, named `myapp_test` for example. Don't forget to change the database information in your `config.test.json` file. 489 490 All functions below require the `database`package to be imported. 491 492 ``` go 493 import "github.com/System-Glitch/goyave/v2/database" 494 ``` 495 496 ::: tip 497 You may want to use a clean database for each of your tests. You can clear your database before each test using [`suite.SetupTest()`](https://pkg.go.dev/github.com/stretchr/testify/suite?tab=doc#SetupTestSuite). 498 499 ``` go 500 func (suite *CustomTestSuite) SetupTest() { 501 suite.ClearDatabase() 502 } 503 ``` 504 ::: 505 506 ### Generators 507 508 Factories need a **generator function**. These functions generate a single random record. You can use the faking library of your choice, but in this example we are going to use [`github.com/bxcodec/faker`](https://github.com/bxcodec/faker). 509 510 ```go 511 import "github.com/bxcodec/faker/v3" 512 513 func UserGenerator() interface{} { 514 user := &User{} 515 user.Name = faker.Name() 516 517 faker.SetGenerateUniqueValues(true) 518 user.Email = faker.Email() 519 faker.SetGenerateUniqueValues(false) 520 return user 521 } 522 ``` 523 524 ::: tip 525 - `database.Generator` is an alias for `func() interface{}`. 526 - Generator functions should be declared in the same file as the model it is generating. 527 ::: 528 529 Generators can also create associated records. Associated records should be generated using their respective generators. In the following example, we are generating users for an application allowing users to write blog posts. 530 531 ``` go 532 func UserGenerator() interface{} { 533 user := &User{} 534 // ... Generate users fields ... 535 536 // Generate between 0 and 10 blog posts 537 rand.Seed(time.Now().UnixNano()) 538 user.Posts = database.NewFactory(PostGenerator).Generate(rand.Intn(10)) 539 540 return user 541 } 542 ``` 543 544 ### Using factories 545 546 You can create a factory from any `database.Generator`. 547 548 ``` go 549 factory := database.NewFactory(model.UserGenerator) 550 551 // Generate 5 random users 552 records := factory.Generate(5) 553 554 // Generate and insert 5 random users into the database 555 insertedRecords := factory.Save(5) 556 ``` 557 558 Note that generated records will not have an ID if they are not inserted into the database. 559 560 Associated records created by the generator will also be inserted on `factory.Save`. 561 562 #### Overrides 563 564 It is possible to override some of the generated data if needed, for example if you need to test the behavior of a function with a specific value. All generated structures will be merged with the override. 565 566 ``` go 567 override := &model.User{ 568 Name: "Jérémy", 569 } 570 records := factory.Override(override).Generate(10) 571 // All generated records will have the same name: "Jérémy" 572 ``` 573 574 ::: warning 575 Overrides must be of the **same type** as the generated record. 576 ::: 577 578 #### Factory reference 579 580 ::: table 581 [NewFactory](#database-newfactory) 582 [Override](#factory-override) 583 ::: 584 585 #### database.NewFactory 586 587 Create a new Factory. The given generator function will be used to generate records. 588 589 | Parameters | Return | 590 |--------------------------------|--------------------| 591 | `generator database.Generator` | `database.Factory` | 592 593 #### Factory.Override 594 595 Set an override model for generated records. Values present in the override model will replace the ones in the generated records. This function expects a struct **pointer** as parameter. This function returns the same instance of `Factory` so this method can be chained. 596 597 | Parameters | Return | 598 |------------------------|--------------------| 599 | `override interface{}` | `database.Factory` | 600 601 #### Factory.Generate 602 603 Generate a number of records using the given factory. 604 605 | Parameters | Return | 606 |--------------|-----------------| 607 | `count uint` | `[]interface{}` | 608 609 #### Factory.Save 610 611 Generate a number of records using the given factory and return the inserted records. 612 613 | Parameters | Return | 614 |--------------|-----------------| 615 | `count uint` | `[]interface{}` | 616 617 618 ### Seeders 619 620 Seeders are functions which create a number of random records in the database in order to create a full and realistic test environment. Seeders are written in the `database/seeder` package. 621 622 Each seeder should have its own file. A seeder's responsibilities are limited to a single table or model. For example, the `seeder.User` should only seed the `users` table. Moreover, seeders should have the same name as the model they are using. 623 624 ``` go 625 package seeder 626 627 import ( 628 "my-project/database/model" 629 "github.com/System-Glitch/goyave/v2/database" 630 ) 631 632 func User() { 633 database.NewFactory(model.UserGenerator).Save(10) 634 } 635 ```