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

ytest match, README #103

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ run ":8080"

### yaptest: HTTP Test Framework

This classfile has the file suffix `_ytest.gox`.
yaptest is a web server testing framework. This classfile has the file suffix `_ytest.gox`.

Suppose we have a web server ([foo/get_p_#id.yap](ytest/demo/foo/get_p_%23id.yap)):

Expand Down
2 changes: 1 addition & 1 deletion gop.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import github.com/goplus/yap/ytest/auth/jwt

project _ytest.gox App github.com/goplus/yap/ytest github.com/goplus/yap/test

class _ytest.gox Case
class _ytest.gox CaseApp

import github.com/goplus/yap/ytest/auth/jwt

Expand Down
30 changes: 30 additions & 0 deletions test/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,27 @@ func toMapAny[T basetype](val map[string]T) map[string]any {
return ret
}

func tryToMapAny(val any) (ret map[string]any, ok bool) {
v := reflect.ValueOf(val)
return castMapAny(v)
}

func castMapAny(v reflect.Value) (ret map[string]any, ok bool) {
if v.Kind() != reflect.Map || v.Type().Key() != tyString {
return
}
ret, ok = make(map[string]any, v.Len()), true
for it := v.MapRange(); it.Next(); {
key := it.Key().String()
ret[key] = it.Value().Interface()
}
return
}

var (
tyString = reflect.TypeOf("")
)

// -----------------------------------------------------------------------------

type baseelem interface {
Expand Down Expand Up @@ -234,6 +255,11 @@ retry:
case *Var__1[map[string]any]:
Gopt_Case_MatchMap(t, ev, gv.Val(), name...)
return
default:
if gv, ok := tryToMapAny(got); ok {
Gopt_Case_MatchMap(t, ev, gv, name...)
return
}
}
case []any:
switch gv := got.(type) {
Expand Down Expand Up @@ -351,6 +377,10 @@ retry:

// other types:
default:
if v, ok := tryToMapAny(expected); ok {
expected = v
goto retry
}
if reflect.DeepEqual(expected, got) {
return
}
Expand Down
112 changes: 112 additions & 0 deletions ytest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
yaptest - Go+ HTTP Test Framework
=====

yaptest is a web server testing framework. This classfile has the file suffix `_ytest.gox`.

Before using `yaptest`, you need to add `github.com/goplus/yap` to `go.mod`:

```
gop get github.com/goplus/yap@latest
```

Suppose we have a web server ([foo/get_p_#id.yap](demo/foo/get_p_%23id.yap)):

```go
json {
"id": ${id},
}
```

Then we create a yaptest file ([foo/foo_ytest.gox](demo/foo/foo_ytest.gox)):

```go
mock "foo.com", new(AppV2) // name of any YAP v2 web server is `AppV2`

id := "123"
get "http://foo.com/p/${id}"
ret 200
json {
"id": id,
}
```

The directive `mock` creates the web server by [mockhttp](https://pkg.go.dev/github.com/qiniu/x/mockhttp). Then we write test code directly.

You can change the directive `mock` to `testServer` (see [foo/bar_ytest.gox](demo/foo/bar_ytest.gox)), and keep everything else unchanged:

```go
testServer "foo.com", new(AppV2)

id := "123"
get "http://foo.com/p/${id}"
ret 200
json {
"id": id,
}
```

The directive `testServer` creates the web server by [net/http/httptest](https://pkg.go.dev/net/http/httptest#NewServer) and obtained a random port as the service address. Then it calls the directive [host](https://pkg.go.dev/github.com/goplus/yap/ytest#App.Host) to map the random service address to `foo.com`. This makes all other code no need to changed.

## yaptest User Manual

### match

This is almost the core concept in `yaptest`. It matches two objects.

Let’s look at [a simple example](demo/match/simple/simple_yapt.gox) first:

```go
id := Var(int)
match id, 1+2
echo id
```

Here we define a variable called `id` and match it with expression `1+2`. If the variable is unbound, it is assigned the value of the expression.

So far, you've seen `match` like the assignment side. But you cannot assign a different value to a variable that has been bound:

```go
id := Var(int)
match id, 1+2
match id, 3
echo id

match id, 5 // unmatched value - expected: 3, got: 5
```

In the second match statement, the variable `id` has been bound. At this time, it will be compared with the expression value. If it is equal, it will succeed, otherwise an error will be reported (such as the third match statement above).

The `match` statement [can be complex](demo/match/complex/complex_yap.gox), such as:

```go
d := Var(string)

match {
"c": {"d": d},
}, {
"a": 1,
"b": 3.14,
"c": {"d": "hello", "e": "world"},
"f": 1,
}

echo d
match d, "hello"
```

Generally, the syntax of the match command is:

```go
match <ExpectedObject> <SourceObject>
```

Unbound variables are allowed in `<ExpectedObject>`, but cannot appear in `<SourceObject>`. `<ExpectedObject>` and `<SourceObject>` do not have to be exactly the same, but what appears in `<ExpectedObject>` must also appear in `<SourceObject>`. That is, it is required to be a subset relationship (`<ExpectedObject>` is a subset of `<SourceObject>`). If a variable in `<ExpectedObject>` has not been bound, it will be bound according to the value of the corresponding `<SourceObject>`; if the variable has been bound, the values on both sides must match.

The cornerstone of `yaptest` is matching grammar. Let's look at the next example you saw at the beginning:

```go
ret 200
json {
"id": id,
}
```
30 changes: 21 additions & 9 deletions ytest/case.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,14 @@ type CaseT = test.CaseT

type Case struct {
*Request
*App
test.Case
app *App

DefaultHeader http.Header
}

// Gopt_Case_TestMain is required by Go+ compiler as the entry of a YAP test case.
func Gopt_Case_TestMain(c interface{ initCase(*App, CaseT) }, t *testing.T) {
app := new(App).initApp()
c.initCase(app, test.NewT(t))
c.(interface{ Main() }).Main()
}

func (p *Case) initCase(app *App, t CaseT) {
p.App = app
p.app = app
p.CaseT = t
p.DefaultHeader = make(http.Header)
}
Expand Down Expand Up @@ -133,3 +126,22 @@ func (p *Case) DELETE(url string) *Request {
}

// -----------------------------------------------------------------------------

type CaseApp struct {
Case
*App
}

// Gopt_CaseApp_TestMain is required by Go+ compiler as the entry of a YAP test case.
func Gopt_CaseApp_TestMain(c interface{ initCaseApp(*App, CaseT) }, t *testing.T) {
app := new(App).initApp()
c.initCaseApp(app, test.NewT(t))
c.(interface{ Main() }).Main()
}

func (p *CaseApp) initCaseApp(app *App, t CaseT) {
p.initCase(app, t)
p.App = app
}

// -----------------------------------------------------------------------------
9 changes: 7 additions & 2 deletions ytest/classfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import (
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"

"github.com/goplus/yap"
"github.com/goplus/yap/test"
"github.com/goplus/yap/test/logt"
"github.com/qiniu/x/mockhttp"
)

const (
GopPackage = "github.com/goplus/yap/test"
GopPackage = "github.com/goplus/yap/test"
GopTestClass = true
)

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -96,8 +99,10 @@ func Gopt_App_Main(app interface{ initApp() *App }, workers ...interface{ initCa
if me, ok := app.(interface{ MainEntry() }); ok {
me.MainEntry()
}
t := logt.New()
for _, worker := range workers {
worker.initCase(a, nil)
worker.initCase(a, t)
reflect.ValueOf(worker).Elem().Field(1).Set(reflect.ValueOf(app)) // (*worker).App = app
worker.(interface{ Main() }).Main()
}
}
Expand Down
4 changes: 2 additions & 2 deletions ytest/demo/basic/gop_autogen_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions ytest/demo/foo/gop_autogen_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions ytest/demo/jwtdemo/gop_autogen_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions ytest/demo/match/complex/complex_yapt.gox
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
d := Var(string)

match {
"c": {"d": d},
}, {
"a": 1,
"b": 3.14,
"c": {"d": "hello", "e": "world"},
"f": 1,
}

echo d
match d, "hello"
39 changes: 39 additions & 0 deletions ytest/demo/match/complex/gop_autogen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions ytest/demo/match/hello/get_p_#id.yap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
json {
"id": ${id},
}
Loading