Skip to content

Commit

Permalink
Merge pull request #103 from xushiwei/q
Browse files Browse the repository at this point in the history
ytest match, README
  • Loading branch information
xushiwei authored Mar 11, 2024
2 parents 359028c + 020b89b commit 1187b09
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 23 deletions.
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

0 comments on commit 1187b09

Please sign in to comment.