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  ```