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

[WIP] Add basic heartbeat job tests #7551

Closed
wants to merge 14 commits into from

Conversation

andrewvc
Copy link
Contributor

@andrewvc andrewvc commented Jul 10, 2018

EDIT: The scope here is now much wider

This PR is now about testing both HTTP and TCP, and introducing a new test schema DSL to do so. It's getting kinda big.

Original below

This PR adds some initial tests for the HTTP task. An open question is how comprehensive we want to be here and where we want to target these tests. It also breaks up one of the longer functions to make reasoning about it easier (and to make understanding the defer behavior more clear)

I think that thorough testing of each intermediate function is a bit overkill, so, I think the question is where to strike the balance. I would love your feedback @urso / @ruflin .

My sense is that we definitely want some acceptance tests for each protocol that test that configuring them correctly does the right thing.

We probably also want unit tests for most helper functions. For the actual IO however, I'm thinking we probably should only setup HTTP servers once in the tests simply to not waste time redundantly testing things. I don't think the spot I'm doing it in these examples is the best one. It probably makes more sense to set that same server up at the 'job level and test the monitors.Job object against it, including thorough tests of all expected fields.

Thoughts?

@andrewvc andrewvc added in progress Pull request is currently in progress. feedback needed needs tests Heartbeat labels Jul 10, 2018
@andrewvc andrewvc requested review from urso and ruflin July 10, 2018 03:33
req, err := http.NewRequest("GET", server.URL, nil)
assert.Nil(t, err)

var validatorResp *http.Response = nil

Choose a reason for hiding this comment

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

should drop = nil from declaration of var validatorResp; it is the zero value

@andrewvc andrewvc changed the title [WIP} Add heartbeat http task tests [WIP] Add heartbeat http task tests Jul 10, 2018
Copy link
Member

@ruflin ruflin left a comment

Choose a reason for hiding this comment

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

As you said it's tricky to find the right balance for the tests. I personally think the biggest value we get out of the tests which run against an endpoint as you did with the http endpoint. There we test directly all the pieces at once. The unit tests can be very useful for developing / adding new features to confirm things work but they can also slow us down if we check too many details. In general I think we should always try to use table test which makes it very easy to add additional checks or modify existing ones.

For the http endpoint tests: I would focus first on getting them running and as soon as we have several test implementations we can still figure out where they fit best and how we can simplify the tests further.

It seems your changes broke the system tests. I also see these as very valuable as they test in the end how the user will interact with the beat and how all pieces play together.

@@ -31,6 +31,8 @@ import (
"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/outputs/transport"

"io"
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Should go on the top list of imports.

@@ -218,41 +220,69 @@ func execPing(
body []byte,
timeout time.Duration,
validator func(*http.Response) error,
) (time.Time, time.Time, common.MapStr, reason.Reason) {
) (start time.Time, end time.Time, event common.MapStr, errReason reason.Reason) {
Copy link
Member

Choose a reason for hiding this comment

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

Not really used to name returns but I see that it increases the readability of the code here. Glad to see that below you don't use a "naked" return.

defer closeIfPresent(&req.Body)

start, end, resp, errReason := execRequest(client, req, validator)
if resp != nil { // If above errors, the response will be nil
Copy link
Member

Choose a reason for hiding this comment

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

What about moving it after the error check in 234? You write that in case of error it is nil. Is also the opposite true that it's not nil in case of no error?

resp, err := client.Do(req)
end := time.Now()
end = time.Now()
Copy link
Member

Choose a reason for hiding this comment

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

Where do you close the response body now?

if err != nil {
return start, end, nil, reason.IOFailed(err)
}
defer resp.Body.Close()

err = validator(resp)
end = time.Now()
Copy link
Member

Choose a reason for hiding this comment

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

This was here before but I'm trying to understand why we move the end time after the validator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question! Any idea @urso ? I can't think of why we'd want to include the validator in the time. That said, I think the difference is negligible. Most validators should be dead simple.

@@ -23,59 +23,174 @@ import (
"net/url"
"reflect"
"testing"

Copy link
Member

Choose a reason for hiding this comment

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

Nits: empty lines

t.Errorf("Expected %T but got %T", err, test.expectedError)
test := test

t.Run(test.name, func(t2 *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Nice.


if err != nil {
if test.expectedError == nil {
t.Error(err)
Copy link
Member

Choose a reason for hiding this comment

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

You name it above t2 but in here you use t. I think you could just stick to t as name also above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's what blindly obeying your IDE gets you ;) Will fix

@ruflin
Copy link
Member

ruflin commented Jul 11, 2018

Something still seems to break a system test: https://beats-ci.elastic.co/job/elastic+beats+pull-request+multijob-linux/5429/beat=heartbeat,label=ubuntu/testReport/junit/test_monitor/Test/test_http_1_404/ Let me know if you need some help to execute it locally to see all the logs etc.

@andrewvc
Copy link
Contributor Author

andrewvc commented Jul 11, 2018 via email


type MapCheckDef common.MapStr

func DeepMapStrCheck(t *testing.T, expected MapCheckDef, actual common.MapStr) {

Choose a reason for hiding this comment

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

exported function DeepMapStrCheck should have comment or be unexported


type MapLeafTest = func(t *testing.T, actual interface{})

type MapCheckDef common.MapStr

Choose a reason for hiding this comment

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

exported type MapCheckDef should have comment or be unexported

assert.True(t, ok)
}

type MapLeafTest = func(t *testing.T, actual interface{})

Choose a reason for hiding this comment

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

exported type MapLeafTest should have comment or be unexported

assert.Nil(t, actual)
}

var IsString = func(t *testing.T, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsString should have comment or be unexported

assert.True(t, converted >= 0)
}

var IsNil = func(t *testing.T, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsNil should have comment or be unexported

io.WriteString(w, BadGatewayBody)
})

var ExactlyEqual = func(expected interface{}) func(t *testing.T, actual interface{}) {

Choose a reason for hiding this comment

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

exported var ExactlyEqual should have comment or be unexported


var BadGatewayBody = "Bad Gateway"

var BadGatewayHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Choose a reason for hiding this comment

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

exported var BadGatewayHandler should have comment or be unexported

io.WriteString(w, HelloWorldBody)
})

var BadGatewayBody = "Bad Gateway"

Choose a reason for hiding this comment

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

exported var BadGatewayBody should have comment or be unexported


var HelloWorldBody = "hello, world!"

var HelloWorldHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Choose a reason for hiding this comment

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

exported var HelloWorldHandler should have comment or be unexported

"github.com/elastic/beats/libbeat/common"
)

var HelloWorldBody = "hello, world!"

Choose a reason for hiding this comment

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

exported var HelloWorldBody should have comment or be unexported

return validateInternal(t, expected, actual, actual, []string{})
}

type WalkObserver func(

Choose a reason for hiding this comment

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

exported type WalkObserver should have comment or be unexported

}
}

func Validate(t *testing.T, expected MapCheckDef, actual common.MapStr) *ValidationResult {

Choose a reason for hiding this comment

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

exported function Validate should have comment or be unexported

}
}

func Scheme(expected MapCheckDef) Validator {

Choose a reason for hiding this comment

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

exported function Scheme should have comment or be unexported

}
}

func Strict(validator Validator) Validator {

Choose a reason for hiding this comment

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

exported function Strict should have comment or be unexported

return &output
}

func Compose(validators ...Validator) Validator {

Choose a reason for hiding this comment

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

exported function Compose should have comment or be unexported

assert.True(t, converted >= 0)
}

var IsNil = func(t *testing.T, _ bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsNil should have comment or be unexported

}
}

var IsDuration = func(t *testing.T, _ bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsDuration should have comment or be unexported

}
}

func TcpChecks(port uint16) MapCheckDef {

Choose a reason for hiding this comment

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

exported function TcpChecks should have comment or be unexported
func TcpChecks should be TCPChecks


// Functions for testing maps in complex ways

func MonitorChecks(id string, ip string, scheme string, status string) MapCheckDef {

Choose a reason for hiding this comment

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

exported function MonitorChecks should have comment or be unexported

}
}

func ServerPort(server *httptest.Server) (uint16, error) {

Choose a reason for hiding this comment

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

exported function ServerPort should have comment or be unexported

assert.True(t, exists)
}

var DoesNotExist = func(t *testing.T, exists bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var DoesNotExist should have comment or be unexported

assert.True(t, ok)
}

var DoesExist = func(t *testing.T, exists bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var DoesExist should have comment or be unexported

assert.Nil(t, actual)
}

var IsString = func(t *testing.T, _ bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsString should have comment or be unexported

assert.True(t, converted >= 0)
}

var IsNil = func(t *testing.T, _ bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsNil should have comment or be unexported

"github.com/stretchr/testify/assert"
)

var IsDuration = func(t *testing.T, _ bool, actual interface{}) {

Choose a reason for hiding this comment

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

exported var IsDuration should have comment or be unexported


type Validator func(*testing.T, common.MapStr) *ValidationResult

type ValidationResult struct {

Choose a reason for hiding this comment

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

exported type ValidationResult should have comment or be unexported


type StrictMap Map

type Validator func(*testing.T, common.MapStr) *ValidationResult

Choose a reason for hiding this comment

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

exported type Validator should have comment or be unexported


type Map map[string]interface{}

type StrictMap Map

Choose a reason for hiding this comment

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

exported type StrictMap should have comment or be unexported


type ValueValidator = func(t *testing.T, keyExists bool, actual interface{})

type Map map[string]interface{}

Choose a reason for hiding this comment

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

exported type Map should have comment or be unexported

"github.com/elastic/beats/libbeat/common"
)

type ValueValidator = func(t *testing.T, keyExists bool, actual interface{})

Choose a reason for hiding this comment

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

exported type ValueValidator should have comment or be unexported

})
}

func TcpChecks(port uint16) skima.Validator {

Choose a reason for hiding this comment

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

exported function TcpChecks should have comment or be unexported
func TcpChecks should be TCPChecks

return uint16(p), nil
}

func MonitorChecks(id string, ip string, scheme string, status string) skima.Validator {

Choose a reason for hiding this comment

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

exported function MonitorChecks should have comment or be unexported

io.WriteString(w, BadGatewayBody)
})

func ServerPort(server *httptest.Server) (uint16, error) {

Choose a reason for hiding this comment

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

exported function ServerPort should have comment or be unexported


var BadGatewayBody = "Bad Gateway"

var BadGatewayHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Choose a reason for hiding this comment

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

exported var BadGatewayHandler should have comment or be unexported

io.WriteString(w, HelloWorldBody)
})

var BadGatewayBody = "Bad Gateway"

Choose a reason for hiding this comment

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

exported var BadGatewayBody should have comment or be unexported

"github.com/stretchr/testify/assert"
)

var IsDuration = func(t *testing.T, _ bool, actual interface{}, dotPath string) {

Choose a reason for hiding this comment

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

exported var IsDuration should have comment or be unexported

return walkValidate(t, expected, actual)
}

type WalkObserver func(

Choose a reason for hiding this comment

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

exported type WalkObserver should have comment or be unexported

}
}

func Validate(t *testing.T, expected Map, actual common.MapStr) *ValidationResult {

Choose a reason for hiding this comment

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

exported function Validate should have comment or be unexported

}
}

func Schema(expected Map) Validator {

Choose a reason for hiding this comment

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

exported function Schema should have comment or be unexported

}
}

func Strict(validator Validator) Validator {

Choose a reason for hiding this comment

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

exported function Strict should have comment or be unexported

@andrewvc
Copy link
Contributor Author

OK, so for the latest round of changes to the schema validation the code could definitely use a few more passes by myself before the internals are nice, but I'd love feedback on the externals at this point. How's this feel as a schema validation library from a user standpoint?

So, I got pretty far on the validation DSL that lets you match a nested map of validators to a MapStr. @urso and I agreed that this would be very useful for the sort of tests heartbeat needs. The following test suite tests the validation framework: https://github.com/andrewvc/beats/blob/add_hb_http_task_tests/heartbeat/skima/core_test.go

You can see how useful it is for heartbeat in the new basic tests I added for the http and tcp endpoints:
https://github.com/andrewvc/beats/blob/add_hb_http_task_tests/heartbeat/monitors/active/http/http_test.go#L66
https://github.com/andrewvc/beats/blob/add_hb_http_task_tests/heartbeat/monitors/active/tcp/tcp_test.go#L38

This obviously exceeds the original scope of this PR. I'll probably shut this one down soon and open a new one.

On @urso’s advice I modified the schema from my original procedural style to be more FP style and be oriented around function composition. I really like the result!

There’s some weirdness with typing ‘strict’ operations (validate that this map passes all checks and also doesn’t have additional keys). I implemented it two ways for now, you can either do it by composing Strict(Schema(Map{…})), or, you can use StrictMap{} instead of Map{}, but due to the lack of union types that doesn’t work at the root level, so Schema(StrictMap{...}) doesn’t work, but submaps can be strict. There are some ways around it (just use interface{})? but probably worth a discussion. I kind of like keeping both options since internally StrictMap is implemented with Strict(Schema). It's also nice because when you use shared schemas and compose them, you might not know whether you want strict validation at schema creation time or not. In other words, the flexibility is nice.

@andrewvc andrewvc changed the title [WIP] Add heartbeat http task tests [WIP] Add basic heartbeat job tests Jul 12, 2018
}
}

func IsIntGt(than int) IsDef {

Choose a reason for hiding this comment

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

exported function IsIntGt should have comment or be unexported

})
}

var IsNil = Is("is nil", func(v interface{}) ValueResult {

Choose a reason for hiding this comment

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

exported var IsNil should have comment or be unexported

}
})

func IsEqual(to interface{}) IsDef {

Choose a reason for hiding this comment

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

exported function IsEqual should have comment or be unexported

"fmt"
)

var IsDuration = Is("is a duration", func(v interface{}) ValueResult {

Choose a reason for hiding this comment

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

exported var IsDuration should have comment or be unexported

return walkValidate(expected, actual)
}

type WalkObserver func(

Choose a reason for hiding this comment

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

exported type WalkObserver should have comment or be unexported

}
}

func Validate(expected Map, actual common.MapStr) MapResults {

Choose a reason for hiding this comment

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

exported function Validate should have comment or be unexported

})
}

func Schema(expected Map) Validator {

Choose a reason for hiding this comment

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

exported function Schema should have comment or be unexported

}
*/

func Test(t *testing.T, r MapResults) {

Choose a reason for hiding this comment

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

exported function Test should have comment or be unexported

return errors
}

func (r MapResults) Valid() bool {

Choose a reason for hiding this comment

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

exported method MapResults.Valid should have comment or be unexported

}
}

func IsIntGt(than int) IsDef {

Choose a reason for hiding this comment

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

exported function IsIntGt should have comment or be unexported

})
}

var IsNil = Is("is nil", func(v interface{}) ValueResult {

Choose a reason for hiding this comment

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

exported var IsNil should have comment or be unexported

}
})

func IsEqual(to interface{}) IsDef {

Choose a reason for hiding this comment

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

exported function IsEqual should have comment or be unexported

"fmt"
)

var IsDuration = Is("is a duration", func(v interface{}) ValueResult {

Choose a reason for hiding this comment

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

exported var IsDuration should have comment or be unexported

return walkValidate(expected, actual)
}

type WalkObserver func(

Choose a reason for hiding this comment

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

exported type WalkObserver should have comment or be unexported

}
}

func Validate(expected Map, actual common.MapStr) MapResults {

Choose a reason for hiding this comment

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

exported function Validate should have comment or be unexported

})
}

func Schema(expected Map) Validator {

Choose a reason for hiding this comment

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

exported function Schema should have comment or be unexported

}
*/

func Test(t *testing.T, r MapResults) {

Choose a reason for hiding this comment

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

exported function Test should have comment or be unexported

return errors
}

func (r MapResults) Valid() bool {

Choose a reason for hiding this comment

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

exported method MapResults.Valid should have comment or be unexported

@andrewvc
Copy link
Contributor Author

This thread is too long. Moved to: #7587

@andrewvc andrewvc closed this Jul 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feedback needed Heartbeat in progress Pull request is currently in progress. needs tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants