Repository: open-feature/go-sdk
Pull request #192 (2023-07-07 21:13:46)

feat: in-memory provider for sdk testing
## This PR

Is related to OFEP  https://github.com/open-feature/ofep/pull/71 and shows the feasibility of an in-memory provider

### Features 

- Provider is constructed with map of pre-defined flags [1]
- Supports context evaluations based on callbacks [2]

[1] - https://github.com/open-feature/ofep/pull/71
[2] - https://github.com/open-feature/go-sdk/pull/192/files#diff-1e56a5f9a72a9694aeb6f3510e8a2e02359c3cbdb9bd5c906219e30756d857efR181


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-feature/go-sdk/pull/192?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#192](https://app.codecov.io/gh/open-feature/go-sdk/pull/192?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (b303a57) into [main](https://app.codecov.io/gh/open-feature/go-sdk/commit/713a1021a8534208046cebcebf067575af465c17?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (713a102) will **decrease** coverage by `3.90%`.
> The diff coverage is `48.63%`.

> :exclamation: Current head b303a57 differs from pull request most recent head 7781013. Consider uploading reports for the commit 7781013 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #192      +/-   ##
==========================================
- Coverage   72.97%   69.07%   -3.90%     
==========================================
  Files           8        9       +1     
  Lines         766      912     +146     
==========================================
+ Hits          559      630      +71     
- Misses        187      255      +68     
- Partials       20       27       +7     
```


| [Impacted Files](https://app.codecov.io/gh/open-feature/go-sdk/pull/192?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [pkg/openfeature/testing/in\_memory\_provider.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/192?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL3Rlc3RpbmcvaW5fbWVtb3J5X3Byb3ZpZGVyLmdv) | `48.63% <48.63%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature)

Kavindu-Dodan: > Perhaps we should add the `e2e` folder to the Codecov ignore list.

For this PR, coverage complaint is on newly added in-memory providers. Some of the error paths are not covered with tests but IMO these are not essential (ex:- testing errors for each flag type)

Patch:
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index 2dcd55f..cc538df 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -44,8 +44,6 @@ jobs:
         uses: actions/checkout@v3
         with:
           submodules: recursive
-      - name: Run flagd-testbed
-        run: docker run -d -p 8013:8013 -v ${{ github.workspace }}/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:v0.2.2
       - name: Setup Environment
         run: |
           echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 70a3110..faf1107 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,16 +34,15 @@ Run unit tests with `make test`.
 
 #### End-to-End tests
 
-The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features) using the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd), [flagd](https://github.com/open-feature/flagd) and [the flagd test module](https://github.com/open-feature/go-sdk-contrib/tree/main/tests/flagd).
+The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features).
+
 If you'd like to run them locally, first pull the `test-harness` git submodule
+
 ```
 git submodule update --init --recursive
 ```
-then start the flagd testbed with 
-```
-docker run -p 8013:8013 -v $PWD/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:latest
-```
- and finally run
+
+and run tests with,
 ```
 make e2e-test
 ```
@@ -51,20 +50,20 @@ make e2e-test
 #### Fuzzing
 
 [Go supports fuzzing natively as of 1.18](https://go.dev/security/fuzz/).
-The fuzzing suite is implemented as an integration of `go-sdk` with the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd) and [flagd](https://github.com/open-feature/flagd).
-The fuzzing tests are found in [./integration/evaluation_fuzz_test.go](./integration/evaluation_fuzz_test.go), they are dependent on the flagd testbed running, you can start it with
-```
-docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest
-```
-then, to execute a fuzzing test, run the following
+The fuzzing suite is implemented as an integration of `go-sdk`.
+The fuzzing tests are found in [./integration/evaluation_fuzz_test.go](./e2e/evaluation_fuzz_test.go).
+
+
+To execute a fuzzing test, run the following
 ```
-go test -fuzz=FuzzBooleanEvaluation ./integration/evaluation_fuzz_test.go
+go test -fuzz=FuzzBooleanEvaluation ./e2e/evaluation_fuzz_test.go
 ```
 substituting the name of the fuzz as appropriate.
 
 ### Releases
 
-This repo uses Release Please to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to conventional commits, generated when PRs are merged. When Release Please's running PR is merged, any changed artifacts are published.
+This repo uses Release Please to release packages. Release Please set up a running PR that tracks all changes for the library components, and maintains the versions according to conventional commits, generated when PRs are merged.
+When Release Please PR is merged, any changed artifacts will be published.
 
 ## Contacting us
 
diff --git a/Makefile b/Makefile
index dbc0ee7..2d9aabf 100644
--- a/Makefile
+++ b/Makefile
@@ -4,10 +4,8 @@ mockgen:
 	mockgen -source=pkg/openfeature/hooks.go -destination=pkg/openfeature/hooks_mock_test.go -package=openfeature
 test:
 	go test --short -cover ./...
-e2e-test: # dependent on: docker run -p 8013:8013 -v $PWD/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:latest
-	go test -cover ./...
-	cd test-harness; git restore testing-flags.json; cd .. # reset testing-flags.json
-
+e2e-test:
+	go test -race -cover ./e2e/...
 lint:
 	go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
 	${GOPATH}/bin/golangci-lint run --deadline=3m --timeout=3m ./... # Run linters
diff --git a/pkg/openfeature/event_executor.go b/pkg/openfeature/event_executor.go
index 281c462..6db3d62 100644
--- a/pkg/openfeature/event_executor.go
+++ b/pkg/openfeature/event_executor.go
@@ -2,6 +2,7 @@ package openfeature
 
 import (
 	"fmt"
+	"reflect"
 	"sync"
 	"time"
 
@@ -260,7 +261,7 @@ func (e *eventExecutor) startListeningAndShutdownOld(newProvider providerReferen
 
 	// drop from active references
 	for i, r := range e.activeSubscriptions {
-		if r == oldReference {
+		if reflect.DeepEqual(oldReference.featureProvider, r.featureProvider) {
 			e.activeSubscriptions = append(e.activeSubscriptions[:i], e.activeSubscriptions[i+1:]...)
 		}
 	}
@@ -355,7 +356,7 @@ func (e *eventExecutor) executeHandler(f func(details EventDetails), event Event
 // isRunning is a helper till we bump to the latest go version with slices.contains support
 func isRunning(provider providerReference, activeProviders []providerReference) bool {
 	for _, activeProvider := range activeProviders {
-		if provider.featureProvider == activeProvider.featureProvider {
+		if reflect.DeepEqual(activeProvider.featureProvider, provider.featureProvider) {
 			return true
 		}
 	}
@@ -365,12 +366,12 @@ func isRunning(provider providerReference, activeProviders []providerReference)
 
 // isRunning is a helper to check if given provider is already in use
 func isBound(provider providerReference, defaultProvider providerReference, namedProviders []providerReference) bool {
-	if provider.featureProvider == defaultProvider.featureProvider {
+	if reflect.DeepEqual(provider.featureProvider, defaultProvider.featureProvider) {
 		return true
 	}
 
 	for _, namedProvider := range namedProviders {
-		if provider.featureProvider == namedProvider.featureProvider {
+		if reflect.DeepEqual(provider.featureProvider, namedProvider.featureProvider) {
 			return true
 		}
 	}
diff --git a/pkg/openfeature/memprovider/README.md b/pkg/openfeature/memprovider/README.md
new file mode 100644
index 0000000..8751e84
--- /dev/null
+++ b/pkg/openfeature/memprovider/README.md
@@ -0,0 +1,5 @@
+# In-memory provider
+
+`InMemoryProvider` is an OpenFeature compliant provider implementation with an in-memory flag storage. 
+
+While the main usage of this provider is SDK testing, you may use it for minimal OpenFeature use cases where appropriate.
\ No newline at end of file
diff --git a/pkg/openfeature/memprovider/in_memory_provider.go b/pkg/openfeature/memprovider/in_memory_provider.go
new file mode 100644
index 0000000..81bcf5b
--- /dev/null
+++ b/pkg/openfeature/memprovider/in_memory_provider.go
@@ -0,0 +1,200 @@
+package memprovider
+
+import (
+	"context"
+	"fmt"
+	"github.com/open-feature/go-sdk/pkg/openfeature"
+)
+
+const (
+	Enabled  State = "ENABLED"
+	Disabled State = "DISABLED"
+)
+
+type InMemoryProvider struct {
+	flags map[string]InMemoryFlag
+}
+
+func NewInMemoryProvider(from map[string]InMemoryFlag) InMemoryProvider {
+	return InMemoryProvider{
+		flags: from,
+	}
+}
+
+func (i InMemoryProvider) Metadata() openfeature.Metadata {
+	return openfeature.Metadata{
+		Name: "InMemoryProvider",
+	}
+}
+
+func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
+	memoryFlag, details, ok := i.find(flag)
+	if !ok {
+		return openfeature.BoolResolutionDetail{
+			Value:                    defaultValue,
+			ProviderResolutionDetail: *details,
+		}
+	}
+
+	resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx)
+	result := genericResolve[bool](resolveFlag, defaultValue, &detail)
+
+	return openfeature.BoolResolutionDetail{
+		Value:                    result,
+		ProviderResolutionDetail: detail,
+	}
+}
+
+func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
+	memoryFlag, details, ok := i.find(flag)
+	if !ok {
+		return openfeature.StringResolutionDetail{
+			Value:                    defaultValue,
+			ProviderResolutionDetail: *details,
+		}
+	}
+
+	resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx)
+	result := genericResolve[string](resolveFlag, defaultValue, &detail)
+
+	return openfeature.StringResolutionDetail{
+		Value:                    result,
+		ProviderResolutionDetail: detail,
+	}
+}
+
+func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
+	memoryFlag, details, ok := i.find(flag)
+	if !ok {
+		return openfeature.FloatResolutionDetail{
+			Value:                    defaultValue,
+			ProviderResolutionDetail: *details,
+		}
+	}
+
+	resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx)
+	result := genericResolve[float64](resolveFlag, defaultValue, &detail)
+
+	return openfeature.FloatResolutionDetail{
+		Value:                    result,
+		ProviderResolutionDetail: detail,
+	}
+}
+
+func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
+	memoryFlag, details, ok := i.find(flag)
+	if !ok {
+		return openfeature.IntResolutionDetail{
+			Value:                    defaultValue,
+			ProviderResolutionDetail: *details,
+		}
+	}
+
+	resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx)
+	result := genericResolve[int](resolveFlag, int(defaultValue), &detail)
+
+	return openfeature.IntResolutionDetail{
+		Value:                    int64(result),
+		ProviderResolutionDetail: detail,
+	}
+}
+
+func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
+	memoryFlag, details, ok := i.find(flag)
+	if !ok {
+		return openfeature.InterfaceResolutionDetail{
+			Value:                    defaultValue,
+			ProviderResolutionDetail: *details,
+		}
+	}
+
+	resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx)
+
+	var result interface{}
+	if resolveFlag != nil {
+		result = resolveFlag
+	} else {
+		result = defaultValue
+		detail.Reason = openfeature.ErrorReason
+		detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association")
+	}
+
+	return openfeature.InterfaceResolutionDetail{
+		Value:                    result,
+		ProviderResolutionDetail: detail,
+	}
+}
+
+func (i InMemoryProvider) Hooks() []openfeature.Hook {
+	return []openfeature.Hook{}
+}
+
+func (i InMemoryProvider) find(flag string) (*InMemoryFlag, *openfeature.ProviderResolutionDetail, bool) {
+	memoryFlag, ok := i.flags[flag]
+	if !ok {
+		return nil,
+			&openfeature.ProviderResolutionDetail{
+				ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)),
+				Reason:          openfeature.ErrorReason,
+			}, false
+	}
+
+	return &memoryFlag, nil, true
+}
+
+// helpers
+
+// genericResolve is a helper to extract type verified evaluation and fill openfeature.ProviderResolutionDetail
+func genericResolve[T comparable](value interface{}, defaultValue T, detail *openfeature.ProviderResolutionDetail) T {
+	v, ok := value.(T)
+
+	if ok {
+		return v
+	}
+
+	detail.Reason = openfeature.ErrorReason
+	detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association")
+	return defaultValue
+}
+
+// Type Definitions for InMemoryProvider flag
+
+// State of the feature flag
+type State string
+
+// ContextEvaluator is a callback to perform openfeature.EvaluationContext backed evaluations.
+// This is a callback implemented by the flag definer.
+type ContextEvaluator *func(this InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail)
+
+// InMemoryFlag is the feature flag representation accepted by InMemoryProvider
+type InMemoryFlag struct {
+	Key              string
+	State            State
+	DefaultVariant   string
+	Variants         map[string]interface{}
+	ContextEvaluator ContextEvaluator
+}
+
+func (flag *InMemoryFlag) Resolve(defaultValue interface{}, evalCtx openfeature.FlattenedContext) (
+	interface{}, openfeature.ProviderResolutionDetail) {
+
+	// check the state
+	if flag.State == Disabled {
+		return defaultValue, openfeature.ProviderResolutionDetail{
+			ResolutionError: openfeature.NewGeneralResolutionError("flag is disabled"),
+			Reason:          openfeature.DisabledReason,
+		}
+	}
+
+	// first resolve from context callback
+	if flag.ContextEvaluator != nil {
+		return (*flag.ContextEvaluator)(*flag, evalCtx)
+	}
+
+	// fallback to evaluation
+
+	return flag.Variants[flag.DefaultVariant], openfeature.ProviderResolutionDetail{
+		Reason:  openfeature.StaticReason,
+		Variant: flag.DefaultVariant,
+	}
+}


Test patch:
diff --git a/e2e/README.md b/e2e/README.md
new file mode 100644
index 0000000..8662331
--- /dev/null
+++ b/e2e/README.md
@@ -0,0 +1,5 @@
+## end-to-end tests
+
+This folder contains e2e tests for go-sdk. Tests written here rely on `InMemoryProvider` to perform flag evaluations.
+Some tests require `test-harness` Git submodule and use behaviour driven tests defined with [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax.
+
diff --git a/e2e/caching_test.go b/e2e/caching_test.go
deleted file mode 100644
index ba80c17..0000000
--- a/e2e/caching_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package e2e_test
-
-import (
-	"testing"
-
-	"github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration"
-
-	"github.com/cucumber/godog"
-)
-
-const flagConfigurationPath = "../test-harness/testing-flags.json"
-
-func TestCaching(t *testing.T) {
-	if testing.Short() {
-		t.Skip()
-	}
-
-	initializeCachingScenario, err := integration.InitializeCachingScenario(flagConfigurationPath)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	suite := godog.TestSuite{
-		Name:                "caching.feature",
-		ScenarioInitializer: initializeCachingScenario,
-		Options: &godog.Options{
-			Format:   "pretty",
-			Paths:    []string{"../test-harness/features/caching.feature"},
-			TestingT: t, // Testing instance that will run subtests.
-		},
-	}
-
-	if suite.Run() != 0 {
-		t.Fatal("non-zero status returned, failed to run caching tests")
-	}
-}
diff --git a/e2e/common_test.go b/e2e/common_test.go
new file mode 100644
index 0000000..60875d2
--- /dev/null
+++ b/e2e/common_test.go
@@ -0,0 +1,112 @@
+package e2e_test
+
+import (
+	"github.com/open-feature/go-sdk/pkg/openfeature"
+	"github.com/open-feature/go-sdk/pkg/openfeature/memprovider"
+)
+
+// ctxFunction is a context based evaluation callback
+var ctxFunction = func(this memprovider.InMemoryFlag, evalCtx openfeature.FlattenedContext) (
+	interface{}, openfeature.ProviderResolutionDetail) {
+
+	defaultValue := this.Variants[this.DefaultVariant]
+	defaultResolution := openfeature.ProviderResolutionDetail{
+		Reason:  openfeature.DefaultReason,
+		Variant: this.DefaultVariant,
+	}
+
+	expects := openfeature.FlattenedContext{
+		"fn":       "Sulisław",
+		"ln":       "Świętopełk",
+		"age":      int64(29),
+		"customer": false,
+	}
+
+	for k, v := range expects {
+		if v != evalCtx[k] {
+			return defaultValue, defaultResolution
+		}
+	}
+
+	return this.Variants["internal"], openfeature.ProviderResolutionDetail{
+		Reason:  openfeature.TargetingMatchReason,
+		Variant: "internal",
+	}
+}
+
+var memoryFlags = map[string]memprovider.InMemoryFlag{
+	"boolean-flag": {
+		Key:            "boolean-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "on",
+		Variants: map[string]interface{}{
+			"on":  true,
+			"off": false,
+		},
+		ContextEvaluator: nil,
+	},
+	"string-flag": {
+		Key:            "string-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "greeting",
+		Variants: map[string]interface{}{
+			"greeting": "hi",
+			"parting":  "bye",
+		},
+		ContextEvaluator: nil,
+	},
+	"integer-flag": {
+		Key:            "integer-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "ten",
+		Variants: map[string]interface{}{
+			"one": 1,
+			"ten": 10,
+		},
+		ContextEvaluator: nil,
+	},
+	"float-flag": {
+		Key:            "float-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "half",
+		Variants: map[string]interface{}{
+			"tenth": 0.1,
+			"half":  0.5,
+		},
+		ContextEvaluator: nil,
+	},
+	"object-flag": {
+		Key:            "object-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "template",
+		Variants: map[string]interface{}{
+			"empty": map[string]interface{}{},
+			"template": map[string]interface{}{
+				"showImages":    true,
+				"title":         "Check out these pics!",
+				"imagesPerPage": 100,
+			},
+		},
+		ContextEvaluator: nil,
+	},
+	"wrong-flag": {
+		Key:            "wrong-flag",
+		State:          memprovider.Enabled,
+		DefaultVariant: "one",
+		Variants: map[string]interface{}{
+			"one": "uno",
+			"two": "dos",
+		},
+		ContextEvaluator: nil,
+	},
+	"context-aware": {
+		Key:            "context-aware",
+		State:          memprovider.Enabled,
+		DefaultVariant: "external",
+		Variants: map[string]interface{}{
+			"internal": "INTERNAL",
+			"external": "EXTERNAL",
+		},
+		ContextEvaluator: &ctxFunction,
+	},
+}
diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go
index d7f3c91..c6a199d 100644
--- a/e2e/evaluation_fuzz_test.go
+++ b/e2e/evaluation_fuzz_test.go
@@ -2,29 +2,21 @@ package e2e_test
 
 import (
 	"context"
+	"github.com/open-feature/go-sdk/pkg/openfeature"
+	"github.com/open-feature/go-sdk/pkg/openfeature/memprovider"
 	"strings"
 	"testing"
-	"time"
-
-	flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg"
-	"github.com/open-feature/go-sdk/pkg/openfeature"
 )
 
 func setupFuzzClient(f *testing.F) *openfeature.Client {
 	f.Helper()
 
-	provider := flagd.NewProvider(flagd.WithPort(8013), flagd.WithoutCache())
-	err := openfeature.SetProvider(provider)
+	memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{})
+	err := openfeature.SetProvider(memoryProvider)
 	if err != nil {
 		f.Errorf("error setting up provider %v", err)
 	}
 
-	select {
-	case <-provider.IsReady():
-	case <-time.After(500 * time.Millisecond):
-		f.Fatal("provider not ready after 500 milliseconds")
-	}
-
 	return openfeature.NewClient("fuzzing")
 }
 
diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go
index 26b3254..99c38b8 100644
--- a/e2e/evaluation_test.go
+++ b/e2e/evaluation_test.go
@@ -1,12 +1,21 @@
 package e2e_test
 
 import (
-	"testing"
-
+	"context"
+	"errors"
+	"fmt"
 	"github.com/cucumber/godog"
-	e2e "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration"
+	"github.com/open-feature/go-sdk/pkg/openfeature"
+	"github.com/open-feature/go-sdk/pkg/openfeature/memprovider"
+	"strconv"
+	"testing"
 )
 
+var client = openfeature.NewClient("evaluation tests")
+
+// ctxStorageKey is the key used to pass test data across context.Context
+type ctxStorageKey struct{}
+
 func TestEvaluation(t *testing.T) {
 	if testing.Short() {
 		t.Skip()
@@ -14,7 +23,7 @@ func TestEvaluation(t *testing.T) {
 
 	suite := godog.TestSuite{
 		Name:                "evaluation.feature",
-		ScenarioInitializer: e2e.InitializeEvaluationScenario(),
+		ScenarioInitializer: initializeEvaluationScenario,
 		Options: &godog.Options{
 			Format:   "pretty",
 			Paths:    []string{"../test-harness/features/evaluation.feature"},
@@ -26,3 +35,755 @@ func TestEvaluation(t *testing.T) {
 		t.Fatal("non-zero status returned, failed to run evaluation tests")
 	}
 }
+
+func initializeEvaluationScenario(ctx *godog.ScenarioContext) {
+	// setup provider
+	ctx.Step(`^a provider is registered with cache disabled$`, aProviderIsRegisteredWithCacheDisabled)
+
+	// basic evaluations
+	ctx.Step(`^a boolean flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aBooleanFlagWithKeyIsEvaluatedWithDefaultValue)
+	ctx.Step(`^the resolved boolean value should be "([^"]*)"$`, theResolvedBooleanValueShouldBe)
+
+	ctx.Step(`^a string flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aStringFlagWithKeyIsEvaluatedWithDefaultValue)
+	ctx.Step(`^the resolved string value should be "([^"]*)"$`, theResolvedStringValueShouldBe)
+
+	ctx.Step(`^an integer flag with key "([^"]*)" is evaluated with default value (\d+)$`, anIntegerFlagWithKeyIsEvaluatedWithDefaultValue)
+	ctx.Step(`^the resolved integer value should be (\d+)$`, theResolvedIntegerValueShouldBe)
+
+	ctx.Step(`^a float flag with key "([^"]*)" is evaluated with default value (\-*\d+\.\d+)$`, aFloatFlagWithKeyIsEvaluatedWithDefaultValue)
+	ctx.Step(`^the resolved float value should be (\-*\d+\.\d+)$`, theResolvedFloatValueShouldBe)
+
+	ctx.Step(`^an object flag with key "([^"]*)" is evaluated with a null default value$`, anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue)
+	ctx.Step(`^the resolved object value should be contain fields "([^"]*)", "([^"]*)", and "([^"]*)", with values "([^"]*)", "([^"]*)" and (\d+), respectively$`, theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespectively)
+
+	// evaluation with variant and reason validation
+	ctx.Step(`^a boolean flag with key "([^"]*)" is evaluated with details and default value "([^"]*)"$`, aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue)
+	ctx.Step(`^the resolved boolean details value should be "([^"]*)", the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe)
+
+	ctx.Step(`^a string flag with key "([^"]*)" is evaluated with details and default value "([^"]*)"$`, aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue)
+	ctx.Step(`^the resolved string details value should be "([^"]*)", the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe)
+
+	ctx.Step(`^an integer flag with key "([^"]*)" is evaluated with details and default value (\d+)$`, anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue)
+	ctx.Step(`^the resolved integer details value should be (\d+), the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe)
+
+	ctx.Step(`^a float flag with key "([^"]*)" is evaluated with details and default value (\-*\d+\.\d+)$`, aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue)
+	ctx.Step(`^the resolved float details value should be (\-*\d+\.\d+), the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe)
+
+	ctx.Step(`^an object flag with key "([^"]*)" is evaluated with details and a null default value$`, anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue)
+	ctx.Step(`^the resolved object details value should be contain fields "([^"]*)", "([^"]*)", and "([^"]*)", with values "([^"]*)", "([^"]*)" and (\d+), respectively$`, theResolvedObjectDetailsValueShouldBeContainFieldsAndWithValuesAndRespectively)
+	ctx.Step(`^the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theVariantShouldBeAndTheReasonShouldBe)
+
+	// context based evaluation
+
+	ctx.Step(`^context contains keys "([^"]*)", "([^"]*)", "([^"]*)", "([^"]*)" with values "([^"]*)", "([^"]*)", (\d+), "([^"]*)"$`, contextContainsKeysWithValues)
+	ctx.Step(`^a flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aFlagWithKeyIsEvaluatedWithDefaultValue)
+	ctx.Step(`^the resolved string response should be "([^"]*)"$`, theResolvedStringResponseShouldBe)
+	ctx.Step(`^the resolved flag value is "([^"]*)" when the context is empty$`, theResolvedFlagValueIsWhenTheContextIsEmpty)
+
+	// scenarios
+
+	ctx.Step(`^a non-existent string flag with key "([^"]*)" is evaluated with details and a default value "([^"]*)"$`, aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue)
+	ctx.Step(`^the default string value should be returned$`, theDefaultStringValueShouldBeReturned)
+	ctx.Step(`^the reason should indicate an error and the error code should indicate a missing flag with "([^"]*)"$`, theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith)
+
+	ctx.Step(`^a string flag with key "([^"]*)" is evaluated as an integer, with details and a default value (\d+)$`, aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue)
+	ctx.Step(`^the default integer value should be returned$`, theDefaultIntegerValueShouldBeReturned)
+	ctx.Step(`^the reason should indicate an error and the error code should indicate a type mismatch with "([^"]*)"$`, theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateATypeMismatchWith)
+}
+
+func aProviderIsRegisteredWithCacheDisabled(ctx context.Context) error {
+	memoryProvider := memprovider.NewInMemoryProvider(memoryFlags)
+
+	err := openfeature.SetProvider(memoryProvider)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func aBooleanFlagWithKeyIsEvaluatedWithDefaultValue(
+	ctx context.Context, flagKey, defaultValueStr string,
+) (context.Context, error) {
+	defaultValue, err := strconv.ParseBool(defaultValueStr)
+	if err != nil {
+		return ctx, errors.New("default value must be of type bool")
+	}
+
+	got, err := client.BooleanValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, got), nil
+}
+
+func theResolvedBooleanValueShouldBe(ctx context.Context, expectedValueStr string) error {
+	expectedValue, err := strconv.ParseBool(expectedValueStr)
+	if err != nil {
+		return errors.New("expected value must be of type bool")
+	}
+
+	got, ok := ctx.Value(ctxStorageKey{}).(bool)
+	if !ok {
+		return errors.New("no flag resolution result")
+	}
+
+	if got != expectedValue {
+		return fmt.Errorf("expected resolved boolean value to be %t, got %t", expectedValue, got)
+	}
+
+	return nil
+}
+
+func aStringFlagWithKeyIsEvaluatedWithDefaultValue(
+	ctx context.Context, flagKey, defaultValue string,
+) (context.Context, error) {
+	got, err := client.StringValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, got), nil
+}
+
+func theResolvedStringValueShouldBe(ctx context.Context, expectedValue string) error {
+	got, ok := ctx.Value(ctxStorageKey{}).(string)
+	if !ok {
+		return errors.New("no flag resolution result")
+	}
+
+	if got != expectedValue {
+		return fmt.Errorf("expected resolved string value to be %s, got %s", expectedValue, got)
+	}
+
+	return nil
+}
+
+func anIntegerFlagWithKeyIsEvaluatedWithDefaultValue(
+	ctx context.Context, flagKey string, defaultValue int64,
+) (context.Context, error) {
+	got, err := client.IntValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, got), nil
+}
+
+func theResolvedIntegerValueShouldBe(ctx context.Context, expectedValue int64) error {
+	got, ok := ctx.Value(ctxStorageKey{}).(int64)
+	if !ok {
+		return errors.New("no flag resolution result")
+	}
+
+	if got != expectedValue {
+		return fmt.Errorf("expected resolved int value to be %d, got %d", expectedValue, got)
+	}
+
+	return nil
+}
+
+func aFloatFlagWithKeyIsEvaluatedWithDefaultValue(
+	ctx context.Context, flagKey string, defaultValue float64,
+) (context.Context, error) {
+	got, err := client.FloatValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, got), nil
+}
+
+func theResolvedFloatValueShouldBe(ctx context.Context, expectedValue float64) error {
+	got, ok := ctx.Value(ctxStorageKey{}).(float64)
+	if !ok {
+		return errors.New("no flag resolution result")
+	}
+
+	if got != expectedValue {
+		return fmt.Errorf("expected resolved int value to be %f, got %f", expectedValue, got)
+	}
+
+	return nil
+}
+
+func anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(ctx context.Context, flagKey string) (context.Context, error) {
+	got, err := client.ObjectValue(ctx, flagKey, nil, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, got), nil
+}
+
+func theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespectively(
+	ctx context.Context, field1, field2, field3, value1, value2 string, value3 int) error {
+
+	got, ok := ctx.Value(ctxStorageKey{}).(map[string]interface{})
+	if !ok {
+		return errors.New("no flag resolution result")
+	}
+
+	// field1 - showImages
+	showImage, ok := got[field1].(bool)
+	if !ok {
+		return fmt.Errorf("expected boolean for key %s", field1)
+	}
+
+	parseBool, err := strconv.ParseBool(value1)
+	if err != nil {
+		return err
+	}
+
+	if showImage != parseBool {
+		return fmt.Errorf("incorrect comparison for boolean")
+	}
+
+	// field2 - title
+	title, ok := got[field2].(string)
+	if !ok {
+		return fmt.Errorf("expected string for key %s", field2)
+	}
+
+	if title != value2 {
+		return fmt.Errorf("incorrect comparison for string")
+	}
+
+	// field3 - imagesPerPage
+	imagesPerPage, ok := got[field3].(int)
+	if !ok {
+		return fmt.Errorf("expected int for key %s", field3)
+	}
+
+	if imagesPerPage != value3 {
+		return fmt.Errorf("incorrect comparison for int")
+	}
+
+	return nil
+}
+
+func aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(
+	ctx context.Context, flagKey string, defaultValueStr string,
+) (context.Context, error) {
+	defaultValue, err := strconv.ParseBool(defaultValueStr)
+	if err != nil {
+		return ctx, errors.New("default value must be of type bool")
+	}
+
+	got, err := client.BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.BooleanEvaluationDetails)
+	if !ok {
+		store = make(map[string]openfeature.BooleanEvaluationDetails)
+	}
+
+	store[flagKey] = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, store), nil
+}
+
+func theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(
+	ctx context.Context, valueStr, variant, reason string,
+) error {
+	value, err := strconv.ParseBool(valueStr)
+	if err != nil {
+		return errors.New("value must be of type bool")
+	}
+
+	got, err := getFirstBooleanEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	if got.Value != value {
+		return fmt.Errorf("expected value to be %t, got %t", value, got.Value)
+	}
+	if got.Variant != variant {
+		return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant)
+	}
+	if string(got.Reason) != reason {
+		return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason)
+	}
+
+	return nil
+}
+
+func aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(
+	ctx context.Context, flagKey, defaultValue string,
+) (context.Context, error) {
+	got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.StringEvaluationDetails)
+	if !ok {
+		store = make(map[string]openfeature.StringEvaluationDetails)
+	}
+
+	store[flagKey] = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, store), nil
+}
+
+func theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(
+	ctx context.Context, value, variant, reason string,
+) error {
+	got, err := getFirstStringEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	if got.Value != value {
+		return fmt.Errorf("expected value to be %s, got %s", value, got.Value)
+	}
+	if got.Variant != variant {
+		return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant)
+	}
+	if string(got.Reason) != reason {
+		return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason)
+	}
+
+	return nil
+}
+
+func anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(
+	ctx context.Context, flagKey string, defaultValue int64,
+) (context.Context, error) {
+	got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.IntEvaluationDetails)
+	if !ok {
+		store = make(map[string]openfeature.IntEvaluationDetails)
+	}
+
+	store[flagKey] = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, store), nil
+}
+
+func theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(
+	ctx context.Context, value int64, variant, reason string,
+) error {
+	got, err := getFirstIntegerEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	if got.Value != value {
+		return fmt.Errorf("expected value to be %d, got %d", value, got.Value)
+	}
+	if got.Variant != variant {
+		return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant)
+	}
+	if string(got.Reason) != reason {
+		return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason)
+	}
+
+	return nil
+}
+
+func aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(
+	ctx context.Context, flagKey string, defaultValue float64,
+) (context.Context, error) {
+	got, err := client.FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.FloatEvaluationDetails)
+	if !ok {
+		store = make(map[string]openfeature.FloatEvaluationDetails)
+	}
+
+	store[flagKey] = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, store), nil
+}
+
+func theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(
+	ctx context.Context, value float64, variant, reason string,
+) error {
+	got, err := getFirstFloatEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	if got.Value != value {
+		return fmt.Errorf("expected value to be %f, got %f", value, got.Value)
+	}
+	if got.Variant != variant {
+		return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant)
+	}
+	if string(got.Reason) != reason {
+		return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason)
+	}
+
+	return nil
+}
+
+func anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue(
+	ctx context.Context, flagKey string,
+) (context.Context, error) {
+	got, err := client.ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{})
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails)
+	if !ok {
+		store = make(map[string]openfeature.InterfaceEvaluationDetails)
+	}
+
+	store[flagKey] = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, store), nil
+}
+
+func theResolvedObjectDetailsValueShouldBeContainFieldsAndWithValuesAndRespectively(
+	ctx context.Context, field1, field2, field3, value1, value2 string, value3 int) error {
+
+	gotResDetail, err := getFirstInterfaceEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	content, ok := gotResDetail.Value.(map[string]interface{})
+	if !ok {
+		return errors.New("unexpected value format")
+	}
+
+	// field1 - showImages
+	showImage, ok := content[field1].(bool)
+	if !ok {
+		return fmt.Errorf("expected boolean for key %s", field1)
+	}
+
+	parseBool, err := strconv.ParseBool(value1)
+	if err != nil {
+		return err
+	}
+
+	if showImage != parseBool {
+		return fmt.Errorf("incorrect comparison for boolean")
+	}
+
+	// field2 - title
+	title, ok := content[field2].(string)
+	if !ok {
+		return fmt.Errorf("expected string for key %s", field2)
+	}
+
+	if title != value2 {
+		return fmt.Errorf("incorrect comparison for string")
+	}
+
+	// field3 - imagesPerPage
+	imagesPerPage, ok := content[field3].(int)
+	if !ok {
+		return fmt.Errorf("expected int for key %s", field3)
+	}
+
+	if imagesPerPage != value3 {
+		return fmt.Errorf("incorrect comparison for int")
+	}
+
+	return nil
+}
+
+func theVariantShouldBeAndTheReasonShouldBe(ctx context.Context, variant, reason string) error {
+	got, err := getFirstInterfaceEvaluationDetails(ctx)
+	if err != nil {
+		return err
+	}
+
+	if got.Variant != variant {
+		return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant)
+	}
+	if string(got.Reason) != reason {
+		return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason)
+	}
+
+	return nil
+}
+
+func contextContainsKeysWithValues(
+	ctx context.Context, ctxKey1, ctxKey2, ctxKey3, ctxKey4, ctxValue1, ctxValue2 string, ctxValue3 int64, ctxValue4 string,
+) (context.Context, error) {
+	evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{
+		ctxKey1: boolOrString(ctxValue1),
+		ctxKey2: boolOrString(ctxValue2),
+		ctxKey3: ctxValue3,
+		ctxKey4: boolOrString(ctxValue4),
+	})
+
+	data := contextAwareEvaluationData{
+		evaluationContext: evalCtx,
+	}
+
+	return context.WithValue(ctx, ctxStorageKey{}, data), nil
+}
+
+func aFlagWithKeyIsEvaluatedWithDefaultValue(
+	ctx context.Context, flagKey, defaultValue string,
+) (context.Context, error) {
+	ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData)
+	if !ok {
+		return ctx, errors.New("no contextAwareEvaluationData found")
+	}
+
+	got, err := client.StringValue(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext)
+	if err != nil {
+		return ctx, fmt.Errorf("openfeature client: %w", err)
+	}
+	ctxAwareEvalData.flagKey = flagKey
+	ctxAwareEvalData.defaultValue = defaultValue
+	ctxAwareEvalData.response = got
+
+	return context.WithValue(ctx, ctxStorageKey{}, ctxAwareEvalData), nil
+}
+
+func theResolvedStringResponseShouldBe(ctx context.Context, expectedResponse string) (context.Context, error) {
+	ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData)
+	if !ok {
+		return ctx, errors.New("no contextAwareEvaluationData found")
+	}
+
+	if ctxAwareEvalData.response != expectedResponse {
+		return ctx, fmt.Errorf("expected response of '%s', got '%s'", expectedResponse, ctxAwareEvalData.response)
+	}
+
+	return ctx, nil
+}
+
+func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedResponse string) error {
+	ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData)
+	if !ok {
+		return errors.New("no contextAwareEvaluationData found")
+	}
+
+	got, err := client.StringValue(
+		ctx, ctxAwareEvalData.flagKey, ctxAwareEvalData.defaultValue, openfeature.EvaluationContext{},
+	)
+	if err != nil {
+		return fmt.Errorf("openfeature client: %w", err)
+	}
+
+	if got != expectedResponse {
+		return fmt.Errorf("expected response of '%s', got '%s'", expectedResponse, got)
+	}
+
+	return nil
+}
+
+func aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue(
+	ctx context.Context, flagKey, defaultValue string,
+) (context.Context, error) {
+	got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+
+	return context.WithValue(ctx, ctxStorageKey{}, stringFlagNotFoundData{
+		evalDetails:  got,
+		defaultValue: defaultValue,
+		err:          err,
+	}), nil
+}
+
+func theDefaultStringValueShouldBeReturned(ctx context.Context) (context.Context, error) {
+	strNotFoundData, ok := ctx.Value(ctxStorageKey{}).(stringFlagNotFoundData)
+	if !ok {
+		return ctx, errors.New("no stringFlagNotFoundData found")
+	}
+
+	if strNotFoundData.evalDetails.Value != strNotFoundData.defaultValue {
+		return ctx, fmt.Errorf(
+			"expected default value '%s', got '%s'",
+			strNotFoundData.defaultValue, strNotFoundData.evalDetails.Value,
+		)
+	}
+
+	return ctx, nil
+}
+
+func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith(
+	ctx context.Context, errorCode string,
+) error {
+	strNotFoundData, ok := ctx.Value(ctxStorageKey{}).(stringFlagNotFoundData)
+	if !ok {
+		return errors.New("no stringFlagNotFoundData found")
+	}
+
+	if strNotFoundData.evalDetails.Reason != openfeature.ErrorReason {
+		return fmt.Errorf(
+			"expected reason '%s', got '%s'",
+			openfeature.ErrorReason, strNotFoundData.evalDetails.Reason,
+		)
+	}
+
+	if string(strNotFoundData.evalDetails.ErrorCode) != errorCode {
+		return fmt.Errorf(
+			"expected error code '%s', got '%s'",
+			errorCode, strNotFoundData.evalDetails.ErrorCode,
+		)
+	}
+
+	if strNotFoundData.err == nil {
+		return errors.New("expected flag evaluation to return an error, got nil")
+	}
+
+	return nil
+}
+
+func aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue(
+	ctx context.Context, flagKey string, defaultValue int64,
+) (context.Context, error) {
+	got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{})
+
+	return context.WithValue(ctx, ctxStorageKey{}, typeErrorData{
+		evalDetails:  got,
+		defaultValue: defaultValue,
+		err:          err,
+	}), nil
+}
+
+func theDefaultIntegerValueShouldBeReturned(ctx context.Context) (context.Context, error) {
+	typeErrData, ok := ctx.Value(ctxStorageKey{}).(typeErrorData)
+	if !ok {
+		return ctx, errors.New("no typeErrorData found")
+	}
+
+	if typeErrData.evalDetails.Value != typeErrData.defaultValue {
+		return ctx, fmt.Errorf(
+			"expected default value %d, got %d",
+			typeErrData.defaultValue, typeErrData.evalDetails.Value,
+		)
+	}
+
+	return ctx, nil
+}
+
+func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateATypeMismatchWith(
+	ctx context.Context, expectedErrorCode string,
+) error {
+	typeErrData, ok := ctx.Value(ctxStorageKey{}).(typeErrorData)
+	if !ok {
+		return errors.New("no typeErrorData found")
+	}
+
+	if typeErrData.evalDetails.Reason != openfeature.ErrorReason {
+		return fmt.Errorf(
+			"expected reason '%s', got '%s'",
+			openfeature.ErrorReason, typeErrData.evalDetails.Reason,
+		)
+	}
+
+	if typeErrData.evalDetails.ErrorCode != openfeature.TypeMismatchCode {
+		return fmt.Errorf(
+			"expected error code '%s', got '%s'",
+			openfeature.TypeMismatchCode, typeErrData.evalDetails.ErrorCode,
+		)
+	}
+
+	return nil
+}
+
+// helpers
+
+type contextAwareEvaluationData struct {
+	flagKey           string
+	defaultValue      string
+	evaluationContext openfeature.EvaluationContext
+	response          string
+}
+
+type stringFlagNotFoundData struct {
+	evalDetails  openfeature.StringEvaluationDetails
+	defaultValue string
+	err          error
+}
+
+type typeErrorData struct {
+	evalDetails  openfeature.IntEvaluationDetails
+	defaultValue int64
+	err          error
+}
+
+func getFirstBooleanEvaluationDetails(ctx context.Context) (openfeature.BooleanEvaluationDetails, error) {
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.BooleanEvaluationDetails)
+	if !ok {
+		return openfeature.BooleanEvaluationDetails{}, errors.New("no flag resolution result")
+	}
+
+	for _, evalDetails := range store {
+		return evalDetails, nil
+	}
+
+	return openfeature.BooleanEvaluationDetails{}, errors.New("no evaluation detail found in context")
+}
+
+func getFirstStringEvaluationDetails(ctx context.Context) (openfeature.StringEvaluationDetails, error) {
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.StringEvaluationDetails)
+	if !ok {
+		return openfeature.StringEvaluationDetails{}, errors.New("no flag resolution result")
+	}
+
+	for _, evalDetails := range store {
+		return evalDetails, nil
+	}
+
+	return openfeature.StringEvaluationDetails{}, errors.New("no evaluation detail found in context")
+}
+
+func getFirstIntegerEvaluationDetails(ctx context.Context) (openfeature.IntEvaluationDetails, error) {
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.IntEvaluationDetails)
+	if !ok {
+		return openfeature.IntEvaluationDetails{}, errors.New("no flag resolution result")
+	}
+
+	for _, evalDetails := range store {
+		return evalDetails, nil
+	}
+
+	return openfeature.IntEvaluationDetails{}, errors.New("no evaluation detail found in context")
+}
+
+func getFirstFloatEvaluationDetails(ctx context.Context) (openfeature.FloatEvaluationDetails, error) {
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.FloatEvaluationDetails)
+	if !ok {
+		return openfeature.FloatEvaluationDetails{}, errors.New("no flag resolution result")
+	}
+
+	for _, evalDetails := range store {
+		return evalDetails, nil
+	}
+
+	return openfeature.FloatEvaluationDetails{}, errors.New("no evaluation detail found in context")
+}
+
+func getFirstInterfaceEvaluationDetails(ctx context.Context) (openfeature.InterfaceEvaluationDetails, error) {
+	store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails)
+	if !ok {
+		return openfeature.InterfaceEvaluationDetails{}, errors.New("no flag resolution result")
+	}
+
+	for _, evalDetails := range store {
+		return evalDetails, nil
+	}
+
+	return openfeature.InterfaceEvaluationDetails{}, errors.New("no evaluation detail found in context")
+}
+
+func boolOrString(str string) interface{} {
+	boolean, err := strconv.ParseBool(str)
+	if err != nil {
+		return str
+	}
+
+	return boolean
+}
diff --git a/pkg/openfeature/memprovider/in_memory_provider_test.go b/pkg/openfeature/memprovider/in_memory_provider_test.go
new file mode 100644
index 0000000..48224d0
--- /dev/null
+++ b/pkg/openfeature/memprovider/in_memory_provider_test.go
@@ -0,0 +1,261 @@
+package memprovider
+
+import (
+	"context"
+	"github.com/open-feature/go-sdk/pkg/openfeature"
+	"testing"
+)
+
+func TestInMemoryProvider_boolean(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"boolFlag": {
+			Key:            "boolFlag",
+			State:          Enabled,
+			DefaultVariant: "true",
+			Variants: map[string]interface{}{
+				"true":  true,
+				"false": false,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test boolean success", func(t *testing.T) {
+		evaluation := memoryProvider.BooleanEvaluation(ctx, "boolFlag", false, nil)
+
+		if evaluation.Value != true {
+			t.Errorf("incorect evaluation, expected %t, got %t", true, evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_String(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"stringFlag": {
+			Key:            "stringFlag",
+			State:          Enabled,
+			DefaultVariant: "stringOne",
+			Variants: map[string]interface{}{
+				"stringOne": "hello",
+				"stringTwo": "GoodBye",
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test string success", func(t *testing.T) {
+		evaluation := memoryProvider.StringEvaluation(ctx, "stringFlag", "none", nil)
+
+		if evaluation.Value != "hello" {
+			t.Errorf("incorect evaluation, expected %s, got %s", "hello", evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_Float(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"floatFlag": {
+			Key:            "floatFlag",
+			State:          Enabled,
+			DefaultVariant: "fOne",
+			Variants: map[string]interface{}{
+				"fOne": 1.1,
+				"fTwo": 2.2,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test float success", func(t *testing.T) {
+		evaluation := memoryProvider.FloatEvaluation(ctx, "floatFlag", 1.0, nil)
+
+		if evaluation.Value != 1.1 {
+			t.Errorf("incorect evaluation, expected %f, got %f", 1.1, evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_Int(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"intFlag": {
+			Key:            "intFlag",
+			State:          Enabled,
+			DefaultVariant: "max",
+			Variants: map[string]interface{}{
+				"min": -9223372036854775808,
+				"max": 9223372036854775807,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test integer success", func(t *testing.T) {
+		evaluation := memoryProvider.IntEvaluation(ctx, "intFlag", 1, nil)
+
+		if evaluation.Value != 9223372036854775807 {
+			t.Errorf("incorect evaluation, expected %d, got %d", 1, evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_Object(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"objectFlag": {
+			Key:            "objectFlag",
+			State:          Enabled,
+			DefaultVariant: "A",
+			Variants: map[string]interface{}{
+				"A": "SomeResult",
+				"B": "OtherResult",
+			},
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test object success", func(t *testing.T) {
+		evaluation := memoryProvider.ObjectEvaluation(ctx, "objectFlag", "unknown", nil)
+
+		if evaluation.Value != "SomeResult" {
+			t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_WithContext(t *testing.T) {
+	var variantKey = "VariantSelector"
+
+	// simple context handling - variant is selected from key and returned
+	var evaluator = func(callerFlag InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail) {
+		s := evalCtx[variantKey]
+		return callerFlag.Variants[s.(string)], openfeature.ProviderResolutionDetail{}
+	}
+
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"contextFlag": {
+			Key:            "contextFlag",
+			State:          Enabled,
+			DefaultVariant: "true",
+			Variants: map[string]interface{}{
+				"true":  true,
+				"false": false,
+			},
+			ContextEvaluator: &evaluator,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test with context", func(t *testing.T) {
+
+		evaluation := memoryProvider.BooleanEvaluation(ctx, "contextFlag", true, map[string]interface{}{
+			variantKey: "false",
+		})
+
+		if evaluation.Value != false {
+			t.Errorf("incorect evaluation, expected %v, got %v", false, evaluation.Value)
+		}
+	})
+}
+
+func TestInMemoryProvider_MissingFlag(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{})
+
+	ctx := context.Background()
+
+	t.Run("test missing flag", func(t *testing.T) {
+		evaluation := memoryProvider.StringEvaluation(ctx, "missing-flag", "GoodBye", nil)
+
+		if evaluation.Value != "GoodBye" {
+			t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value)
+		}
+
+		if evaluation.Reason != openfeature.ErrorReason {
+			t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason)
+		}
+
+		if evaluation.ResolutionDetail().ErrorCode != openfeature.FlagNotFoundCode {
+			t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.ResolutionDetail().ErrorCode)
+		}
+	})
+}
+
+func TestInMemoryProvider_TypeMismatch(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"boolFlag": {
+			Key:            "boolFlag",
+			State:          Enabled,
+			DefaultVariant: "true",
+			Variants: map[string]interface{}{
+				"true":  true,
+				"false": false,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test type mismatch flag", func(t *testing.T) {
+		evaluation := memoryProvider.StringEvaluation(ctx, "boolFlag", "GoodBye", nil)
+
+		if evaluation.Value != "GoodBye" {
+			t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value)
+		}
+
+		if evaluation.ResolutionDetail().ErrorCode != openfeature.TypeMismatchCode {
+			t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason)
+		}
+	})
+}
+
+func TestInMemoryProvider_Disabled(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{
+		"boolFlag": {
+			Key:            "boolFlag",
+			State:          Disabled,
+			DefaultVariant: "true",
+			Variants: map[string]interface{}{
+				"true":  true,
+				"false": false,
+			},
+			ContextEvaluator: nil,
+		},
+	})
+
+	ctx := context.Background()
+
+	t.Run("test disabled flag", func(t *testing.T) {
+		evaluation := memoryProvider.BooleanEvaluation(ctx, "boolFlag", false, nil)
+
+		if evaluation.Value != false {
+			t.Errorf("incorect evaluation, expected %v, got %v", false, evaluation.Value)
+		}
+
+		if evaluation.Reason != openfeature.DisabledReason {
+			t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason)
+		}
+	})
+}
+
+func TestInMemoryProvider_Metadata(t *testing.T) {
+	memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{})
+
+	metadata := memoryProvider.Metadata()
+
+	if metadata.Name == "" {
+		t.Errorf("expected non-empty name for in-memory provider")
+	}
+
+	if metadata.Name != "InMemoryProvider" {
+		t.Errorf("incorrect name for in-memory provider")
+	}
+}