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

[ISSUE-70] Possibility to not fail the test during retries #72

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ type HTTPTestMaker struct {
}

// NewHTTPTestMaker is function for set options for all cute.
// For example, you can set timeout for all requests or set custom http client
// Options:
// - WithCustomHTTPTimeout - set timeout for all requests
// - WithHTTPClient - set custom http client
// - WithCustomHTTPRoundTripper - set custom http round tripper
// - WithJSONMarshaler - set custom json marshaler
// - WithMiddlewareAfter - set function which will run AFTER test execution
// - WithMiddlewareAfterT - set function which will run AFTER test execution with TB
// - WithMiddlewareBefore - set function which will run BEFORE test execution
// - WithMiddlewareBeforeT - set function which will run BEFORE test execution with TB
func NewHTTPTestMaker(opts ...Option) *HTTPTestMaker {
var (
o = &options{
Expand Down
36 changes: 36 additions & 0 deletions builder_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,54 @@ import (
"time"
)

// RequestRepeat is a function for set options in request
// if response.Code != Expect.Code, than request will repeat Count counts with Delay delay.
// Default delay is 1 second.
func (qt *cute) RequestRepeat(count int) RequestHTTPBuilder {
qt.tests[qt.countTests].Request.Repeat.Count = count

return qt
}

// RequestRepeatDelay set delay for request repeat.
// if response.Code != Expect.Code, than request will repeat Count counts with Delay delay.
// Default delay is 1 second.
func (qt *cute) RequestRepeatDelay(delay time.Duration) RequestHTTPBuilder {
qt.tests[qt.countTests].Request.Repeat.Delay = delay

return qt
}

// RequestRepeatPolitic set politic for request repeat.
// if response.Code != Expect.Code, than request will repeat Count counts with Delay delay.
// if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute.
// If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute.
func (qt *cute) RequestRepeatPolitic(politic *RequestRepeatPolitic) RequestHTTPBuilder {
if politic == nil {
panic("politic is nil in RequestRepeatPolitic")
}

qt.tests[qt.countTests].Request.Repeat = politic

return qt
}

// RequestRepeatOptional set option politic for request repeat.
// if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute.
func (qt *cute) RequestRepeatOptional(option bool) RequestHTTPBuilder {
qt.tests[qt.countTests].Request.Repeat.Optional = option

return qt
}

// RequestRepeatBroken set broken politic for request repeat.
// If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute.
func (qt *cute) RequestRepeatBroken(broken bool) RequestHTTPBuilder {
qt.tests[qt.countTests].Request.Repeat.Broken = broken

return qt
}

func (qt *cute) Request(r *http.Request) ExpectHTTPBuilder {
qt.tests[qt.countTests].Request.Base = r

Expand Down
33 changes: 23 additions & 10 deletions errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ type CuteError struct {
Attachments []*Attachment
}

// NewCuteError is the function, which creates cute error with "Name" and "Message" for allure
func NewCuteError(name string, err error) *CuteError {
return &CuteError{
Name: name,
Err: err,
}
}

// NewAssertError is the function, which creates error with "Actual" and "Expected" for allure
func NewAssertError(name string, message string, actual interface{}, expected interface{}) error {
return &CuteError{
Expand All @@ -94,15 +102,16 @@ func NewAssertError(name string, message string, actual interface{}, expected in
}
}

// NewAssertErrorWithMessage ...
// NewAssertErrorWithMessage is the function, which creates error with "Name" and "Message" for allure
// Deprecated: use NewEmptyAssertError instead
func NewAssertErrorWithMessage(name string, message string) error {
return &CuteError{
Name: name,
Message: message,
}
return NewEmptyAssertError(name, message)
}

// NewEmptyAssertError ...
// NewEmptyAssertError is the function, which creates error with "Name" and "Message" for allure
// Returns AssertError with empty fields
// You can use PutFields and PutAttachment to add additional information
// You can use SetOptional, SetRequire, SetBroken to change error behavior
func NewEmptyAssertError(name string, message string) AssertError {
return &CuteError{
Name: name,
Expand All @@ -111,12 +120,14 @@ func NewEmptyAssertError(name string, message string) AssertError {
}
}

// Unwrap ...
// Unwrap is a method to get wrapped error
// It is used for errors.Is and errors.As functions
func (a *CuteError) Unwrap() error {
return a.Err
}

// Error ...
// Error is a method to get error message
// It is used for fmt.Errorf and fmt.Println functions
func (a *CuteError) Error() string {
if a.Trace == "" {
return a.Message
Expand All @@ -131,12 +142,14 @@ func (a *CuteError) Error() string {
return fmt.Sprintf("%s\nCalled from: %s", errText, a.Trace)
}

// GetName ...
// GetName is a method to get error name
// It is used for allure step name
func (a *CuteError) GetName() string {
return a.Name
}

// SetName ...
// SetName is a method to set error name
// It is used for allure step name
func (a *CuteError) SetName(name string) {
a.Name = name
}
Expand Down
36 changes: 36 additions & 0 deletions examples/single_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,42 @@ func Test_Single_Broken(t *testing.T) {
ExecuteTest(context.Background(), t)
}

func Test_Single_RepeatPolitic_Optional_Success_Test(t *testing.T) {
cute.NewTestBuilder().
Title("Test_Single_RepeatPolitic_Optional_Success_Test").
Create().
RequestRepeat(2).
RequestRepeatOptional(true).
RequestBuilder(
cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
).
BrokenAssertBodyT(func(t cute.T, body []byte) error {
return errors.New("example broken error")
}).
ExpectStatus(http.StatusCreated).
ExecuteTest(context.Background(), t)

t.Logf("You should see it")
}

func Test_Single_RepeatPolitic_Broken_Failed_Test(t *testing.T) {
cute.NewTestBuilder().
Title("Test_Single_RepeatPolitic_Broken_Failed_Test").
Create().
RequestRepeat(2).
RequestRepeatOptional(true).
siller174 marked this conversation as resolved.
Show resolved Hide resolved
RequestBuilder(
cute.WithURI("https://jsonplaceholder.typicode.com/posts/1/comments"),
).
BrokenAssertBodyT(func(t cute.T, body []byte) error {
return errors.New("example broken error")
}).
ExpectStatus(http.StatusCreated).
ExecuteTest(context.Background(), t)

t.Logf("You should see it")
}

func Test_Single_Broken_2(t *testing.T) {
cute.NewTestBuilder().
Title("Test_Single_Broken_2").
Expand Down
30 changes: 24 additions & 6 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ type TableTest interface {
ControlTest
}

// RequestHTTPBuilder is a scope of methods for create HTTP requests
// RequestHTTPBuilder is a scope of methods to create HTTP requests
type RequestHTTPBuilder interface {
// Request is function for set http.Request
Request(r *http.Request) ExpectHTTPBuilder
// RequestBuilder is function for create http.Request with help builder.
// RequestBuilder is function for set http.Request with builders
// Available builders:
// WithMethod
// WithURL
Expand All @@ -169,13 +169,31 @@ type RequestHTTPBuilder interface {
RequestParams
}

// RequestParams is a scope of methods for configurate http request
// RequestParams is a scope of methods to configure request
type RequestParams interface {
// RequestRepeat is a count of repeat request, if request was failed.
// RequestRepeat is a function for set options in request
// if response.Code != Expect.Code, than request will repeat counts with delay.
// Default delay is 1 second.
RequestRepeat(count int) RequestHTTPBuilder
// RequestRepeatDelay is a time between repeat request, if request was failed.
// Default 1 second

// RequestRepeatDelay set delay for request repeat.
// if response.Code != Expect.Code, than request will repeat counts with delay.
// Default delay is 1 second.
RequestRepeatDelay(delay time.Duration) RequestHTTPBuilder

// RequestRepeatPolitic is a politic for repeat request.
// if response.Code != Expect.Code, than request will repeat counts with delay.
// if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute.
// If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will execute.
RequestRepeatPolitic(politic *RequestRepeatPolitic) RequestHTTPBuilder

// RequestRepeatOptional is a option politic for repeat request.
// if Optional is true and request is failed, than test step allure will be skipped, and t.Fail() will not execute.
RequestRepeatOptional(optional bool) RequestHTTPBuilder

// RequestRepeatBroken is a broken politic for repeat request.
// If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will execute.
Copy link
Contributor

Choose a reason for hiding this comment

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

If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will execute.

I guess you mean t.Fail() will not execute here ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right now, t.fail will be executed if we have a broken test. What do you think about it? Is it good way or not?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that Broken will be very similar to just keep default behaviour then
I was thinking that you introduced the Broken flag to have another way of handling the tests.

Basically I thought as follow:

  1. Optional: true -> Will mark the retry as skipped and not fail the whole test
  2. Broken: true -> Will mark the retry as failed, the whole test as broken, but not fail the whole test
  3. Optional: false, Broken false -> Will mark the retries as failed as well as the overall test as failed

Let me know if my understanding is incorrect

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Broken true -> Will mark the retries as failed as well as the overall test as failed.

Copy link
Contributor

Choose a reason for hiding this comment

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

@siller174 Sorry for late reply, let me give more details:
image
This is currently what is happening when I put Optional: true

Here is what I had in mind with the optional mechanism:

  1. If all retries are failing -> fail the test and do not execute the rest of the test (assertions and so on)
  2. If after x retry (x < maxRetryCount), the x retry is successful, continue the rest of the test (assertions and so on)

Aside from this, for now the retry is mostly based on the HttpCode assertion, but I would say it can apply to all kind of assertions instead (body assertion, http code assertion, header assertion, response assertion)
i.e. if any kind of assertion fails during the test process, apply the same retry logic as mentioned in points 1. and 2. above

RequestRepeatBroken(broken bool) RequestHTTPBuilder
}

// ExpectHTTPBuilder is a scope of methods for validate http response
Expand Down
2 changes: 1 addition & 1 deletion jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func checkJSONSchema(expect gojsonschema.JSONLoader, data []byte) []error {

validateResult, err := gojsonschema.Validate(expect, gojsonschema.NewBytesLoader(data))
if err != nil {
return []error{errors.NewAssertErrorWithMessage("could not validate json schema", err.Error())}
return []error{errors.NewEmptyAssertError("could not validate json schema", err.Error())}
}

if !validateResult.Valid() && len(validateResult.Errors()) > 0 {
Expand Down
31 changes: 20 additions & 11 deletions roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ func (it *Test) makeRequest(t internalT, req *http.Request) (*http.Response, []e
executeWithStep(t, createTitle(i, countRepeat, req), func(t T) []error {
resp, err = it.doRequest(t, req)
if err != nil {
if it.Request.Repeat.Broken {
err = wrapBrokenError(err)
}

if it.Request.Repeat.Optional {
err = wrapOptionalError(err)
}

return []error{err}
}

Expand All @@ -60,10 +68,13 @@ func (it *Test) doRequest(t T, baseReq *http.Request) (*http.Response, error) {
// copy request, because body can be read once
req, err := copyRequest(baseReq.Context(), baseReq)
if err != nil {
return nil, err
return nil, cuteErrors.NewCuteError("[Internal] Could not copy request", err)
}

resp, httpErr := it.httpClient.Do(req)
if resp == nil {
return nil, cuteErrors.NewCuteError("[HTTP] Response is nil", httpErr)
}

// BAD CODE. Need to copy body, because we can't read body again from resp.Request.Body. Problem is io.Reader
resp.Request.Body, baseReq.Body, err = utils.DrainBody(baseReq.Body)
Expand All @@ -80,19 +91,17 @@ func (it *Test) doRequest(t T, baseReq *http.Request) (*http.Response, error) {
}

if httpErr != nil {
return nil, httpErr
return nil, cuteErrors.NewCuteError("[HTTP] Could not do request", httpErr)
}

if resp != nil {
// Add information (code, body, headers) about response to Allure step
if addErr := it.addInformationResponse(t, resp); addErr != nil {
// Ignore err return, because it's connected with test logic
it.Error(t, "[ERROR] Could not log information about response. Error %v", addErr)
}
// Add information (code, body, headers) about response to Allure step
if addErr := it.addInformationResponse(t, resp); addErr != nil {
// Ignore err return, because it's connected with test logic
it.Error(t, "[ERROR] Could not log information about response. Error %v", addErr)
}

if validErr := it.validateResponseCode(resp); validErr != nil {
return nil, validErr
}
if validErr := it.validateResponseCode(resp); validErr != nil {
return nil, validErr
}

return resp, nil
Expand Down
22 changes: 15 additions & 7 deletions test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,15 @@ type Request struct {
}

// RequestRepeatPolitic is struct for repeat politic
// if Optional is true and request is failed, than test step allure will be skip, and t.Fail() will not execute.
// If Broken is true and request is failed, than test step allure will be broken, and t.Fail() will not execute.
// If Optional and Broken is false, than test step will be failed, and t.Fail() will execute.
// If response.Code != Expect.Code, than request will repeat Count counts with Delay delay.
type RequestRepeatPolitic struct {
Count int
Delay time.Duration
Count int
Delay time.Duration
Optional bool
Broken bool
}

// Middleware is struct for executeInsideAllure something before or after test
Expand Down Expand Up @@ -206,18 +211,17 @@ func (it *Test) executeInsideAllure(ctx context.Context, allureProvider allurePr
}

// processTestErrors returns flag, which mean finish test or not.
// If test has not optional errors, than test will be failed.
// If test has broken errors, than test will be broken.
// If test has require errors, than test will be failed.
// If test has success, than test will be success.
// If test has only optional errors, than test will be success
// If test has broken errors, than test will be broken on allure and executed t.FailNow().
// If test has require errors, than test will be failed on allure and executed t.FailNow().
func (it *Test) processTestErrors(t internalT, errs []error) ResultState {
if len(errs) == 0 {
return ResultStateSuccess
}

var (
countNotOptionalErrors = 0
state = ResultStateFail
state ResultState
)

for _, err := range errs {
Expand All @@ -227,6 +231,8 @@ func (it *Test) processTestErrors(t internalT, errs []error) ResultState {
if tErr.IsOptional() {
it.Info(t, "[OPTIONAL ERROR] %v", err.Error())

state = ResultStateSuccess

continue
}
}
Expand Down Expand Up @@ -262,6 +268,8 @@ func (it *Test) processTestErrors(t internalT, errs []error) ResultState {
}

if countNotOptionalErrors != 0 {
state = ResultStateFail

it.Error(t, "Test finished with %v errors", countNotOptionalErrors)
}

Expand Down
Loading