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

Move integration tests to a dedicated package #2459

Closed
na-- opened this issue Mar 24, 2022 · 7 comments · Fixed by #2821
Closed

Move integration tests to a dedicated package #2459

na-- opened this issue Mar 24, 2022 · 7 comments · Fixed by #2821
Assignees
Labels
evaluation needed proposal needs to be validated or tested before fully implementing it in k6 high prio refactor tests
Milestone

Comments

@na--
Copy link
Member

na-- commented Mar 24, 2022

#2412 and its preceding PRs refactored some cruft in cmd/ and allowed us to safely and easily write full integration tests of k6. However, these tests are limited to cmd/, since the globalTestState and globalState types, as well as the helper functions and other types they depend on are not exported:

k6/cmd/root_test.go

Lines 18 to 30 in 6d7d732

type globalTestState struct {
*globalState
cancel func()
stdOut, stdErr *bytes.Buffer
loggerHook *testutils.SimpleLogrusHook
cwd string
expectedExitCode int
}
func newGlobalTestState(t *testing.T) *globalTestState {

As evidenced by #2399 (comment), it would be much better to further refactor cmd/, to export and maybe even move these types out of there into a new package or packages, so we can have a dedicated top-level tests/ or integration_tests/ package where we could organize these tests nicely.

@na-- na-- added tests refactor high prio evaluation needed proposal needs to be validated or tested before fully implementing it in k6 labels Mar 24, 2022
@na-- na-- added this to the v0.39.0 milestone Mar 24, 2022
@olegbespalov
Copy link
Contributor

olegbespalov commented Mar 24, 2022

Are we going to tackle this in the v0.39.0? 🤔

I'm asking because if yes, I'd like to merge the #2453 as it is (in v0.38.0), and move the tests to a separate module in the next cycle.

@na--
Copy link
Member Author

na-- commented Mar 24, 2022

Yeah, I think we should leave it for the next release, possibly start this cooldown. I think it's relatively urgent that we do it because a whole bunch of not-quite-ok integration tests will start piling up until we do, and so I added the high prio tag... But it's not so urgent that we need to drop things to get it done in this release, we already have plenty of integration test goodness for this one, even if it's not quite as user-friendly as I'd like 😅

@na-- na-- modified the milestones: v0.39.0, v0.40.0 Jun 20, 2022
@na-- na-- modified the milestones: v0.40.0, v0.41.0 Jun 29, 2022
@olegbespalov olegbespalov modified the milestones: v0.41.0, v0.42.0 Oct 25, 2022
@olegbespalov olegbespalov self-assigned this Oct 25, 2022
@na-- na-- modified the milestones: v0.42.0, TBD Nov 9, 2022
@olegbespalov olegbespalov removed their assignment Nov 16, 2022
@imiric imiric self-assigned this Nov 23, 2022
@imiric imiric modified the milestones: TBD, v0.42.0 Nov 23, 2022
@imiric
Copy link
Contributor

imiric commented Nov 23, 2022

@na-- I started looking into this, and have a couple of questions:

  • Since the goal is to export globalTestState and related types, and move them to a separate package, this means we'd also need to export cmd.globalState (since testGlobalState embeds it).

    But this comment mentions that we want to prevent direct access to some of these fields from the rest of the codebase. So how do you envision this working?

  • What other non-cmd integration tests do you have in mind that we should add?

    Currently these test types are only useful for cmd integration tests, which look fine to me being in cmd/.

    If we have tests in mind for other packages, it would help determine what exactly needs to be exported, and what can remain in cmd/.

@na--
Copy link
Member Author

na-- commented Nov 24, 2022

Since the goal is to export globalTestState and related types, and move them to a separate package, this means we'd also need to export cmd.globalState (since testGlobalState embeds it).

Not quite - we'd will definitely need to export cmd.GlobalState, and I don't see a need to move it to a separate package. cmd is probably fine. We can probably get away with not exporting globalTestState, it should be able to remain private for just the package with the integration tests 🤔 Though, as my comment below explains, even exporting both and having them in a separate package is probably not a big deal.

But this comment mentions that we want to prevent direct access to some of these fields from the rest of the codebase. So how do you envision this working?

Even if we export cmd.GlobalState, nothing else in the codebase is actually going to be able to use it because of import loops. The cmd/ package is essentially the k6 entry point, the root of the import tree that eventually reaches all other modules in k6. Only modules "above" it, like the one with integration tests, will be able to import and use it. So no regular k6 module should be able to import the cmd or the integration tests package without an import loop.

That said, if we need to, I don't even see a problem with having the GlobalState and GlobalTestState in separate packages, outside of cmd/, so they could technically be import-able by other modules 🤔 It might even make things cleaner. We can juse keep the newGlobalState() and newGlobalTestState() constructors private, so other packages are not tempted to use them, and be careful in how we propagate the actual objects down to the rest of the code 🤔

It is a balance between cleanliness and usability, but our goal is not to completely prevent such things. We need to make it easy to use these mock-able objects and to make it obvious that it's a bad idea to mess with the OS and the external environment directly, that all interactions have to pass through these objects. However, nothing is stopping any k6 module (or an xk6 extension) to directly import the os package and do whatever it wants. We can't prevent that, we just have to make the better approach easy and obvious.

What other non-cmd integration tests do you have in mind that we should add?

Nothing, only integration tests. That is, these tests that essentially start from the root of k6 and run an almost version of the code. I think the only part of k6 that is not touched by the integration tests is newGlobalState()...

However, some currently existing "integration tests" that are outside of the main cmd/ module, like these ones, unfortunately don't fall under this category... 😞 They are what made me create this issue. Since they can't import globalTesState, they have to re-implement a bunch of boilerlpate code from cmd/run.go and elsewhere, just so they can run the test. This makes them harder to maintain (we have to update these tests often when we change something internal) and less reliable (they might not re-implement the test run logic exactly as a normal k6 run will).

And, crucially, they don't need any of these internal implementation details to actually verify whatever they are testing. They are just forced to re-implement them because they don't have access to the globalTesState if they need to be in a separate package. And they need to be in a separate package because they pollute with the modules registry, which unfortunately (because of how xk6 works) needs to be a global variable. Having the integration test in a separate package ensures that this pollution is limited only to the test, because of how Go executes tests.

Currently these test types are only useful for cmd integration tests, which look fine to me being in cmd/.

See ⬆️, some integration tests actually need to be in a separate package. But we might want to move even the ones that are fine in cmd/ eventually, simply because may be easier to organize and nicer to review and add new tests if they can have their own package(s).

@imiric
Copy link
Contributor

imiric commented Nov 24, 2022

What other non-cmd integration tests do you have in mind that we should add?

Nothing, only integration tests. That is, these tests that essentially start from the root of k6 and run an almost version of the code. I think the only part of k6 that is not touched by the integration tests is newGlobalState()...

No, I meant, what other integration tests that are not related to cmd you had in mind. If the entry point will always be cmd, I reiterate my point that these integration tests should continue to exist in cmd.

However, some currently existing "integration tests" that are outside of the main cmd/ module, like these ones, unfortunately don't fall under this category... 😞 They are what made me create this issue.

Ah I see, but these tests are lower level than cmd integration tests. The definition of an "integration test" is broad and fuzzy, but I see these as more lower level and white-boxy (setting up the init state, JS runner, and running the ExecutionScheduler directly), whereas the cmd tests that use globalState work on a higher level and are more black-boxy (they set args, flags, standard streams, etc., and run k6 commands).

So I don't see this as boilerplate. It's more of the required test setup for the level they're written in. If we had a need for more tests at this level, then it would make sense to abstract this away. If not, as you seem to imply, and it only adds maintenance headaches, then sure, let's convert them to cmd integration tests instead.

See ⬆️, some integration tests actually need to be in a separate package. But we might want to move even the ones that are fine in cmd/ eventually, simply because may be easier to organize and nicer to review and add new tests if they can have their own package(s).

Got it. Makes sense.

The reason I'm reluctant to move globalState (and thus globalTestState) outside of cmd is because it's very specific to cmd. It contains fields like stdOut/stdErr/stdIn, args, flags, fs, etc. But I guess it would be fine to export it while keeping it in cmd, and just rely on the fact that import loops will prevent any other non-test package from accessing it. globalTestState can remain unexported in a different package, as you say.

I'll experiment a bit with this, but my rough plan so far is to:

  1. Export cmd.globalState, and keep it in cmd.

  2. Move globalTestState and all integration tests currently in cmd to cmd/integration_tests or just cmd/tests. Since there are no non-cmd integration tests at this level, it makes more sense to keep these in a subpackage of cmd, rather than in e.g. a new root package.

  3. Rewrite the current cmd/integration_tests into cmd integration tests (this reads funny 😅, but you get what I mean: to use globalTestState).

Does this sound good?

@na--
Copy link
Member Author

na-- commented Nov 24, 2022

Ah I see, but these tests are lower level than cmd integration tests. The definition of an "integration test" is broad and fuzzy, but I see these as more lower level and white-boxy (setting up the init state, JS runner, and running the ExecutionScheduler directly), whereas the cmd tests that use globalState work on a higher level and are more black-boxy (they set args, flags, standard streams, etc., and run k6 commands).

What I was trying to say is that these tests are currently lower level, but they didn't need to be lower level. They were forced to be white-boxy because they didn't have access to the globalState and globalTestState. There is no technical reason why they couldn't be simplified to rely on it, like the other integration tests. They will simultaneously become simpler, more robust and easier to maintain, and better tests (closer to reality).

The reason I'm reluctant to move globalState (and thus globalTestState) outside of cmd is because it's very specific to cmd. It contains fields like stdOut/stdErr/stdIn, args, flags, fs, etc. But I guess it would be fine to export it while keeping it in cmd, and just rely on the fact that import loops will prevent any other non-test package from accessing it. globalTestState can remain unexported in a different package, as you say.

I somewhat disagree, it's not very specific to cmd/. Again, the purpose of globalState is not to be inaccessible from the rest of the code. The goal is for everything inside k6 to use it (or its properties), and not the os package directly, so everything can actually be reliably tested with these integration tests that use the globalTestState mocks, without any side-effects like polluting the actual process environment.

It just so happens that the majority of globalState's direct usage is in cmd/, for now. However, most of its properties like the file system and stdOut/stdErr/etc. are threaded and propagated throughout the rest of k6. So it's not going go be a big deal to pass the whole globalState somewhere outside of cmd/, it may even simplify some things in the future, who knows...

The only reason not to do something like this would be to gently encourage lower-level components inside of k6 to avoid directly querying the "raw" environment and use other finer-grained abstractions instead. For example, instead of reading an env variable, to query the value from the Options. However, as long as that querying happens from globalState.envVars and not os.Environ, everything is still perfectly fine, we would still be able to test the behavior in an idempotent integration test.

If we want to go to extremes, I'd definitely prefer to have the whole GlobalState object available everywhere in the k6 code, even if we don't need all of it, than to need some of its properties somewhere deep in the code and have to either use os directly, or refactor half the codebase to thread that single property to that specific place.

I'll experiment a bit with this, but my rough plan so far is to:

  1. Export cmd.globalState, and keep it in cmd.

  2. Move globalTestState and all integration tests currently in cmd to cmd/integration_tests or just cmd/tests. Since there are no non-cmd integration tests at this level, it makes more sense to keep these in a subpackage of cmd, rather than in e.g. a new root package.

Hmm I kind of think it will be simpler to just have GlobalState exported in a sub-package of cmd/, e.g. cmd/state. And have its constructor newGlobalState() unchanged - unexported and in cmd/. We don't need to use it outside of cmd/ yet, but if we ever need to, we won't have to rewrite the whole cmd/ code once again.

Similarly, GlobalTestState can be exported and moved to a cmd sub-package, e.g. cmd/tests/global_test_state.go or cmd/integration_tests/global_test_state.go. Most of the integration tests can be in the same package, but other ones (e.g. ones that add JS or output modules and pollute) can be in sub-packages. It's important that GlobalTestState is not actually in a _test.go file, since then standalone integration tests (in their own package) won't be able to import it, I think.

@imiric
Copy link
Contributor

imiric commented Dec 13, 2022

Hey, I'm done with most of the changes we agreed on, but have a question about which tests should be moved to cmd/tests. Which ones are "integration" tests, essentially.

I moved all of the ones in cmd/integration_test.go, but should most of cmd/archive_test.go and cmd/run_test.go also be moved?

I'm inclined to say "yes", and to consider an integration test any test that runs newRootCommand().execute() (they will actually run cmd.Execute() once moved, but we can discuss the details in the PR). Everything else should be a unit test that stays in cmd. Sounds good?

EDIT 2022-12-16: I opted to keep these where they are (see #2821), since it would mean most of the cmd tests would be moved to cmd/tests, which would be a much larger diff, for no real benefit. I still think we should properly define what an "integration" test is, as technically, most cmd tests are very much alike the tests in cmd/tests.

imiric pushed a commit that referenced this issue Dec 16, 2022
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Dec 16, 2022
imiric pushed a commit that referenced this issue Dec 16, 2022
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
@imiric imiric linked a pull request Dec 16, 2022 that will close this issue
imiric pushed a commit that referenced this issue Dec 20, 2022
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Dec 20, 2022
imiric pushed a commit that referenced this issue Dec 20, 2022
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
imiric pushed a commit that referenced this issue Jan 12, 2023
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Jan 12, 2023
imiric pushed a commit that referenced this issue Jan 12, 2023
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
imiric pushed a commit that referenced this issue Jan 13, 2023
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Jan 13, 2023
imiric pushed a commit that referenced this issue Jan 13, 2023
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
imiric pushed a commit that referenced this issue Jan 16, 2023
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Jan 16, 2023
imiric pushed a commit that referenced this issue Jan 16, 2023
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
imiric pushed a commit that referenced this issue Jan 16, 2023
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
imiric pushed a commit that referenced this issue Jan 17, 2023
This allows us to use it in tests outside of the cmd package.

See #2459
imiric pushed a commit that referenced this issue Jan 17, 2023
imiric pushed a commit that referenced this issue Jan 17, 2023
This also removes the duplication of the xk6-timers module, and uses
it directly now that it's an experimental built-in module.

Although, shouldn't these be xk6-timers tests rather than event loop
ones? We have plenty of tests that indirectly test the event loop, and
we could always add much simpler ones that don't involve the timers
module.

See #2459
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
evaluation needed proposal needs to be validated or tested before fully implementing it in k6 high prio refactor tests
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants