Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 Feature: Add TestConfig to app.Test() for configurable testing #3161

Merged
merged 27 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
00405f7
🔥 Feature: Add thread-safe reading from a closed testConn
grivera64 Oct 1, 2024
71c153b
🔥 Feature: Add TestConfig to app.Test()
grivera64 Oct 2, 2024
073ec97
📚 Doc: Add more details about TestConfig in docs
grivera64 Oct 3, 2024
228392e
Merge branch 'gofiber:main' into feature/add-app-test-config
grivera64 Oct 4, 2024
6c006a0
🩹 Fix: Correct testConn tests
grivera64 Oct 8, 2024
b07e05d
🎨 Style: Respect linter in Add App Test Config
grivera64 Oct 8, 2024
3c4017e
Merge branch 'gofiber:main' into feature/add-app-test-config
grivera64 Oct 8, 2024
6112c04
Merge branch 'gofiber:main' into feature/add-app-test-config
grivera64 Oct 9, 2024
4a97d7b
🎨 Styles: Update app.go to respect linter
grivera64 Oct 9, 2024
3639a78
Merge branch 'main' into feature/add-app-test-config
grivera64 Oct 12, 2024
4a2a77d
Merge branch 'main' into feature/add-app-test-config
grivera64 Oct 16, 2024
3776184
♻️ Refactor: Rename TestConfig's ErrOnTimeout to FailOnTimeout
grivera64 Oct 23, 2024
78e32a7
🩹 Fix: Fix typo in TestConfig struct comment in app.go
grivera64 Oct 23, 2024
20bac97
Merge branch 'main' into feature/add-app-test-config
grivera64 Nov 12, 2024
ca3efd2
♻️ Refactor: Change app.Test() fail on timeouterror to os.ErrDeadline…
grivera64 Nov 12, 2024
1220df7
♻️ Refactor:Update middleware that use the same TestConfig to use a g…
grivera64 Nov 12, 2024
498df60
🩹 Fix: Update error from FailOnTimeout to os.ErrDeadlineExceeded in t…
grivera64 Nov 12, 2024
438b630
🩹 Fix: Remove errors import from middlware/proxy/proxy_test.go
grivera64 Nov 12, 2024
0326c07
📚 Doc: Add `app.Test()` config changes to docs/whats_new.md
grivera64 Nov 13, 2024
6b6674f
Merge branch 'main' into feature/add-app-test-config
grivera64 Nov 17, 2024
334dbe8
♻ Refactor: Change app.Test() and all uses to accept 0 as no timeout …
grivera64 Nov 17, 2024
b389aaa
📚 Doc: Add TestConfig option details to docs/whats_new.md
grivera64 Nov 19, 2024
98d2fcc
🎨 Styles: Update docs/whats_new.md to respect markdown-lint
grivera64 Nov 19, 2024
be650d6
Merge branch 'main' into feature/add-app-test-config
grivera64 Nov 19, 2024
250b836
🎨 Styles: Update docs/whats_new.md to use consistent style for TestCo…
grivera64 Nov 19, 2024
7bae815
Merge branch 'main' into feature/add-app-test-config
gaby Nov 20, 2024
b6daa78
Merge branch 'main' into feature/add-app-test-config
grivera64 Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
"encoding/xml"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"os"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -901,13 +903,33 @@ func (app *App) Hooks() *Hooks {
return app.hooks
}

// TestConfig is a struct holding Test settings
type TestConfig struct {
grivera64 marked this conversation as resolved.
Show resolved Hide resolved
// Timeout defines the maximum duration a
// test can run before timing out.
// Default: time.Second
Timeout time.Duration

// FailOnTimeout specifies whether the test
// should return a timeout error if the HTTP response
// exceeds the Timeout duration.
// Default: true
FailOnTimeout bool
}

// Test is used for internal debugging by passing a *http.Request.
// Timeout is optional and defaults to 1s, -1 will disable it completely.
func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Response, error) {
// Set timeout
to := 1 * time.Second
if len(timeout) > 0 {
to = timeout[0]
// Config is optional and defaults to a 1s error on timeout,
// 0 timeout will disable it completely.
func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error) {
// Default config
cfg := TestConfig{
Timeout: time.Second,
FailOnTimeout: true,
}

// Override config if provided
if len(config) > 0 {
cfg = config[0]
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
}

// Add Content-Length if not provided with body
Expand Down Expand Up @@ -946,12 +968,15 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Respons
}()

// Wait for callback
if to >= 0 {
if cfg.Timeout > 0 {
// With timeout
select {
case err = <-channel:
case <-time.After(to):
return nil, fmt.Errorf("test: timeout error after %s", to)
case <-time.After(cfg.Timeout):
conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here
if cfg.FailOnTimeout {
return nil, os.ErrDeadlineExceeded
}
}
} else {
// Without timeout
Expand All @@ -969,6 +994,9 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (*http.Respons
// Convert raw http response to *http.Response
res, err := http.ReadResponse(buffer, req)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, errors.New("test: got empty response")
}
return nil, fmt.Errorf("failed to read response: %w", err)
}

Expand Down
37 changes: 32 additions & 5 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"regexp"
"runtime"
Expand Down Expand Up @@ -1124,7 +1125,9 @@ func Test_Test_Timeout(t *testing.T) {

app.Get("/", testEmptyHandler)

resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), -1)
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{
Timeout: 0,
})
require.NoError(t, err, "app.Test(req)")
require.Equal(t, 200, resp.StatusCode, "Status code")

Expand All @@ -1133,7 +1136,10 @@ func Test_Test_Timeout(t *testing.T) {
return nil
})

_, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), 20*time.Millisecond)
_, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), TestConfig{
Timeout: 20 * time.Millisecond,
FailOnTimeout: true,
})
require.Error(t, err, "app.Test(req)")
}

Expand Down Expand Up @@ -1432,7 +1438,9 @@ func Test_App_Test_no_timeout_infinitely(t *testing.T) {
})

req := httptest.NewRequest(MethodGet, "/", nil)
_, err = app.Test(req, -1)
_, err = app.Test(req, TestConfig{
Timeout: 0,
})
}()

tk := time.NewTimer(5 * time.Second)
Expand Down Expand Up @@ -1460,8 +1468,27 @@ func Test_App_Test_timeout(t *testing.T) {
return nil
})

_, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), 100*time.Millisecond)
require.Equal(t, errors.New("test: timeout error after 100ms"), err)
_, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{
Timeout: 100 * time.Millisecond,
FailOnTimeout: true,
})
require.Equal(t, os.ErrDeadlineExceeded, err)
}

func Test_App_Test_timeout_empty_response(t *testing.T) {
t.Parallel()

app := New()
app.Get("/", func(_ Ctx) error {
time.Sleep(1 * time.Second)
return nil
})

_, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{
Timeout: 100 * time.Millisecond,
FailOnTimeout: false,
})
require.Equal(t, errors.New("test: got empty response"), err)
}

func Test_App_SetTLSHandler(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3243,7 +3243,10 @@ func Test_Static_Compress(t *testing.T) {

req := httptest.NewRequest(MethodGet, "/file", nil)
req.Header.Set("Accept-Encoding", algo)
resp, err := app.Test(req, 10*time.Second)
resp, err := app.Test(req, TestConfig{
Timeout: 10 * time.Second,
FailOnTimeout: true,
})

require.NoError(t, err, "app.Test(req)")
require.Equal(t, 200, resp.StatusCode, "Status code")
Expand Down
29 changes: 27 additions & 2 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,10 +641,10 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler)

## Test

Testing your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass `-1` as the second argument.
Testing your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass a `TestConfig` struct with `Timeout: 0`.

```go title="Signature"
func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error)
func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error)
```

```go title="Example"
Expand Down Expand Up @@ -685,6 +685,31 @@ func main() {
}
```

If not provided, TestConfig is set to the following defaults:

```go title="Default TestConfig"
config := fiber.TestConfig{
Timeout: time.Second(),
FailOnTimeout: true,
}
```

:::caution

This is **not** the same as supplying an empty `TestConfig{}` to
`app.Test(), but rather be the equivalent of supplying:

```go title="Empty TestConfig"
cfg := fiber.TestConfig{
Timeout: 0,
FailOnTimeout: false,
}
```

This would make a Test that has no timeout.

:::

## Hooks

`Hooks` is a method to return the [hooks](./hooks.md) property.
Expand Down
65 changes: 64 additions & 1 deletion docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ We have made several changes to the Fiber app, including:

### Methods changes

- Test -> timeout changed to 1 second
- Test -> Replaced timeout with a config parameter
- -1 represents no timeout -> 0 represents no timeout
- Listen -> has a config parameter
- Listener -> has a config parameter

Expand Down Expand Up @@ -184,6 +185,68 @@ To enable the routing changes above we had to slightly adjust the signature of t
+ Add(methods []string, path string, handler Handler, middleware ...Handler) Router
```

### Test Config

The `app.Test()` method now allows users to customize their test configurations:

<details>
<summary>Example</summary>

```go
// Create a test app with a handler to test
app := fiber.New()
app.Get("/", func(c fiber.Ctx) {
return c.SendString("hello world")
})

// Define the HTTP request and custom TestConfig to test the handler
req := httptest.NewRequest(MethodGet, "/", nil)
testConfig := fiber.TestConfig{
Timeout: 0,
FailOnTimeout: false,
}

// Test the handler using the request and testConfig
resp, err := app.Test(req, testConfig)
```

</details>

To provide configurable testing capabilities, we had to change
the signature of the `Test` method.

```diff
- Test(req *http.Request, timeout ...time.Duration) (*http.Response, error)
+ Test(req *http.Request, config ...fiber.TestConfig) (*http.Response, error)
```
gaby marked this conversation as resolved.
Show resolved Hide resolved

The `TestConfig` struct provides the following configuration options:

- `Timeout`: The duration to wait before timing out the test. Use 0 for no timeout.
- `FailOnTimeout`: Controls the behavior when a timeout occurs:
- When true, the test will return an `os.ErrDeadlineExceeded` if the test exceeds the `Timeout` duration.
- When false, the test will return the partial response received before timing out.

If a custom `TestConfig` isn't provided, then the following will be used:

```go
testConfig := fiber.TestConfig{
Timeout: time.Second,
FailOnTimeout: true,
}
```

**Note:** Using this default is **NOT** the same as providing an empty `TestConfig` as an argument to `app.Test()`.

An empty `TestConfig` is the equivalent of:

```go
testConfig := fiber.TestConfig{
Timeout: 0,
FailOnTimeout: false,
}
```

---

## 🧠 Context
Expand Down
33 changes: 28 additions & 5 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,13 +613,36 @@ func isNoCache(cacheControl string) bool {
}

type testConn struct {
r bytes.Buffer
w bytes.Buffer
r bytes.Buffer
w bytes.Buffer
isClosed bool
sync.Mutex
}

func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } //nolint:wrapcheck // This must not be wrapped
func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } //nolint:wrapcheck // This must not be wrapped
func (*testConn) Close() error { return nil }
func (c *testConn) Read(b []byte) (int, error) {
c.Lock()
defer c.Unlock()

return c.r.Read(b) //nolint:wrapcheck // This must not be wrapped
}

func (c *testConn) Write(b []byte) (int, error) {
c.Lock()
defer c.Unlock()

if c.isClosed {
return 0, errors.New("testConn is closed")
}
return c.w.Write(b) //nolint:wrapcheck // This must not be wrapped
}

func (c *testConn) Close() error {
c.Lock()
defer c.Unlock()

c.isClosed = true
return nil
}

func (*testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} }
func (*testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} }
Expand Down
42 changes: 42 additions & 0 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,48 @@ func Test_Utils_TestConn_Deadline(t *testing.T) {
require.NoError(t, conn.SetWriteDeadline(time.Time{}))
}

func Test_Utils_TestConn_ReadWrite(t *testing.T) {
t.Parallel()
conn := &testConn{}

// Verify read of request
_, err := conn.r.Write([]byte("Request"))
require.NoError(t, err)

req := make([]byte, 7)
_, err = conn.Read(req)
require.NoError(t, err)
require.Equal(t, []byte("Request"), req)

// Verify write of response
_, err = conn.Write([]byte("Response"))
require.NoError(t, err)

res := make([]byte, 8)
_, err = conn.w.Read(res)
require.NoError(t, err)
require.Equal(t, []byte("Response"), res)
}

func Test_Utils_TestConn_Closed_Write(t *testing.T) {
t.Parallel()
conn := &testConn{}

// Verify write of response
_, err := conn.Write([]byte("Response 1\n"))
require.NoError(t, err)

// Close early, write should fail
conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here
_, err = conn.Write([]byte("Response 2\n"))
require.Error(t, err)

res := make([]byte, 11)
_, err = conn.w.Read(res)
require.NoError(t, err)
require.Equal(t, []byte("Response 1\n"), res)
}

func Test_Utils_IsNoCache(t *testing.T) {
t.Parallel()
testCases := []struct {
Expand Down
Loading
Loading