Closed Mo0rBy closed 1 year ago
I've done some more research and it looks like theres no way to implement this kind of functionality. Golang does no allow imports to subpackages to prevent infinite import loops so I don't think there is anyway to get this value to be access globally by all packages and sub packages.
Hey! First off - I'd like to encourage you to explore Go's notion of packages a bit more so you can build a stronger mental model for how they work. In brief - you're correct that Go does not allow for circular imports. Note that this isn't related to how deep or shallow a package is - it's simply a statement that if package A
imports package B
then package B
is not allowed to import package A
.
Moreover, the way tests work in Go is a bit tricky to understand at first. Normally all files in a package are compiled into the same package (e.g. all your app_service_*.go
files get compiled into the expected/deployments
package. However there is one special case scenario that Go treats differently: tests. Any files that end with *_test.go
are treated differently. They are never compiled into a standalone importable package. Instead, they are only compiled when the go test
command is invoked (or when you run ginkgo
- which is simply using go test
under the hood) to generate a new, distinct, temporary "testing" package that is then executed to run the tests. I try to cover this in a bit more detail on the Ginkgo docs as it can be confusing at first. An important thing to understand is that you cannot import test packages. So any values defined and exported in your test packages can't be accessed elsewhere.
With all that said there is a way to do what you want to do. I typically create a new package (e.g. config
) that defines the flags, has the init()
function and then exports variables/functions/etc. for use by any consumers. This breaks the import cycle and allows you to use the config package wherever you need:
app_name/
|-- config/
| |-- config.go
|-- expected/
| |-- deployments/
| | |-- app_service_A.go
| | |-- app_service_B.go
| | `-- app_service_C.go
| `-- services/
| |-- app_service_A.go
| |-- app_service_B.go
| `-- app_service_C.go
|-- deployments_test.go
|-- services_test.go
|-- app_suite_test.go
//config.go
var context_name, namespace string
func init() {
flag.StringVar(&context_name, "context-name", "my-kube-context", "The kube context name to use")
flag.StringVar(&namespace, "namespace", "", "The cluster namespace to test against")
}
func KubeOptions() *k8s.KubectlOptions {
return k8s.NewKubectlOptions(context_name, "", namespace)
}
now you can import ".../config"
from any code that needs it and use config.KubeOptions()
to get the relevant options object.
Lastly - the environment variable approach you attempted should have worked. It might be an indication that there is something about your set up that doesn't play well with how Ginkgo expects parallel suites to be set up. I can help with that if you'd like.
Thanks for your reply @onsi ! A few follow up questions.
You've created the KubeOptions()
function and then that returns a new object every time it is called. If I'm using this lots, will this affect the performance of my test framework? Would it be more ideal to create a var Kube_options = k8s.NewKubectlOptions(context_name, "", namespace)
and then import that var object?
And am I correct in saying that I don't need to use the BeforeSuite()
because this config.go
is not a test file and is compiled before the _test.go
files (just as you have described). For this reason, the object I want will be created and usable in all my other test packages?
hey @Mo0rBy
You've created the
KubeOptions()
function and then that returns a new object every time it is called. If I'm using this lots, will this affect the performance of my test framework? Would it be more ideal to create avar Kube_options = k8s.NewKubectlOptions(context_name, "", namespace)
and then import that var object?
I very much doubt you'll have any measurable performance impact, but yes you can define a variable however you'll want to define it after the flags are parsed which can be tricky to get right. I'd just stick with a function call personally.
And am I correct in saying that I don't need to use the
BeforeSuite()
because thisconfig.go
is not a test file and is compiled before the_test.go
files (just as you have described). For this reason, the object I want will be created and usable in all my other test packages?
The objects defined in the new config
package will be accessible to all specs, yes. You won't need to use BeforeSuite
for this purpose any more (however if there is some other kind of set up your BeforeSuite
is doing you'll obviously need to keep it around for that!)
I attempted to do what I've described above and it doesn't seem to work. I had a utils
package already, so I decided to put my init()
function in there.
package utils
import (
"flag"
"fmt"
"github.com/gruntwork-io/terratest/modules/k8s"
)
var context_name, namespace string
var Kube_options *k8s.KubectlOptions
func init() {
flag.StringVar(&context_name, "context-name", "my-kube-context", "The kube context name to use")
flag.StringVar(&namespace, "namespace", "", "The cluster namespace to test against")
fmt.Println("LOOK HERE!")
fmt.Println(namespace)
Kube_options = k8s.NewKubectlOptions(context_name, "", namespace)
}
I was getting an error from my tests stating that the an empty namespace may not be set
, so I tried to print the value here and it is blank.
Setting it up exactly how you have suggest works beautifully, but again, I'm concerned about creating a new object every time I call the KubeOptions()
function
Tried this too and that doesn't work either.
var context_name, namespace string
func init() {
flag.StringVar(&context_name, "context-name", "deva-psdt", "The kube context name to use")
flag.StringVar(&namespace, "namespace", "", "The cluster namespace to test against")
}
var Kube_options = k8s.NewKubectlOptions(context_name, "", namespace)
I'll just stick with the function way you described. Thanks again!!
@Mo0rBy - yep this is what I meant by "'ll want to define it after the flags are parsed" - the flag.StringVar
statements simply define the flags. They are parsed later by Go's testing runtime. I really wouldn't worry about the cost of creating an object in Go (or any modern language for that matter) and would encourage you to measure the impact to allay your concerns!
With that said, you can memoize kubeOptions
:
package utils
import (
"flag"
"fmt"
"github.com/gruntwork-io/terratest/modules/k8s"
)
var context_name, namespace string
var kubeOptions *k8s.KubectlOptions //note this is not exported
func init() {
flag.StringVar(&context_name, "context-name", "my-kube-context", "The kube context name to use")
flag.StringVar(&namespace, "namespace", "", "The cluster namespace to test against")
func KubeOptions() *k8s.KubectlOptions {
if kubeOptions == nil {
kubeOptions = k8s.NewKubectlOptions(context_name, "", namespace)
}
return kubeOptions
}
I've done some more playing around and noticed that your first suggestion with the function KubeOptions()
seems to work the best, but it doesn't put the value into my expected
files.
app_name/
|-- config/
| |-- config.go
|-- expected/
| |-- deployments/ (KubeOptions values do not go in here correctly, I still get empty strings like before)
| | |-- app_service_A.go
| | |-- app_service_B.go
| | `-- app_service_C.go
| `-- services/
| |-- app_service_A.go
| |-- app_service_B.go
| `-- app_service_C.go
|-- deployments_test.go (KubeOptions values goes in here correctly, nice)
|-- services_test.go
|-- app_suite_test.go
Example of a var object inside a file in teh deployments
directory:
var service_A_expected_deployment_labels = map[string]string{
"app.kubernetes.io/managed-by": "Helm",
"helm.toolkit.fluxcd.io/name": service_A_service_name,
"helm.toolkit.fluxcd.io/namespace": utils.KubeOptions().Namespace, // This is an empty string, but it shouldn't be
}
The *k8s.KubectlOptions
has a Namespace
field.
Just tried this as well and got the same result:
var context_name, namespace string
func init() {
flag.StringVar(&context_name, "context-name", "my-kube-context", "The kube context name to use")
flag.StringVar(&namespace, "namespace", "", "The cluster namespace to test against")
}
func KubeOptions() *k8s.KubectlOptions {
return k8s.NewKubectlOptions(context_name, "", namespace)
}
func Namespace() string {
return namespace
}
var service_A_expected_deployment_labels = map[string]string{
"app.kubernetes.io/managed-by": "Helm",
"helm.toolkit.fluxcd.io/name": service_A_service_name,
"helm.toolkit.fluxcd.io/namespace": utils.Namespace(), // This is an empty string, but it shouldn't be
}
hey - i'd suggest reading the go docs on the init function, this StackOverflow post is useful too. Here's how things work:
init()
functions are calledThe problem you are running into is that you are trying to define a top-level variable that has access to the parsed flags. This is not possible as 3 and 4 run after 2. If you want to use variables like this you'll need something somewhat ugly like this:
var service_A_expected_deployment_labels map[string]string
func InitializeVariables() {
service_A_expected_deployment_labels = map[string]string{
"app.kubernetes.io/managed-by": "Helm",
"helm.toolkit.fluxcd.io/name": service_A_service_name,
"helm.toolkit.fluxcd.io/namespace": utils.Namespace(), // This is an empty string, but it shouldn't be
}
}
and then call InitializeVariables
yourself in a BeforeSuite
(i.e. after #4) or something like that.
Ah right ok, well I'll close this issue now as it's not really a Ginkgo thing, it's more about my misunderstanding or lack of knowledge about how GO and it's testing actually works. When I find a nice way that works, I'll still post it here, just to help anyone that finds it.
Thanks again fro your help @onsi and I really like the framework!
I want to set a flag when executing my tests from the CLI. The value of this flag is used within a
BeforeSuite()
and I also want to use it in a bunch of other files to create some variables that store my expected values. These values are then imported into a file that contains some specs.First, here is my directory structure:
I am doing some kubernetes testing, so I am creating an expected object (these object can be fairly large and complex) and then importing the expected object into my file containing the specs and using it for assertions against the actual object which I get in a
BeforeEach()
. Theexpected
directory is where I create an object. If we create a new microservice, we can just copy/paste one of these files and then change some values, so I believe this structure is good, but if not, please let me know.Here is my
app_suite_test.go
file where I use theinit()
function and theBeforeSuite()
:I want to import my
namespace
value OR mykube_options
value fromapp_suite_test.go
into the files within theexpected
directory. I have tried importing, but GO doesn't like it. I always get anundefined
error, and I believe this is because the files in theexpected
directory are deeper in the file tree thanapp_suite_test.go
and you can only import FROM files that are shallower in the file tree (because I can import the object I need fromexpected
intodeployments_test.go
with no problems). This is just a guess and I could be wrong.And here is an example of where I try to use this value to create an expected value within a file in my
expected
directory (there are many more variables that are created in this file and used to create 1 object that is imported into my specs. This is just 1 variable of about 15):I've tried using environment variables which results in code that looks like this:
Using environment variables does actually work (YAY)! However, when running
ginkgo -p -- --namespace="my_namespace"
, it doesn't seem to set the environment variable correctly, or something else is happening because the specs are using multiple cores or something? I really don't know.I would very much prefer to be able to use parallel testing. My test framework is very new and just getting started so it takes about 5 seconds to run all tests, but it will be growing at a very fast pace, so parallel testing is a must have for my case.