Skip to content

Commit

Permalink
Merge pull request #130 from eliseeviam/basedOnRequest-strategy
Browse files Browse the repository at this point in the history
new: basedOnRequest strategy implemented
  • Loading branch information
Nikita Tomchik authored Feb 25, 2022
2 parents 3b352f9 + dca563b commit 56e0afd
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 0 deletions.
39 changes: 39 additions & 0 deletions README-ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,45 @@ Example:
...
```

###### basedOnRequest

Использует разные стратегии ответа, в зависимости от пути запрашиваемого ресурса. Разрешает объявлять несколько стратегий для одного пути. Конкурентно безопасен.

При получении запроса на ресурс, который не задан в параметрах, тест считается проваленным.

Параметры:
- `uris` (обязательный) - список ресурсов, каждый ресурс можно сконфигурировать как отдельный мок-сервис, используя любые доступные проверки запросов и стратегии ответов (см. пример)

Example:
```yaml
...
mocks:
service1:
strategy: basedOnRequest
uris:
- strategy: constant
body: >
{
"ok": true
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value1"
- kind: pathMatches
path: /request
- strategy: constant
body: >
{
"ok": true
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value2"
- kind: pathMatches
path: /request
...
```

##### Подсчет количества вызовов

Вы можете указать, сколько раз должен быть вызван мок или отдельный ресурс мока (используя `uriVary`). Если фактическое количество вызовов будет отличаться от ожидаемого, тест будет считаться проваленным.
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,45 @@ Example:
...
```

###### basedOnRequest

Allows multiple requests with same request path. Concurrent safe.

When receiving a request for a resource that is not defined in the parameters, the test will be considered failed.

Parameters:
- `uris` (mandatory) - a list of resources, each resource can be configured as a separate mock-service using any available request constraints and response strategies (see example)

Example:
```yaml
...
mocks:
service1:
strategy: basedOnRequest
uris:
- strategy: constant
body: >
{
"ok": true
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value1"
- kind: pathMatches
path: /request
- strategy: constant
body: >
{
"ok": true
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value2"
- kind: pathMatches
path: /request
...
```

##### Calls count

You can define, how many times each mock or mock resource must be called (using `uriVary`). If the actual number of calls is different from expected, the test will be considered failed.
Expand Down
30 changes: 30 additions & 0 deletions examples/mock-based-on-request/cases/do.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
- name: Test concurrent with query mathing
mocks:
backend:
strategy: basedOnRequest
uris:
- strategy: constant
body: >
{
"value": 1
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value1"
- kind: pathMatches
path: /request
- strategy: constant
body: >
{
"value": 22
}
requestConstraints:
- kind: queryMatches
expectedQuery: "key=value2"
- kind: pathMatches
path: /request
method: GET
path: /do
response:
200: |
{"total":23}
28 changes: 28 additions & 0 deletions examples/mock-based-on-request/func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"net/http/httptest"
"os"
"testing"

"github.com/lamoda/gonkey/mocks"
"github.com/lamoda/gonkey/runner"
)

func TestProxy(t *testing.T) {
m := mocks.NewNop("backend")
if err := m.Start(); err != nil {
t.Fatal(err)
}
defer m.Shutdown()

os.Setenv("BACKEND_ADDR", m.Service("backend").ServerAddr())
initServer()
srv := httptest.NewServer(nil)

runner.RunWithTesting(t, &runner.RunWithTestingParams{
Server: srv,
TestsDir: "cases",
Mocks: m,
})
}
71 changes: 71 additions & 0 deletions examples/mock-based-on-request/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"encoding/json"
"fmt"
"golang.org/x/sync/errgroup"
"io"
"io/ioutil"
"log"
"net/http"
urlpkg "net/url"
"os"
"sync/atomic"
)

func main() {
initServer()
log.Fatal(http.ListenAndServe(":8080", nil))
}

func initServer() {
http.HandleFunc("/do", Do)
}

func Do(w http.ResponseWriter, r *http.Request) {
params1 := urlpkg.Values{"key": []string{"value1"}}.Encode()
params2 := urlpkg.Values{"key": []string{"value2"}}.Encode()
doRequest := func(params string) (int64, error) {
url := fmt.Sprintf("http://%s/request?%s", os.Getenv("BACKEND_ADDR"), params)
res, err := http.Get(url)
if err != nil {
return 0, err
}
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("backend response status code %d", res.StatusCode)
}
body, err := ioutil.ReadAll(res.Body)
_ = res.Body.Close()
if err != nil {
return 0, fmt.Errorf("cannot read response body %w", err)
}
var resp struct {
Value int64 `json:"value"`
}
err = json.Unmarshal(body, &resp)
if err != nil {
return 0, fmt.Errorf("cannot unmarshal response body %w", err)
}
return resp.Value, nil
}

var total int64
errg := errgroup.Group{}
errg.Go(func() error {
v, err := doRequest(params1)
atomic.AddInt64(&total, v)
return err
})
errg.Go(func() error {
v, err := doRequest(params2)
atomic.AddInt64(&total, v)
return err
})
err := errg.Wait()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = io.WriteString(w, err.Error())
return
}
_, _ = fmt.Fprintf(w, `{"total":%v}`, total)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.13.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
32 changes: 32 additions & 0 deletions mocks/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,35 @@ func (d *definition) EndRunningContext() []error {
}
return errs
}

func verifyRequestConstraints(requestConstraints []verifier, r *http.Request) []error {
var errors []error
if len(requestConstraints) > 0 {
requestDump, err := httputil.DumpRequest(r, true)
if err != nil {
requestDump = []byte(fmt.Sprintf("failed to dump request: %s", err))
}
for _, c := range requestConstraints {
errs := c.Verify(r)
for _, e := range errs {
errors = append(errors, &RequestConstraintError{
error: e,
Constraint: c,
RequestDump: requestDump,
})
}
}
}
return errors
}
func (d *definition) ExecuteWithoutVerifying(w http.ResponseWriter, r *http.Request) []error {
d.Lock()
d.calls++
d.Unlock()
if d.replyStrategy != nil {
return d.replyStrategy.HandleRequest(w, r)
}
return []error{
fmt.Errorf("reply strategy undefined"),
}
}
26 changes: 26 additions & 0 deletions mocks/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ func (l *Loader) loadStrategy(path, strategyName string, definition map[interfac
case "sequence":
*ak = append(*ak, "sequence")
return l.loadSequenceStrategy(path, definition)
case "basedOnRequest":
*ak = append(*ak, "basePath", "uris")
return l.loadBasedOnRequestStrategy(path, definition)
default:
return nil, fmt.Errorf("unknown strategy: %s", strategyName)
}
Expand Down Expand Up @@ -215,6 +218,29 @@ func (l *Loader) loadSequenceStrategy(path string, def map[interface{}]interface
return newSequentialReply(strategies), nil
}

func (l *Loader) loadBasedOnRequestStrategy(path string, def map[interface{}]interface{}) (replyStrategy, error) {
var uris []*definition
if u, ok := def["uris"]; ok {
urisList, ok := u.([]interface{})
if !ok {
return nil, errors.New("`basedOnRequest` requires list under `uris` key")
}
uris = make([]*definition, 0, len(urisList))
for i, v := range urisList {
v, ok := v.(map[interface{}]interface{})
if !ok {
return nil, errors.New("`uris` list item must be a map")
}
def, err := l.loadDefinition(path+"."+strconv.Itoa(i), v)
if err != nil {
return nil, err
}
uris = append(uris, def)
}
}
return newBasedOnRequestReply(uris), nil
}

func (l *Loader) loadHeaders(def map[interface{}]interface{}) (map[string]string, error) {
var headers map[string]string
if h, ok := def["headers"]; ok {
Expand Down
43 changes: 43 additions & 0 deletions mocks/reply_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,46 @@ func (s *sequentialReply) HandleRequest(w http.ResponseWriter, r *http.Request)
s.count++
return def.Execute(w, r)
}

type basedOnRequestReply struct {
sync.Mutex
replyStrategy
contextAwareStrategy

variants []*definition
}

func newBasedOnRequestReply(variants []*definition) replyStrategy {
return &basedOnRequestReply{
variants: variants,
}
}

func (s *basedOnRequestReply) HandleRequest(w http.ResponseWriter, r *http.Request) []error {
s.Lock()
defer s.Unlock()

var errors []error
for _, def := range s.variants {
errs := verifyRequestConstraints(def.requestConstraints, r)
if errs == nil {
return def.ExecuteWithoutVerifying(w, r)
}
errors = append(errors, errs...)
}
return append(errors, unhandledRequestError(r)...)
}

func (s *basedOnRequestReply) ResetRunningContext() {
for _, def := range s.variants {
def.ResetRunningContext()
}
}

func (s *basedOnRequestReply) EndRunningContext() []error {
var errs []error
for _, def := range s.variants {
errs = append(errs, def.EndRunningContext()...)
}
return errs
}

0 comments on commit 56e0afd

Please sign in to comment.