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

assert: Add HTTP builder to combine request x response using builder. #1491

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions assert/assertion_format.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions assert/assertion_forward.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions assert/http_assertions.go
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
)

Expand Down Expand Up @@ -163,3 +164,55 @@ func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url strin

return !contains
}

// HTTP asserts that a specfied handler returns set of expected values given by HttpOptions.
//
// assert.HTTP(t, myHandler, "www.google.com", nil, WithCode(200), WithBody("I'm Feeling Lucky"), WithRequestHeader(http.Header{"a": []string{"b"}}, WithExpectedBody(bytes.NewBuffer("c"))))
//
// Returns whether the assertion was successful (true) or not (false).
func HTTP(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, options ...HttpOption) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}

if options == nil {
Fail(t, fmt.Sprintf("No options selected, no assertions can be executed"))
return false
}

b := &builder{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the options is optional, should the builder have some defaults?

Consider this:

func handler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello"))
}

func TestHttp(t *testing.T) {
	assert.HTTP(t, handler, http.MethodGet, "/", nil)
}

Fails with:

=== RUN   TestHttp
    main_test.go:15: 
        	Error Trace:	/Users/armahishi/dev/personal/forks/testify/example/main_test.go:15
        	Error:      	Expected HTTP success status code for "/?" but received 200
        	Test:       	TestHttp
--- FAIL: TestHttp (0.00s)
FAIL
FAIL	github.com/stretchr/testify/example	0.173s
FAIL

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point I have added the default code for builder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made a change, based on what you said in next review step.
There are no defaults as the option would override something that is default for the builder. You mentioned that you expected that the status.code is not tested as You did not ask for it.
As the options should be optional, I made a change when you select no option, there is nothing to be tested so the test fails. It is possible to to remove this check, however it is incorrectly tested handler as there is not assertion.

for _, option := range options {
err := option(b)
if err != nil {
Fail(t, fmt.Sprintf("Failed to build http options, got error: %s", err))
}
}
Comment on lines +184 to +189
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only a subset of options are provided, it should only assert that.

Example:

func handler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello"))
}

func TestHttp(t *testing.T) {
	buf := bytes.NewBufferString("hello")
	assert.HTTP(t, handler, http.MethodGet, "/", nil, assert.WithExpectedBody(*buf))
}
=== RUN   TestHttp
    main_test.go:18: 
        	Error Trace:	/Users/armahishi/dev/personal/forks/testify/example/main_test.go:18
        	Error:      	Expected HTTP success status code for "/?" but received 200
        	Test:       	TestHttp
--- FAIL: TestHttp (0.00s)
FAIL
FAIL	github.com/stretchr/testify/example	0.162s
FAIL

In general, if the all those options are not optional, they should not be variadic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To preserve the variadic options, then the code cannot be set as default because there is option that would be set up always and therefore it is not an option, it would be a required parameter of function.
Therefore I decided to make a status.code to be option however to make sure the usage of tests is correct, I throw a fail when no options are set as there is nothing to assert using result of handler call.
Basically it is a prevention of missuse of test that it does nothing except the handler execution.


w := httptest.NewRecorder()
req, err := http.NewRequest(method, url, b.body)
if b.err == nil && err != nil {
Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err))
} else if b.err != nil {
return err == b.err
}

req.Header = b.requestHeader
req.URL.RawQuery = values.Encode()
handler(w, req)
if b.code != nil && w.Code != *b.code {
Fail(t, fmt.Sprintf("Expected HTTP success status code for %q but received %d", url+"?"+values.Encode(), w.Code))
return false
}

if b.responseHeader != nil && !reflect.DeepEqual(w.HeaderMap, b.responseHeader) {
Fail(t, fmt.Sprintf("Expected HTTP header to be equal for %q but received %v", url+"?"+values.Encode(), w.HeaderMap))
return false
}

contains := strings.Contains(w.Body.String(), b.expectedBody.String())
if !contains {
Fail(t, fmt.Sprintf("Expected HTTP body to be equal for %q but received %s", url+"?"+values.Encode(), w.Body.String()))
}

return contains
}
25 changes: 25 additions & 0 deletions assert/http_assertions_test.go
@@ -1,6 +1,7 @@
package assert

import (
"bytes"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -212,3 +213,27 @@ func TestHttpBodyWrappers(t *testing.T) {
assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World"))
assert.True(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world"))
}

func TestHTTPBuilder(t *testing.T) {
assert := New(t)
mockAssert := New(new(testing.T))

// Test status codes
assert.Equal(mockAssert.HTTP(httpOK, "GET", "/", nil, WithCode(200)), true)
assert.Equal(mockAssert.HTTP(httpRedirect, "GET", "/", nil, WithCode(200)), false)
assert.Equal(mockAssert.HTTP(httpError, "GET", "/", nil, WithCode(200)), false)

assert.Equal(mockAssert.HTTP(httpOK, "GET", "/", nil, WithCode(200)), true)
assert.Equal(mockAssert.HTTP(httpRedirect, "GET", "/", nil, WithCode(307)), true)
assert.Equal(mockAssert.HTTP(httpError, "GET", "/", nil, WithCode(500)), true)

// Test codes and body
assert.True(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithExpectedBody(*bytes.NewBufferString("Hello, World!"))))
assert.True(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithExpectedBody(*bytes.NewBufferString("World"))))
assert.False(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithExpectedBody(*bytes.NewBufferString("world"))))

// Test codes headers and body
assert.True(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithResponseHeader(http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}), WithExpectedBody(*bytes.NewBufferString("Hello, World!"))))
assert.True(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithResponseHeader(http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}), WithExpectedBody(*bytes.NewBufferString("World"))))
assert.False(mockAssert.HTTP(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, WithCode(200), WithResponseHeader(http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}), WithExpectedBody(*bytes.NewBufferString("world"))))
}
65 changes: 65 additions & 0 deletions assert/http_options.go
@@ -0,0 +1,65 @@
package assert

import (
"bytes"
"errors"
"io"
http "net/http"
)

type builder struct {
code *int
body io.ReadCloser
expectedBody bytes.Buffer
requestHeader http.Header
responseHeader http.Header
err error
}

type HttpOption func(*builder) error

func WithCode(code int) HttpOption {
return func(b *builder) error {
if code < 100 || code > 511 {
return errors.New("Given HTTP code is outside range of possible values assignement")
}

b.code = &code
return nil
}
}

func WithErr(err error) HttpOption {
return func(b *builder) error {
b.err = err
return nil
}
}

func WithBody(body io.ReadCloser) HttpOption {
return func(b *builder) error {
b.body = body
return nil
}
}

func WithExpectedBody(expectedBody bytes.Buffer) HttpOption {
return func(b *builder) error {
b.expectedBody = expectedBody
return nil
}
}

func WithRequestHeader(requestHeader http.Header) HttpOption {
return func(b *builder) error {
b.requestHeader = requestHeader
return nil
}
}

func WithResponseHeader(responseHeader http.Header) HttpOption {
return func(b *builder) error {
b.responseHeader = responseHeader
return nil
}
}
30 changes: 30 additions & 0 deletions require/require.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions require/require_forward.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.