-
Notifications
You must be signed in to change notification settings - Fork 925
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
Beginnings of a test framework #149
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: totherme Assign the PR to them by writing The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
42b51c7
to
6a2d02b
Compare
59630f0
to
6c49758
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've stopped before that commit: "Implement first command talking to the APIServer". I'll keep reading later.
I think it's looking good, I have minor nits and dumb questions, nothing blocking.
pkg/framework/test/apiserver.go
Outdated
} | ||
|
||
// Start starts the apiserver, and returns a gexec.Session. To stop it again, call Terminate and Wait on that session. | ||
func (s APIServer) Start(etcdURL string) (*gexec.Session, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming it's part of the plan, but we'll probably want to run these in parallel:
- Can the non-insecure port be selected, or is it necessarily 443?
- The insecure-port will have to be configurable (and maybe automatically selected for you),
- Do you happen to know how long it takes to spawn a new apiserver?
- We might end-up with incompatible options for various versions of the apiserver, something to keep in mind.
Nothing to be changed here, just thinking out loud ... :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Right now only the etcd port is configurable. However we know that we might need to make other ports configurable or random. We have some stories for that in our backlog to address that and see if and how we can make all the components run multiple times in parallel and play nicely together.
- We started tracking the time it takes to bring up and bring down the whole thing, see here.
- I see your point with the incompatible options. When this becomes an issue we can introduce some versioning or something else to address that.
pkg/framework/test/apiserver_test.go
Outdated
Context("when given a path to a non-executable", func() { | ||
It("fails with a helpful error", func() { | ||
apiServer := APIServer{ | ||
Path: "./apiserver.go", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we ever use bazel
, that's probably going to break. IOW, it creates a weird implicit dependency on that file being here. Maybe create a tempfile?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point -- we can use a temporary file here.
Is this also an issue with any use of gexec.Build
?
|
||
Context("when given a path to a binary that runs for a long time", func() { | ||
It("can start and stop that binary", func() { | ||
pathToFakeAPIServer, err := gexec.Build("k8s.io/kubectl/pkg/framework/test/assets/fakeapiserver") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is testing gexec
or the framework. The coupling with main.go
below is kind of awkward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you say "the coupling with main.go
below is kind of awkward", we think you're primarily referring to the duplication between the arguments in test/apiserver.go and fakeapiserver/apiserver.go. We totally agree that this is awkward.
That fake is only used by this one test -- so if we delete this test, we can delete that fake. That might be a good thing -- especially since the wiring which that test was originally written to drive out is now covered by a higher level integration test. The non-happy-path cases in this file cover code that's not covered anywhere else, so we'd definitely like to keep those.
From a "tests as docs" viewpoint, it feels like a little bit of a pity to leave that file as an incomplete doc -- describing only the error cases... But maybe that's actually just fine? We might have a bit of a think about whether we can write any useful happy-path test (motivated primarily by wanting to keep some executable docs for that path) that doesn't use that fake.
) | ||
|
||
func main() { | ||
expectedArgs := []string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as for the apiserver. This feels kind of awkward and out of place.
@@ -0,0 +1,83 @@ | |||
package test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts about fixtures.
In kubernetes, the etcd server is mostly hidden behind the apiserver. I'm wondering if the abstraction should be:
apiserver := APIServer{Path: "/bin/apiserver", Etcd: Etcd{Path: "/bin/etcd"}}
// The test doesn't really need etcd
// Maybe going further, and completely hiding the apiserver:
kubectl := Kubectl{Path: "/bin/kubectl", apiserver: APIServer{Path: "/bin/apiserver", Etcd: Etcd{Path: "/bin/etcd"}}}
// We have a kubectl that is connected to the apiserver that is connected to etcd.
// I don't have to KNOW anything about apiserver, and how it's connected. If I need, I can keep a handle to the APIServer object, but I don't have to.
IOW: explicit dependency between the two components. The connection between the two would be somewhat hidden from the user.
Interested in your opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of random thoughts we just discussed:
- We tried to make the interface for the processes we start in our framework rather generic. That should make it relatively easy to introduce new processes (Scheduler, Controller(Manager), ...) somewhen in the future.
- We see that the APIServer depends on Etcd -- from that point of view it might be a good idea to make this dependency explicit as you suggested.
- I believe we can run the APIServer without Etcd. When Etcd eventually comes up APIServer will connect and everything will just work. We might have test which actually test with a missing Etcd and/or an dis- and reappearing Etcd.
- ( What if I'd like to run my tests against a already running instance of Etcd on a different machine or so?[1] )
So we are not against making the dependency explicit, we think both ways have their pros and cons. I -- personally -- would opt for keeping it as is for now, but that's just a gut feeling. We are happy to discuss that in more depth!
[1] That just came to our minds -- we know that is not supported or even planned to be supported by our framework. However we thought that might be a valuable use case to keep in mind ...
|
||
var _ = Describe("Integration", func() { | ||
It("Successfully manages the fixtures lifecycle", func() { | ||
assetsDir, ok := os.LookupEnv("KUBE_ASSETS_DIR") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you planning on having a generic way to give the binaries? KUBE_TEST_APISERVER
and KUBE_TEST_ETCD
, so that my test can easily switch the underlying binary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. We have that in the back of our heads. We have one story in our backlog for that.
) | ||
|
||
var _ = Describe("Integration", func() { | ||
It("Successfully manages the fixtures lifecycle", func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to have a benchmark test to see how long it takes to spawn and stop a fixture? I think that's a metric we'll be interested in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We introduced that in "Track the time it takes to start&stop the fixtures"
You can find the timings in our pipeline:
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/23#L5a1d829b:71
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/22#L5a1d90c7:27
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/20#L5a1d8f04:71
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/16#L5a1558c6:24
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/15#L5a1558c3:20
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/14#L5a15589a:19
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/13#L5a1558b8:62
- https://wings.concourse.ci/teams/k8s-c10s/pipelines/kubectl/jobs/test-dev-branch/builds/12#L5a1558b5:65
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW: You can run our tests via ./scripts/run-tests.sh
; If you run GINKGO_PERFORMANCE=1 ./scripts/run-tests.sh
it will also do some benchmarking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's super exciting to see! The numbers are not that exciting. Let's see later what we can do.
pkg/framework/test/etcd.go
Outdated
args := []string{ | ||
"--debug", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have the feeling that this is going to be painful to maintain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is only about using --debug
or not, we can totally remove that line. If it's about management of arguments (potentially for different versions, ...), we believe we have already covered that in the other comment.
// Create knows how to create a temporary directory and how to keep track of it. | ||
func (t *TempDirManager) Create() (string, error) { | ||
if t.dir == "" { | ||
dir, err := t.Maker("", "kube-test-framework") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kube-test-framework-
? (extra-dash)
@@ -27,11 +27,11 @@ var _ = Describe("Integration", func() { | |||
|
|||
Eventually(func() bool { | |||
return isSomethingListeningOnPort(2379) | |||
}, 5*time.Second).Should(BeTrue(), "Expected Etcd to listen on 2379") | |||
}, 25*time.Second).Should(BeTrue(), "Expected Etcd to listen on 2379") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I foresee ❄️ :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah ... we need to figure out how to handle that properly -- we dropped a story in our backlog for that.
We use [ginkgo](http://onsi.github.io/ginkgo/) and [gomega](http://onsi.github.io/gomega/) for testing. We generate some boilerplate with: ``` mkdir integration cd integration ginkgo bootstrap ginkgo generate integration ``` We use [gexec](http://onsi.github.io/gomega/#gexec-testing-external-processes) to compile and run the CLI under test, and to inspect its output. We use `dep ensure` to ensure that all our dependencies are properly vendored. From now on, this will be our workflow with every commit.
As a separate commit, to make review easier.
We use [ginkgo](http://onsi.github.io/ginkgo/) and [gomega](http://onsi.github.io/gomega/) for testing. We generate some boilerplate with: ``` mkdir integration cd integration ginkgo bootstrap ginkgo generate integration ``` We use [gexec](http://onsi.github.io/gomega/#gexec-testing-external-processes) to compile and run the CLI under test, and to inspect its output. We use `dep ensure` to ensure that all our dependencies are properly vendored. From now on, this will be our workflow with every commit.
As a separate commit, to make review easier.
To start an apiserver: ``` apiServer := APIServer{Path: "/path/to/my/apiserver/binary"} session, err := apiServer.Start("tcp://whereever.is.my.etcd:port") Expect(err).NotTo(HaveOccurred()) ``` When you're done testing against that apiserver: ``` session.Terminate().Wait() ``` ...or if you prefer: ``` gexec.Terminate() ``` ...which will terminate not only this apiserver, but also all other command sessions you started in this test.
This can be started and stopped the same way as the apiserver.
Create a new set of test fixtures by doing: ``` f := test.NewFixtures("/path/to/etcd", "/path/to/apiserver") ``` Before running your integration tests, start all your fixtures: ``` err := f.Start() Expect(err).NotTo(HaveOccurred()) ``` Now that you have started your etcd and apiserver, you'll find the apiserver listening locally on the default port. When you're done with your testing, stop and clean up: ``` err := f.Stop() Expect(err).NotTo(HaveOccurred()) ```
We're using concourse because we happen to have a concourse deployment available. You can look at it here: https://wings.concourse.ci/teams/k8s-c10s/
We're not exercising the test framework yet, but it's in place. Our democli expects its test assets to be in `./assets/bin`. We have a script `./scripts/download-binaries.sh` which will populate that directory from a google storage bucket. Once those assets are in place, you can run tests with `./scripts/run-tests.sh`.
os.tempDir() gives the path to the temporary directory, it does not create a random temporary directory.
Using tcp:// does not work.
Testing the lifecycle of our fixtures with the real binaries. Test if we can start the fixtures, the porcesses actually listen on the ports and if we tear down all the parts successfully again.
- Store stdout,stderr in private buffers - Configure the etcURL on construction instead of at start time - Handle the creation of the temporary directory (for the data directory) internally
- Store stdout,stderr in private buffers - Configure the etcURL on construction instead of at start time
Instead of the separate {Etcd,APIServer}StartStopper use the unified interface FixtureProcess
We are now returning an error instead of using an Expectation inline.
- Start() should only return when the process is actually up and listening - It may take some time to tear down a process, so we increased the timeout for Stop() (to some random number) - We make sure Std{Out,Err} is properly initialized, we should not rely on Ginkgo/Gomega to do that for us
... as they have been unified into the FixtureProcess interface and thus they are not needed anymore.
We use the standard go client for kubernetes `client-go`. To vendor it and all its denpendecies we use ``` dep ensure -add k8s.io/client-go@5.0.0 ``` We create a new cobra command with ``` cobra add listPods ``` Note: The new command in cmd/listPods.go uses [the "magic" function init()](https://golang.org/ref/spec#Package_initialization) to register itself.
As a separate commit, to make review easier.
Performance tests are now skipped by default, but run in CI.
While doing that we found that we needed to refactor the fakes to handle command line arguments which are not known up front; we do this by using regular expresseions.
[#153243856]
[#153246098]
- Use a `.gitignore` local to the test framework - Remove some local only things (`.idea`) from the `.gitignore` and push that to `.git/info/exclude`
Actually now put the dependencies into the global `vendor` direcotry.
25badce
to
849d4f9
Compare
Eventually we want our framework to work nicely with just `go test`. To get there we need to - inject KUBE_ASSETS_DIR - make the framework work when run multiple times in parallel (port collitions, expose bound ports the the subject under test, ...) We decided to make sure our tests are run in sequence (and not in parallel to any other thing using etcd, for that matter) by making this explicit in the `pre-commit.sh` - for now. As soon as we are there, we can rollback the change to the `pre-commit.sh` end have the test framework be tested the same as everything else. [#153248975]
3df0191
to
4d4026f
Compare
} | ||
|
||
func runGetPods() { | ||
config, _ := clientcmd.BuildConfigFromFlags("http://localhost:8080", "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a parameter instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. We want to have our framework generate some sort of config (the framework will be the thing that determines the port eg. the APIServer is listening on) and somehow expose that to the clients resp. the component under test. Until we have that and somewhat in the spirit of TDD we just did the simplest thing possible to make our stuff work. Next iterations will most definitely change that.
@@ -20,4 +20,11 @@ var _ = Describe("Integration", func() { | |||
Expect(session.Out).To(gbytes.Say(helpfulMessage)) | |||
}) | |||
|
|||
It("can get a list of pods", func() { | |||
command := exec.Command(pathToDemoCommand, "listPods") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APIServer coordinates should be passed here.
@@ -40,6 +32,15 @@ var _ = Describe("Integration", func() { | |||
By("Ensuring APIServer is not listening anymore") | |||
Expect(isAPIServerListening()).To(BeFalse(), "Expected APIServer not to listen anymore") | |||
}) | |||
|
|||
Measure("It should be fast to bring up and tear down the fixtures", func(b Benchmarker) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! I didn't know that!
pkg/framework/test/etcd.go
Outdated
} | ||
|
||
// DataDirManager knows how to create and destroy Etcd's data directory. | ||
type DataDirManager interface { | ||
type dataDirManager interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or you can move that to an internal packages if you want to reuse it somewhere else.
"time" | ||
) | ||
|
||
func main() { | ||
expectedArgs := []string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list is going to be a nightmare to maintain. Isn't it already?
@@ -19,7 +19,12 @@ go vet -all ./... | |||
rc=$((rc || $?)) | |||
|
|||
echo "Running go test" | |||
go test -v ./... | |||
go list ./... | grep -vF pkg/framework/test | xargs go test -v |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there should be some sort of "default" value for KUBE_ASSERT_DIR
and maybe we can download the binaries before running go test
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I think this also ties back to making our think work well with go test
. As soon as we have that we can retire our test wrapper script pkg/framework/test/scripts/run-tests.sh
and think about better ways of handling the test binaries/assets resp. their location. We in fact have all those things in our backlog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good for now.
Things that are really important to improve:
- Fix the
go test
hack, - Easier to inject fixtures components (apiserver, etcd, ...) or sane default,
Thanks!
As discussed in #87 and in slack and hangouts.
Known issues: